com.door43.translationstudio.core.TargetTranslationMigrator.java Source code

Java tutorial

Introduction

Here is the source code for com.door43.translationstudio.core.TargetTranslationMigrator.java

Source

package com.door43.translationstudio.core;

import android.content.res.AssetManager;

import com.door43.translationstudio.AppContext;
import com.door43.translationstudio.rendering.USXtoUSFMConverter;
import com.door43.util.FileUtilities;
import com.door43.util.Manifest;

import org.apache.commons.io.FileUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

/**
 * Created by joel on 11/4/2015.
 */
public class TargetTranslationMigrator {

    private static final String MANIFEST_FILE = "manifest.json";
    public static final String LICENSE = "LICENSE";

    /**
     * Performs a migration on a manifest object.
     * We just throw it into a temporary directory and run the normal migration on it.
     * @param manifestJson
     * @return
     */
    public static JSONObject migrateManifest(JSONObject manifestJson) {
        File tempDir = new File(AppContext.context().getCacheDir(), System.currentTimeMillis() + "");
        // TRICKY: the migration can change the name of the translation dir so we nest it to avoid conflicts.
        File fakeTranslationDir = new File(tempDir, "translation");
        fakeTranslationDir.mkdirs();
        JSONObject migratedManifest = null;
        try {
            FileUtils.writeStringToFile(new File(fakeTranslationDir, "manifest.json"), manifestJson.toString());
            fakeTranslationDir = migrate(fakeTranslationDir);
            if (fakeTranslationDir != null) {
                migratedManifest = new JSONObject(
                        FileUtils.readFileToString(new File(fakeTranslationDir, "manifest.json")));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // clean up
            FileUtils.deleteQuietly(tempDir);
        }
        return migratedManifest;
    }

    /**
     * Performs necessary migration operations on a target translation
     * @param targetTranslationDir
     * @return the target translation dir. Null if the migration failed
     */
    public static File migrate(File targetTranslationDir) {
        File migratedDir = targetTranslationDir;
        File manifestFile = new File(targetTranslationDir, MANIFEST_FILE);
        try {
            JSONObject manifest = new JSONObject(FileUtils.readFileToString(manifestFile));
            int packageVersion = 2; // default to version 2 if no package version is available
            if (manifest.has("package_version")) {
                packageVersion = manifest.getInt("package_version");
            }
            switch (packageVersion) {
            case 2:
                migratedDir = v2(migratedDir);
                if (migratedDir == null)
                    break;
            case 3:
                migratedDir = v3(migratedDir);
                if (migratedDir == null)
                    break;
            case 4:
                migratedDir = v4(migratedDir);
                if (migratedDir == null)
                    break;
            case 5:
                migratedDir = v5(migratedDir);
                if (migratedDir == null)
                    break;
            case 6:
                migratedDir = v6(migratedDir);
                if (migratedDir == null)
                    break;
            default:
                if (migratedDir != null && !validateTranslationType(migratedDir)) {
                    migratedDir = null;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            migratedDir = null;
        }
        return migratedDir;
    }

    /**
     * current version
     * @param path
     * @return
     * @throws Exception
     */
    private static File v6(File path) throws Exception {
        return path;
    }

    /**
     * Updated the id format of target translations
     * @param path
     * @return
     */
    private static File v5(File path) throws Exception {
        File manifestFile = new File(path, MANIFEST_FILE);
        JSONObject manifest = new JSONObject(FileUtils.readFileToString(manifestFile));

        // pull info to build id
        String targetLanguageCode = manifest.getJSONObject("target_language").getString("id");
        String projectSlug = manifest.getJSONObject("project").getString("id");
        String translationTypeSlug = manifest.getJSONObject("type").getString("id");
        String resourceSlug = null;
        if (translationTypeSlug.equals("text")) {
            resourceSlug = manifest.getJSONObject("resource").getString("id");
        }

        // build new id
        String id = targetLanguageCode + "_" + projectSlug + "_" + translationTypeSlug;
        if (translationTypeSlug.equals("text") && resourceSlug != null) {
            id += "_" + resourceSlug;
        }

        // add license file
        File licenseFile = new File(path, "LICENSE.md");
        if (!licenseFile.exists()) {
            AssetManager am = AppContext.context().getAssets();
            InputStream is = am.open("LICENSE.md");
            if (is != null) {
                FileUtils.copyInputStreamToFile(is, licenseFile);
            } else {
                throw new Exception("Failed to open the template license file");
            }
        }

        // update package version
        manifest.put("package_version", 6);
        FileUtils.write(manifestFile, manifest.toString(2));

        // update target translation dir name
        File newPath = new File(path.getParentFile(), id.toLowerCase());
        FileUtilities.safeDelete(newPath);
        FileUtils.moveDirectory(path, newPath);
        return newPath;
    }

    /**
     * major restructuring of the manifest to provide better support for future front/back matter, drafts, rendering,
     * and resolves issues between desktop and android platforms.
     * @param path
     * @return
     */
    private static File v4(File path) throws Exception {
        File manifestFile = new File(path, MANIFEST_FILE);
        JSONObject manifest = new JSONObject(FileUtils.readFileToString(manifestFile));

        // type
        {
            String typeId = "text";
            if (manifest.has("project")) {
                try {
                    JSONObject projectJson = manifest.getJSONObject("project");
                    typeId = projectJson.getString("type");
                    projectJson.remove("type");
                    manifest.put("project", projectJson);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            JSONObject typeJson = new JSONObject();
            TranslationType translationType = TranslationType.get(typeId);
            typeJson.put("id", typeId);
            if (translationType != null) {
                typeJson.put("name", translationType.getName());
            } else {
                typeJson.put("name", "");
            }
            manifest.put("type", typeJson);
        }

        // update project
        // NOTE: this was actually in v3 but we missed it so we need to catch it here
        if (manifest.has("project_id")) {
            String projectId = manifest.getString("project_id");
            manifest.remove("project_id");
            JSONObject projectJson = new JSONObject();
            projectJson.put("id", projectId);
            projectJson.put("name", projectId.toUpperCase()); // we don't know the full name at this point
            manifest.put("project", projectJson);
        }

        // update resource
        if (manifest.getJSONObject("type").getString("id").equals("text")) {
            if (manifest.has("resource_id")) {
                String resourceId = manifest.getString("resource_id");
                manifest.remove("resource_id");
                JSONObject resourceJson = new JSONObject();
                // TRICKY: supported resource id's (or now types) are "reg", "obs", "ulb", and "udb".
                if (resourceId.equals("ulb")) {
                    resourceJson.put("name", "Unlocked Literal Bible");
                } else if (resourceId.equals("udb")) {
                    resourceJson.put("name", "Unlocked Dynamic Bible");
                } else if (resourceId.equals("obs")) {
                    resourceJson.put("name", "Open Bible Stories");
                } else {
                    // everything else changes to "reg"
                    resourceId = "reg";
                    resourceJson.put("name", "Regular");
                }
                resourceJson.put("id", resourceId);
                manifest.put("resource", resourceJson);
            } else if (!manifest.has("resource")) {
                // add missing resource
                JSONObject resourceJson = new JSONObject();
                JSONObject projectJson = manifest.getJSONObject("project");
                JSONObject typeJson = manifest.getJSONObject("type");
                if (typeJson.getString("id").equals("text")) {
                    String resourceId = projectJson.getString("id");
                    if (resourceId.equals("obs")) {
                        resourceJson.put("id", "obs");
                        resourceJson.put("name", "Open Bible Stories");
                    } else {
                        // everything else changes to reg
                        resourceJson.put("id", "reg");
                        resourceJson.put("name", "Regular");
                    }
                    manifest.put("resource", resourceJson);
                }
            }
        } else {
            // non-text translation types do not have resources
            manifest.remove("resource_id");
            manifest.remove("resource");
        }

        // update source translations
        if (manifest.has("source_translations")) {
            JSONObject oldSourceTranslationsJson = manifest.getJSONObject("source_translations");
            manifest.remove("source_translations");
            JSONArray newSourceTranslationsJson = new JSONArray();
            Iterator<String> keys = oldSourceTranslationsJson.keys();
            while (keys.hasNext()) {
                try {
                    String key = keys.next();
                    JSONObject oldObj = oldSourceTranslationsJson.getJSONObject(key);
                    JSONObject sourceTranslation = new JSONObject();
                    String[] parts = key.split("-", 2);
                    if (parts.length == 2) {
                        String languageResourceId = parts[1];
                        String[] pieces = languageResourceId.split("-");
                        if (pieces.length > 0) {
                            String resId = pieces[pieces.length - 1];
                            sourceTranslation.put("resource_id", resId);
                            sourceTranslation.put("language_id", languageResourceId.substring(0,
                                    languageResourceId.length() - resId.length() - 1));
                            sourceTranslation.put("checking_level", oldObj.getString("checking_level"));
                            sourceTranslation.put("date_modified", oldObj.getInt("date_modified"));
                            sourceTranslation.put("version", oldObj.getString("version"));
                            newSourceTranslationsJson.put(sourceTranslation);
                        }
                    }
                } catch (Exception e) {
                    // don't fail migration just because a source translation was invalid
                    e.printStackTrace();
                }
            }
            manifest.put("source_translations", newSourceTranslationsJson);
        }

        // update parent draft
        if (manifest.has("parent_draft_resource_id")) {
            JSONObject draftStatus = new JSONObject();
            draftStatus.put("resource_id", manifest.getString("parent_draft_resource_id"));
            draftStatus.put("checking_entity", "");
            draftStatus.put("checking_level", "");
            draftStatus.put("comments", "The parent draft is unknown");
            draftStatus.put("contributors", "");
            draftStatus.put("publish_date", "");
            draftStatus.put("source_text", "");
            draftStatus.put("source_text_version", "");
            draftStatus.put("version", "");
            manifest.put("parent_draft", draftStatus);
            manifest.remove("parent_draft_resource_id");
        }

        // update finished chunks
        if (manifest.has("finished_frames")) {
            JSONArray finishedFrames = manifest.getJSONArray("finished_frames");
            manifest.remove("finished_frames");
            manifest.put("finished_chunks", finishedFrames);
        }

        // remove finished titles
        if (manifest.has("finished_titles")) {
            JSONArray finishedChunks = manifest.getJSONArray("finished_chunks");
            JSONArray finishedTitles = manifest.getJSONArray("finished_titles");
            manifest.remove("finished_titles");
            for (int i = 0; i < finishedTitles.length(); i++) {
                String chapterId = finishedTitles.getString(i);
                finishedChunks.put(chapterId + "-title");
            }
            manifest.put("finished_chunks", finishedChunks);
        }

        // remove finished references
        if (manifest.has("finished_references")) {
            JSONArray finishedChunks = manifest.getJSONArray("finished_chunks");
            JSONArray finishedReferences = manifest.getJSONArray("finished_references");
            manifest.remove("finished_references");
            for (int i = 0; i < finishedReferences.length(); i++) {
                String chapterId = finishedReferences.getString(i);
                finishedChunks.put(chapterId + "-reference");
            }
            manifest.put("finished_chunks", finishedChunks);
        }

        // remove project components
        // NOTE: this was never quite official, just in android
        if (manifest.has("finished_project_components")) {
            JSONArray finishedChunks = manifest.getJSONArray("finished_chunks");
            JSONArray finishedProjectComponents = manifest.getJSONArray("finished_project_components");
            manifest.remove("finished_project_components");
            for (int i = 0; i < finishedProjectComponents.length(); i++) {
                String component = finishedProjectComponents.getString(i);
                finishedChunks.put("00-" + component);
            }
            manifest.put("finished_chunks", finishedChunks);
        }

        // add format
        if (!Manifest.valueExists(manifest, "format") || manifest.getString("format").equals("usx")
                || manifest.getString("format").equals("default")) {
            String typeId = manifest.getJSONObject("type").getString("id");
            String projectId = manifest.getJSONObject("project").getString("id");
            if (!typeId.equals("text") || projectId.equals("obs")) {
                manifest.put("format", "markdown");
            } else {
                manifest.put("format", "usfm");
            }
        }

        // update where project title is saved.
        File oldProjectTitle = new File(path, "title.txt");
        File newProjectTitle = new File(path, "00/title.txt");
        if (oldProjectTitle.exists()) {
            newProjectTitle.getParentFile().mkdirs();
            FileUtils.moveFile(oldProjectTitle, newProjectTitle);
        }

        // update package version
        manifest.put("package_version", 5);

        FileUtils.write(manifestFile, manifest.toString(2));

        // migrate usx to usfm
        String format = manifest.getString("format");
        // TRICKY: we just added the new format field, anything marked as usfm may have residual usx and needs to be migrated
        if (format.equals("usfm")) {
            File[] chapterDirs = path.listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return pathname.isDirectory() && !pathname.getName().equals(".git");
                }
            });
            for (File cDir : chapterDirs) {
                File[] chunkFiles = cDir.listFiles();
                for (File chunkFile : chunkFiles) {
                    try {
                        String usx = FileUtils.readFileToString(chunkFile);
                        String usfm = USXtoUSFMConverter.doConversion(usx).toString();
                        FileUtils.writeStringToFile(chunkFile, usfm);
                    } catch (IOException e) {
                        // this conversion may have failed but don't stop the rest of the migration
                        e.printStackTrace();
                    }
                }
            }
        }

        return path;
    }

    /**
     * We changed how the translator information is stored
     * we no longer store sensitive information like email and phone number
     * @param path
     * @return
     */
    private static File v3(File path) throws Exception {
        File manifestFile = new File(path, MANIFEST_FILE);
        JSONObject manifest = new JSONObject(FileUtils.readFileToString(manifestFile));
        if (manifest.has("translators")) {
            JSONArray legacyTranslators = manifest.getJSONArray("translators");
            JSONArray translators = new JSONArray();
            for (int i = 0; i < legacyTranslators.length(); i++) {
                Object obj = legacyTranslators.get(i);
                if (obj instanceof JSONObject) {
                    translators.put(((JSONObject) obj).getString("name"));
                } else if (obj instanceof String) {
                    translators.put(obj);
                }
            }
            manifest.put("translators", translators);
            manifest.put("package_version", 4);
            FileUtils.write(manifestFile, manifest.toString(2));
        }
        migrateChunkChanges(path);
        return path;
    }

    /**
     * upgrade from v2
     * @param path
     * @return
     */
    private static File v2(File path) throws Exception {
        File manifestFile = new File(path, MANIFEST_FILE);
        JSONObject manifest = new JSONObject(FileUtils.readFileToString(manifestFile));
        // fix finished frames
        if (manifest.has("frames")) {
            JSONObject legacyFrames = manifest.getJSONObject("frames");
            Iterator<String> keys = legacyFrames.keys();
            JSONArray finishedFrames = new JSONArray();
            while (keys.hasNext()) {
                String key = keys.next();
                JSONObject frameState = legacyFrames.getJSONObject(key);
                boolean finished = false;
                if (frameState.has("finished")) {
                    finished = frameState.getBoolean("finished");
                }
                if (finished) {
                    finishedFrames.put(key);
                }
            }
            manifest.remove("frames");
            manifest.put("finished_frames", finishedFrames);
        }
        // fix finished chapter titles and references
        if (manifest.has("chapters")) {
            JSONObject legacyChapters = manifest.getJSONObject("chapters");
            Iterator<String> keys = legacyChapters.keys();
            JSONArray finishedTitles = new JSONArray();
            JSONArray finishedReferences = new JSONArray();
            while (keys.hasNext()) {
                String key = keys.next();
                JSONObject chapterState = legacyChapters.getJSONObject(key);
                boolean finishedTitle = false;
                boolean finishedReference = false;
                if (chapterState.has("finished_title")) {
                    finishedTitle = chapterState.getBoolean("finished_title");
                }
                if (chapterState.has("finished_reference")) {
                    finishedTitle = chapterState.getBoolean("finished_reference");
                }
                if (finishedTitle) {
                    finishedTitles.put(key);
                }
                if (finishedReference) {
                    finishedReferences.put(key);
                }
            }
            manifest.remove("chapters");
            manifest.put("finished_titles", finishedTitles);
            manifest.put("finished_references", finishedReferences);
        }
        // fix project id
        if (manifest.has("slug")) {
            String projectSlug = manifest.getString("slug");
            manifest.remove("slug");
            manifest.put("project_id", projectSlug);
        }
        // fix target language id
        JSONObject targetLanguage = manifest.getJSONObject("target_language");
        if (targetLanguage.has("slug")) {
            String targetLanguageSlug = targetLanguage.getString("slug");
            targetLanguage.remove("slug");
            targetLanguage.put("id", targetLanguageSlug);
            manifest.put("target_language", targetLanguage);
        }

        manifest.put("package_version", 3);
        FileUtils.write(manifestFile, manifest.toString(2));
        return path;
    }

    /**
     * Merges chunks found in a target translation Project that do not exist in the source translation
     * to a sibling chunk so that no data is lost.
     * @param targetTranslationDir
     * @return
     */
    private static boolean migrateChunkChanges(File targetTranslationDir) {
        // TRICKY: calling the AppContext here is bad practice, but we'll deprecate this soon anyway.
        final Library library = AppContext.getLibrary();
        final SourceTranslation sourceTranslation = library
                .getDefaultSourceTranslation(targetTranslationDir.getName(), "en");
        if (sourceTranslation == null) {
            // if there is no source we are done
            return true;
        }
        File[] chapterDirs = targetTranslationDir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.isDirectory() && !pathname.getName().equals(".git")
                        && !pathname.getName().equals("00"); // 00 contains project title translations
            }
        });
        for (File cDir : chapterDirs) {
            mergeInvalidChunksInChapter(library, new File(targetTranslationDir, "manifest.json"), sourceTranslation,
                    cDir);
        }
        return true;
    }

    /**
     * Merges invalid chunks found in the target translation with a valid sibling chunk in order
     * to preserve translation data. Merged chunks are marked as not finished to force
     * translators to review the changes.
     * @param library
     * @param manifestFile
     * @param sourceTranslation
     * @param chapterDir
     * @return
     */
    private static boolean mergeInvalidChunksInChapter(final Library library, File manifestFile,
            final SourceTranslation sourceTranslation, final File chapterDir) {
        JSONObject manifest;
        try {
            manifest = new JSONObject(FileUtils.readFileToString(manifestFile));
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        final String chunkMergeMarker = "\n----------\n";
        File[] frameFiles = chapterDir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return !pathname.getName().equals("title.txt") && !pathname.getName().equals("reference.txt");
            }
        });
        String invalidChunks = "";
        File lastValidFrameFile = null;
        String chapterId = chapterDir.getName();
        for (File frameFile : frameFiles) {
            String frameId = frameFile.getName();
            Frame frame = library.getFrame(sourceTranslation, chapterId, frameId);
            String frameBody = "";
            try {
                frameBody = FileUtils.readFileToString(frameFile).trim();
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (frame != null) {
                lastValidFrameFile = frameFile;
                // merge invalid frames into the existing frame
                if (!invalidChunks.isEmpty()) {
                    try {
                        FileUtils.writeStringToFile(frameFile, invalidChunks + frameBody);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    invalidChunks = "";
                    try {
                        Manifest.removeValue(manifest.getJSONArray("finished_frames"), chapterId + "-" + frameId);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            } else if (!frameBody.isEmpty()) {
                // collect invalid frame
                if (lastValidFrameFile == null) {
                    invalidChunks += frameBody + chunkMergeMarker;
                } else {
                    // append to last valid frame
                    String lastValidFrameBody = "";
                    try {
                        lastValidFrameBody = FileUtils.readFileToString(lastValidFrameFile);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    try {
                        FileUtils.writeStringToFile(lastValidFrameFile,
                                lastValidFrameBody + chunkMergeMarker + frameBody);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    try {
                        Manifest.removeValue(manifest.getJSONArray("finished_frames"),
                                chapterId + "-" + lastValidFrameFile.getName());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
                // delete invalid frame
                FileUtils.deleteQuietly(frameFile);
            }
        }
        // clean up remaining invalid chunks
        if (!invalidChunks.isEmpty()) {
            // grab updated list of frames
            frameFiles = chapterDir.listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    return !pathname.getName().equals("title.txt") && !pathname.getName().equals("reference.txt");
                }
            });
            if (frameFiles != null && frameFiles.length > 0) {
                String frameBody = "";
                try {
                    frameBody = FileUtils.readFileToString(frameFiles[0]);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    FileUtils.writeStringToFile(frameFiles[0], invalidChunks + chunkMergeMarker + frameBody);
                    try {
                        Manifest.removeValue(manifest.getJSONArray("finished_frames"),
                                chapterId + "-" + frameFiles[0].getName());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return true;
    }

    /**
     * Checks if the android app can support this translation type.
     * Example: ts-desktop can translate tW but ts-android cannot.
     * @param path
     * @return
     */
    private static boolean validateTranslationType(File path) throws Exception {
        JSONObject manifest = new JSONObject(FileUtils.readFileToString(new File(path, MANIFEST_FILE)));
        String typeId = manifest.getJSONObject("type").getString("id");
        // android only supports TEXT translations for now
        return TranslationType.get(typeId) == TranslationType.TEXT;
    }
}