package com.door43.translationstudio.core;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.support.annotation.Nullable;
import com.door43.tools.reporting.Logger;
import com.door43.translationstudio.git.Repo;
import com.door43.translationstudio.util.NumericStringComparator;
import com.door43.util.Manifest;
import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.CreateBranchCommand;
import org.eclipse.jgit.api.DeleteBranchCommand;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListTagCommand;
import org.eclipse.jgit.api.LogCommand;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.TagCommand;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.FetchResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
* Created by joel on 8/29/2015.
*/
public class TargetTranslation {
public static final String TAG = TargetTranslation.class.getSimpleName();
public static final int PACKAGE_VERSION = 6; // the version of the target translation implementation
public static final String LICENSE_FILE = "LICENSE.md";
public static final String OBS_LICENSE_FILE = "OBS_LICENSE.md";
private static final String FIELD_PARENT_DRAFT = "parent_draft";
private static final String FIELD_FINISHED_CHUNKS = "finished_chunks";
private static final String FIELD_TRANSLATORS = "translators";
public static final String FIELD_MANIFEST_TARGET_LANGUAGE = "target_language";
public static final String FIELD_MANIFEST_FORMAT = "format";
public static final String FIELD_MANIFEST_RESOURCE = "resource";
public static final String FIELD_SOURCE_TRANSLATIONS = "source_translations";
public static final String FIELD_MANIFEST_PACKAGE_VERSION = "package_version";
public static final String FIELD_MANIFEST_PROJECT = "project";
public static final String FIELD_MANIFEST_GENERATOR = "generator";
public static final String FIELD_MANIFEST_TRANSLATION_TYPE = "type";
public static final String FIELD_TRANSLATION_FORMAT = "format";
public static final String FIELD_MANIFEST_ID = "id";
public static final String FIELD_MANIFEST_NAME = "name";
public static final String FIELD_MANIFEST_BUILD = "build";
public static final String APPLICATION_NAME = "ts-android";
private final File targetTranslationDir;
private final Manifest manifest;
private final String targetLanguageId;
private final String targetLanguageName;
private final LanguageDirection targetLanguageDirection;
private final String projectId;
private final String projectName;
private final TranslationType translationType;
private final String translationTypeName;
private String resourceSlug = null;
private String resourceName = null;
private TranslationFormat mTranslationFormat;
private PersonIdent author = null;
/**
* Creates a new instance of the target translation
* @param targetTranslationDir
*/
private TargetTranslation(File targetTranslationDir) throws Exception {
this.targetTranslationDir = targetTranslationDir;
this.manifest = Manifest.generate(targetTranslationDir);
// target language
JSONObject targetLanguageJson = this.manifest.getJSONObject(FIELD_MANIFEST_TARGET_LANGUAGE);
this.targetLanguageId = targetLanguageJson.getString(FIELD_MANIFEST_ID);
this.targetLanguageName = Manifest.valueExists(targetLanguageJson, FIELD_MANIFEST_NAME) ? targetLanguageJson.getString(FIELD_MANIFEST_NAME) : this.targetLanguageId.toUpperCase();
this.targetLanguageDirection = LanguageDirection.get(targetLanguageJson.getString("direction"));
// project
JSONObject projectJson = this.manifest.getJSONObject(FIELD_MANIFEST_PROJECT);
this.projectId = projectJson.getString(FIELD_MANIFEST_ID);
this.projectName = Manifest.valueExists(projectJson, FIELD_MANIFEST_NAME) ? projectJson.getString(FIELD_MANIFEST_NAME) : this.projectId.toUpperCase();
// translation type
JSONObject typeJson = this.manifest.getJSONObject(FIELD_MANIFEST_TRANSLATION_TYPE);
this.translationType = TranslationType.get(typeJson.getString(FIELD_MANIFEST_ID));
this.translationTypeName = Manifest.valueExists(typeJson, FIELD_MANIFEST_NAME) ? typeJson.getString(FIELD_MANIFEST_NAME) : this.translationType.toString().toUpperCase();
if(this.translationType == TranslationType.TEXT) {
// resource
JSONObject resourceJson = this.manifest.getJSONObject(FIELD_MANIFEST_RESOURCE);
this.resourceSlug = resourceJson.getString(FIELD_MANIFEST_ID);
this.resourceName = Manifest.valueExists(resourceJson, FIELD_MANIFEST_NAME) ? resourceJson.getString(FIELD_MANIFEST_NAME) : this.resourceSlug.toUpperCase();
}
mTranslationFormat = readTranslationFormat();
}
/**
* Returns the id of the target translation
* @return
*/
public String getId() {
return generateTargetTranslationId(this.targetLanguageId, this.projectId, this.translationType, this.resourceSlug);
}
/**
* Returns a properly formatted target translation id
* @param targetLanguageSlug
* @param projectSlug
* @param translationType
* @param resourceSlug
* @return
*/
public static String generateTargetTranslationId(String targetLanguageSlug, String projectSlug, TranslationType translationType, String resourceSlug) {
String id = targetLanguageSlug + "_" + projectSlug + "_" + translationType;
if(translationType == TranslationType.TEXT && resourceSlug != null) {
id += "_" + resourceSlug;
}
return id.toLowerCase();
}
/**
* Returns the resource slug
* @return
*/
public String getResourceSlug() {
return this.resourceSlug;
}
/**
* Returns the directory to this target translation
* @return
*/
public File getPath() {
return targetTranslationDir;
}
/**
* Returns the language direction of the target language
* @return
*/
public LanguageDirection getTargetLanguageDirection() {
return targetLanguageDirection;
}
/**
* Returns the name of the target language
* @return
*/
public String getTargetLanguageName() {
return targetLanguageName;
}
/**
* Returns the id of the project being translated
* @return
*/
public String getProjectId() {
return projectId;
}
/**
* Returns the id of the target language the project is being translated into
* @return
*/
public String getTargetLanguageId() {
return targetLanguageId;
}
/**
* get format of translation
* @return
*/
public TranslationFormat getFormat() {
return mTranslationFormat;
}
/**
* read the format of the translation
* @return
*/
private TranslationFormat readTranslationFormat() {
TranslationFormat format = fetchTranslationFormat(manifest);
if(null == format) {
TranslationType translationType = fetchTranslationType(manifest);
if(translationType != TranslationType.TEXT) {
return TranslationFormat.MARKDOWN;
} else {
String projectIdStr = fetchProjectID(manifest);
if("obs".equalsIgnoreCase(projectIdStr)) {
return TranslationFormat.MARKDOWN;
}
return TranslationFormat.USFM;
}
}
return format;
}
private static TranslationFormat fetchTranslationFormat(Manifest manifest) {
String formatStr = manifest.getString(FIELD_TRANSLATION_FORMAT);
return TranslationFormat.get(formatStr);
}
public static String fetchProjectID(Manifest manifest) {
String projectIdStr = "";
JSONObject projectIdJson = manifest.getJSONObject(FIELD_MANIFEST_PROJECT);
if(projectIdJson != null) {
try {
projectIdStr = projectIdJson.getString(FIELD_MANIFEST_ID);
} catch(Exception e) {
projectIdStr = "";
}
}
return projectIdStr;
}
public static TranslationType fetchTranslationType(Manifest manifest) {
String translationTypeStr = "";
JSONObject typeJson = manifest.getJSONObject(FIELD_MANIFEST_TRANSLATION_TYPE);
if(typeJson != null) {
try {
translationTypeStr = typeJson.getString(FIELD_MANIFEST_ID);
} catch(Exception e) {
translationTypeStr = "";
}
}
return TranslationType.get(translationTypeStr);
}
/**
* Opens an existing target translation.
* @param targetTranslationDir
* @return null if the directory does not exist or the manifest is invalid
*/
public static TargetTranslation open(File targetTranslationDir) {
if(targetTranslationDir != null) {
File manifestFile = new File(targetTranslationDir, "manifest.json");
if (manifestFile.exists()) {
try {
JSONObject manifest = new JSONObject(FileUtils.readFileToString(manifestFile));
int version = manifest.getInt(FIELD_MANIFEST_PACKAGE_VERSION);
if (version == PACKAGE_VERSION) {
return new TargetTranslation(targetTranslationDir);
} else {
Logger.w(TargetTranslation.class.getName(), "Unsupported target translation version " + version + " in" + targetTranslationDir.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
Logger.w(TargetTranslation.class.getName(), "Missing manifest file in target translation " + targetTranslationDir.getName());
}
}
return null;
}
/**
* Creates a new target translation
* @param translator
* @param translationFormat
* @param targetLanguage
* @param projectId
* @param translationType
* @param resourceSlug
* @param packageInfo
* @param targetTranslationDir
* @return
* @throws Exception
*/
public static TargetTranslation create(Context context, NativeSpeaker translator, TranslationFormat translationFormat, TargetLanguage targetLanguage, String projectId, TranslationType translationType, String resourceSlug, PackageInfo packageInfo, File targetTranslationDir) throws Exception {
targetTranslationDir.mkdirs();
Manifest manifest = Manifest.generate(targetTranslationDir);
// build new manifest
JSONObject projectJson = new JSONObject();
projectJson.put(FIELD_MANIFEST_ID, projectId);
projectJson.put(FIELD_MANIFEST_NAME, "");
manifest.put(FIELD_MANIFEST_PROJECT, projectJson);
JSONObject typeJson = new JSONObject();
typeJson.put(FIELD_MANIFEST_ID, translationType);
typeJson.put(FIELD_MANIFEST_NAME, translationType.getName());
manifest.put(FIELD_MANIFEST_TRANSLATION_TYPE, typeJson);
JSONObject generatorJson = new JSONObject();
generatorJson.put(FIELD_MANIFEST_NAME, APPLICATION_NAME);
generatorJson.put(FIELD_MANIFEST_BUILD, packageInfo.versionCode);
manifest.put(FIELD_MANIFEST_GENERATOR, generatorJson);
manifest.put(FIELD_MANIFEST_PACKAGE_VERSION, PACKAGE_VERSION);
manifest.put(FIELD_MANIFEST_TARGET_LANGUAGE, targetLanguage.toJson());
manifest.put(FIELD_MANIFEST_FORMAT, translationFormat);
JSONObject resourceJson = new JSONObject();
resourceJson.put(FIELD_MANIFEST_ID, resourceSlug);
manifest.put(FIELD_MANIFEST_RESOURCE, resourceJson);
File licenseFile = new File(targetTranslationDir, LICENSE_FILE);
InputStream is;
if(projectId.toLowerCase().equals("obs")) {
is = context.getAssets().open(OBS_LICENSE_FILE);
} else {
is = context.getAssets().open(LICENSE_FILE);
}
if(is != null) {
FileUtils.copyInputStreamToFile(is, licenseFile);
} else {
throw new FileNotFoundException("The template LICENSE.md file could not be found in the assets");
}
// return the new target translation
TargetTranslation targetTranslation = new TargetTranslation(targetTranslationDir);
targetTranslation.addContributor(translator);
return targetTranslation;
}
/**
* Updates the recorded information about the generator of the target translation
* @param context
* @param targetTranslation
* @throws Exception
*/
public static void updateGenerator(Context context, TargetTranslation targetTranslation) throws Exception{
JSONObject generatorJson = new JSONObject();
generatorJson.put(FIELD_MANIFEST_NAME, "ts-android");
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
generatorJson.put(FIELD_MANIFEST_BUILD, pInfo.versionCode);
targetTranslation.manifest.put(FIELD_MANIFEST_GENERATOR, generatorJson);
}
/**
* Returns the id of the project of the target translation
* @param targetTranslationId the target translation id
* @return
*/
public static String getProjectSlugFromId(String targetTranslationId) throws StringIndexOutOfBoundsException {
String[] complexId = targetTranslationId.split("_");
if(complexId.length >= 3) {
return complexId[1];
} else {
throw new StringIndexOutOfBoundsException("malformed target translation id " + targetTranslationId);
}
}
/**
* Returns the id of the target lanugage of the target translation
* @param targetTranslationId the target translation id
* @return
*/
public static String getTargetLanguageSlugFromId(String targetTranslationId) throws StringIndexOutOfBoundsException {
String[] complexId = targetTranslationId.split("_");
if(complexId.length >= 3) {
return complexId[0];
} else {
throw new StringIndexOutOfBoundsException("malformed target translation id" + targetTranslationId);
}
}
/**
* Generates the file to the directory where the target translation is located
*
* @param targetTranslationId the language to which the project is being translated
* @param rootDir the directory where the target translations are stored
* @return
*/
public static File generateTargetTranslationDir(String targetTranslationId, File rootDir) {
return new File(rootDir, targetTranslationId);
}
/**
* Adds a source translation to the list of used sources
* This is used for tracking what source translations are used to create a target translation
*
* @param sourceTranslation
* @throws JSONException
*/
public void addSourceTranslation(SourceTranslation sourceTranslation) throws JSONException {
JSONArray sourceTranslationsJson = manifest.getJSONArray(FIELD_SOURCE_TRANSLATIONS);
// check for duplicate
boolean foundDuplicate = false;
for(int i = 0; i < sourceTranslationsJson.length(); i ++) {
JSONObject obj = sourceTranslationsJson.getJSONObject(i);
if(obj.getString("language_id").equals(sourceTranslation.sourceLanguageSlug) && obj.getString("resource_id").equals(sourceTranslation.resourceSlug)) {
foundDuplicate = true;
break;
}
}
if(!foundDuplicate) {
JSONObject translationJson = new JSONObject();
translationJson.put("language_id", sourceTranslation.sourceLanguageSlug);
translationJson.put("resource_id", sourceTranslation.resourceSlug);
translationJson.put("checking_level", sourceTranslation.getCheckingLevel());
translationJson.put("date_modified", sourceTranslation.getDateModified());
translationJson.put("version", sourceTranslation.getVersion());
sourceTranslationsJson.put(translationJson);
manifest.put(FIELD_SOURCE_TRANSLATIONS, sourceTranslationsJson);
}
}
/**
* get list of source translation slugs used
*/
public String[] getSourceTranslations() {
try {
List<String> sources = new ArrayList<>();
JSONArray sourceTranslationsJson = manifest.getJSONArray(FIELD_SOURCE_TRANSLATIONS);
for (int i = 0; i < sourceTranslationsJson.length(); i++) {
JSONObject obj = sourceTranslationsJson.getJSONObject(i);
String sourceLanguageSlug = obj.getString("language_id");
String resourceSlug = obj.getString("resource_id");
SourceTranslation sourceTranslation = SourceTranslation.simple(this.projectId, sourceLanguageSlug, resourceSlug);
sources.add(sourceTranslation.getId());
}
return sources.toArray(new String[sources.size()]);
} catch(Exception e) {
Logger.e(TAG, "Error reading sources", e);
}
return new String[0]; // return empty array on error
}
/**
* Adds a native speaker as a translator
* This will replace contributors with the same name
* @param speaker
*/
public void addContributor(NativeSpeaker speaker) {
if(speaker != null) {
removeContributor(speaker);
JSONArray translatorsJson = manifest.getJSONArray(FIELD_TRANSLATORS);
translatorsJson.put(speaker.name);
manifest.put(FIELD_TRANSLATORS, translatorsJson);
}
}
/**
* Removes a native speaker from the list of translators
* This will remove all contributors with the same name as the given speaker
* @param speaker
*/
public void removeContributor(NativeSpeaker speaker) {
if(speaker != null) {
JSONArray translatorsJson = manifest.getJSONArray(FIELD_TRANSLATORS);
manifest.put(FIELD_TRANSLATORS, Manifest.removeValue(translatorsJson, speaker.name));
}
}
/**
* Returns the contributor that has the given name
* @param name
* @return null if no contributor was found
*/
public NativeSpeaker getContributor(String name) {
manifest.load();
ArrayList<NativeSpeaker> translators = getContributors();
for (NativeSpeaker speaker:translators) {
if (speaker.name.equals(name)) {
return speaker;
}
}
return null;
}
/**
* Returns an array of native speakers who have worked on this translation.
* This will look into the "translators" field in the manifest and check in the commit history.
* @return
*/
public ArrayList<NativeSpeaker> getContributors() {
manifest.load();
JSONArray translatorsJson = manifest.getJSONArray(FIELD_TRANSLATORS);
ArrayList<NativeSpeaker> translators = new ArrayList<>();
for(int i = 0; i < translatorsJson.length(); i ++) {
try {
String name = translatorsJson.getString(i);
if(!name.isEmpty()) {
translators.add(new NativeSpeaker(name));
}
} catch (JSONException e) {
e.printStackTrace();
}
}
return translators;
}
/**
* This will add the default translator if no other translator has been recorded
* @param speaker
*/
public void setDefaultContributor(NativeSpeaker speaker) {
if(speaker != null) {
if (getContributors().isEmpty()) {
addContributor(speaker);
}
}
}
/**
* Returns the translation of a frame
*
* @param frame
* @return
*/
public FrameTranslation getFrameTranslation(Frame frame) {
if(frame == null) {
return null;
}
return getFrameTranslation(frame.getChapterId(), frame.getId(), frame.getFormat());
}
/**
* Returns the translation of a frame
* @param chapterId
* @param frameId
* @param format
* @return
*/
public FrameTranslation getFrameTranslation(String chapterId, String frameId, TranslationFormat format) {
File frameFile = getFrameFile(chapterId, frameId);
if(frameFile.exists()) {
try {
String body = FileUtils.readFileToString(frameFile);
return new FrameTranslation(frameId, chapterId, body, format, isFrameFinished(chapterId + "-" + frameId));
} catch (IOException e) {
e.printStackTrace();
}
}
// give empty translation
return new FrameTranslation(frameId, chapterId, "", format, false);
}
/**
* Returns the translation of a chapter
* This includes the chapter title and reference
*
* @param chapter
* @return
*/
public ChapterTranslation getChapterTranslation(Chapter chapter) {
if(chapter == null) {
return null;
}
return getChapterTranslation(chapter.getId());
}
/**
* Returns the translation of a chapter
* @param chapterSlug
* @return
*/
public ChapterTranslation getChapterTranslation(String chapterSlug) {
File referenceFile = getChapterReferenceFile(chapterSlug);
File titleFile = getChapterTitleFile(chapterSlug);
String reference = "";
String title = "";
if(referenceFile.exists()) {
try {
reference = FileUtils.readFileToString(referenceFile);
} catch (IOException e) {
e.printStackTrace();
}
}
if(titleFile.exists()) {
try {
title = FileUtils.readFileToString(titleFile);
} catch (IOException e) {
e.printStackTrace();
}
}
return new ChapterTranslation(title, reference, chapterSlug, isChapterTitleFinished(chapterSlug), isChapterReferenceFinished(chapterSlug), getFormat());
}
/**
* Returns the translation of a project
* This is just for the project title
*
* @return
*/
public ProjectTranslation getProjectTranslation() {
File titleFile = getProjectTitleFile();
String title = "";
if(titleFile.exists()) {
try {
title = FileUtils.readFileToString(titleFile);
} catch (IOException e) {
e.printStackTrace();
}
}
return new ProjectTranslation(title, isProjectComponentFinished("title"));
}
/**
* Stages a frame translation to be saved
* @param frameTranslation
* @param translatedText
*/
public void applyFrameTranslation(final FrameTranslation frameTranslation, final String translatedText) {
// testing this performance. it will make a lot of things easier if we don't have to use a timeout for performance.
try {
saveFrameTranslation(frameTranslation, translatedText);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Saves the project title translation
* @param translatedText
*/
public void applyProjectTitleTranslation(String translatedText) throws IOException {
File titleFile = getProjectTitleFile();
if(translatedText.isEmpty()) {
titleFile.delete();
} else {
titleFile.getParentFile().mkdirs();
FileUtils.write(titleFile, translatedText);
}
}
/**
* Saves a frame translation to the disk
* if the translated text is null the frame will be removed
* @param frameTranslation
* @param translatedText
*/
private void saveFrameTranslation(FrameTranslation frameTranslation, String translatedText) throws IOException {
File frameFile = getFrameFile(frameTranslation.getChapterId(), frameTranslation.getId());
if(translatedText.isEmpty()) {
frameFile.delete();
} else {
frameFile.getParentFile().mkdirs();
FileUtils.write(frameFile, translatedText);
}
}
/**
* Saves a chapter reference translation to the disk
* if the translated text is null the reference will be removed
* @param chapterTranslation
* @param translatedText
* @throws IOException
*/
private void saveChapterReferenceTranslation(ChapterTranslation chapterTranslation, String translatedText) throws IOException {
File chapterReferenceFile = getChapterReferenceFile(chapterTranslation.getId());
if(translatedText.isEmpty()) {
chapterReferenceFile.delete();
} else {
chapterReferenceFile.getParentFile().mkdirs();
FileUtils.write(chapterReferenceFile, translatedText);
}
}
/**
* Saves a chapter title translation to the disk
* if the translated text is null the title will be removed
* @param chapterTranslation
* @param translatedText
* @throws IOException
*/
private void saveChapterTitleTranslation(ChapterTranslation chapterTranslation, String translatedText) throws IOException {
File chapterTitleFile = getChapterTitleFile(chapterTranslation.getId());
if(translatedText.isEmpty()) {
chapterTitleFile.delete();
} else {
chapterTitleFile.getParentFile().mkdirs();
FileUtils.write(chapterTitleFile, translatedText);
}
}
/**
* Returns the frame file
* @param chapterId
* @param frameId
* @return
*/
public File getFrameFile(String chapterId, String frameId) {
return new File(targetTranslationDir, chapterId + "/" + frameId + ".txt");
}
/**
* Returns the chapter reference file
* @param chapterId
* @return
*/
public File getChapterReferenceFile(String chapterId) {
return new File(targetTranslationDir, chapterId + "/reference.txt");
}
/**
* Returns the chapter title file
* @param chapterId
* @return
*/
public File getChapterTitleFile(String chapterId) {
return new File(targetTranslationDir, chapterId + "/title.txt");
}
/**
* Returns the project title file
* @return
*/
public File getProjectTitleFile() {
return new File(targetTranslationDir, "00/title.txt");
}
/**
* Marks the project title as finished
* @return
*/
public boolean closeProjectTitle() {
File file = getProjectTitleFile();
if(file.exists()) {
return finishProjectComponent("title");
}
return false;
}
/**
* Marks the project title as not finished
* @return
*/
public boolean openProjectTitle() {
return openProjectComponent("title");
}
/**
* Checks if the translation of a component of a project has been marked as done
* @param component
* @return
*/
private boolean isProjectComponentFinished(String component) {
return isChunkClosed("00-" + component);
}
/**
* Reopens a project component
* @param component
* @return
*/
private boolean openProjectComponent(String component) {
return openChunk("00-" + component);
}
/**
* Marks a component of the project as finished
* @param component
* @return
*/
private boolean finishProjectComponent(String component) {
return closeChunk("00-" + component);
}
/**
* Marks the translation of a chapter title as complete
* @param chapter
* @return returns true if the translation actually exists and the update was successful
*/
public boolean finishChapterTitle(Chapter chapter) {
File file = getChapterTitleFile(chapter.getId());
if(file.exists()) {
return closeChunk(chapter.getId() + "-title");
}
return false;
}
/**
* Marks the translation of a chapter title as not complete
* @param chapter
* @return
*/
public boolean reopenChapterTitle(Chapter chapter) {
return openChunk(chapter.getId() + "-title");
}
/**
* Checks if the translation of a chapter title has been marked as done
* @param chapterSlug
* @return
*/
private boolean isChapterTitleFinished(String chapterSlug) {
return isChunkClosed(chapterSlug + "-title");
}
/**
* Marks the translation of a chapter title as complete
* @param chapter
* @return returns true if the translation actually exists and the update was successful
*/
public boolean finishChapterReference(Chapter chapter) {
File file = getChapterReferenceFile(chapter.getId());
if(file.exists()) {
return closeChunk(chapter.getId() + "-reference");
}
return false;
}
/**
* Marks the translation of a chapter title as not complete
* @param chapter
* @return
*/
public boolean reopenChapterReference(Chapter chapter) {
return openChunk(chapter.getId() + "-reference");
}
/**
* Checks if the translation of a chapter reference has been marked as done
* @param chapterSlug
* @return
*/
private boolean isChapterReferenceFinished(String chapterSlug) {
return isChunkClosed(chapterSlug + "-reference");
}
/**
* Marks the translation of a frame as complete
* @param frame
* @return returns true if the translation actually exists and the update was successful
*/
public boolean finishFrame(Frame frame) {
File file = getFrameFile(frame.getChapterId(), frame.getId());
if(file.exists()) {
return closeChunk(frame.getComplexId());
}
return false;
}
/**
* Marks the translation of a frame as not complete
* @param frame
* @return
*/
public boolean reopenFrame(Frame frame) {
return openChunk(frame.getComplexId());
}
/**
* Checks if the translation of a frame has been marked as done
* @param frameComplexId
* @return
*/
private boolean isFrameFinished(String frameComplexId) {
return isChunkClosed(frameComplexId);
}
/**
* Closes a chunk from editing. e.g. marks as finished
* @param complexId the chapter + chunk id e.g. `01-05`, or `01-title`
* @return
*/
private boolean closeChunk(String complexId) {
if(!isChunkClosed(complexId)) {
JSONArray finishedChunks = manifest.getJSONArray(FIELD_FINISHED_CHUNKS);
finishedChunks.put(complexId);
manifest.put(FIELD_FINISHED_CHUNKS, finishedChunks);
}
return true;
}
/**
* Opens a chunk for editing. e.g. marks as not finished
* @param complexId the chapter + chunk id e.g. `01-05`, or `01-title`
* @return
*/
private boolean openChunk(String complexId) {
JSONArray finishedChunks = manifest.getJSONArray(FIELD_FINISHED_CHUNKS);
JSONArray updatedChunks = new JSONArray();
try {
for (int i = 0; i < finishedChunks.length(); i++) {
String currId = finishedChunks.getString(i);
if(!currId.equals(complexId)) {
updatedChunks.put(finishedChunks.getString(i));
}
}
manifest.put(FIELD_FINISHED_CHUNKS, updatedChunks);
return true;
} catch (JSONException e) {
e.printStackTrace();
}
return false;
}
/**
* Checks if a chunk has been closed. e.g. has been marked as finished
* @param complexId the chapter + chunk id e.g. `01-05`, or `01-title`
* @return
*/
private boolean isChunkClosed(String complexId) {
JSONArray finishedChunks = manifest.getJSONArray(FIELD_FINISHED_CHUNKS);
try {
for (int i = 0; i < finishedChunks.length(); i++) {
if(finishedChunks.getString(i).equals(complexId)) {
return true;
}
}
} catch (JSONException e) {
e.printStackTrace();
}
return false;
}
public boolean commitSync() throws Exception {
return commitSync(".");
}
/**
* Checks if there are any non-committed changes in the repo
* @return
* @throws Exception
*/
public boolean isClean() {
try {
Git git = getRepo().getGit();
return git.status().call().isClean();
} catch(Exception e) {
e.printStackTrace();
}
return false;
}
/**
* Sets the author to be used when making commits
* @param name
* @param email
*/
public void setAuthor(String name, String email) {
this.author = new PersonIdent(name, email);
}
public boolean commitSync(String filePattern) throws Exception {
Git git = getRepo().getGit();
// check if dirty
if(isClean()) {
return true;
}
// stage changes
AddCommand add = git.add();
add.addFilepattern(filePattern).call();
// commit changes
final CommitCommand commit = git.commit();
commit.setAll(true);
if(author != null) {
commit.setAuthor(author);
}
commit.setMessage("auto save");
try {
commit.call();
} catch (Exception e) {
Logger.e(TargetTranslation.class.getName(), "Failed to commit changes", e);
return false;
}
return true;
}
/**
* Stages and commits changes to the repository
* @throws Exception
*/
public void commit() throws Exception {
commit(".", null);
}
/**
* Stages and commits changes to the repository
* @param listener
* @throws Exception
*/
public void commit(OnCommitListener listener) throws Exception {
commit(".", listener);
}
/**
* Stages and commits changes to the repository
* @param filePattern the file pattern that will be used to match files for staging
* @param listener the listener that will be called when finished
*/
private void commit(final String filePattern, final OnCommitListener listener) throws Exception {
Thread thread = new Thread() {
@Override
public void run() {
try {
boolean result = commitSync(filePattern);
if(listener != null) {
listener.onCommit(result);
}
} catch (Exception e) {
if(listener != null) {
listener.onCommit(false);
}
}
}
};
thread.start();
}
/**
* Marks the current HEAD of the translation repo as published
* @return true if successful
*/
public void setPublished(final OnPublishedListener listener) {
Thread thread = new Thread() {
@Override
public void run() {
try {
commitSync();
Git git = getRepo().getGit();
final TagCommand tag = git.tag();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd/HH.mm.ss", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
String name = "R2P/" + format.format(new Date());
tag.setName(name);
if(author != null) {
tag.setTagger(author);
}
// tag if not already
if(getPublishedStatus() != PublishStatus.IS_CURRENT) {
tag.call();
}
if (listener != null) {
listener.onSuccess();
}
} catch (Exception e) {
if(listener != null) {
listener.onFailed(e);
}
}
}
};
thread.start();
}
/**
* Returns the most recent published tag
* @return
*/
public RevCommit getLastPublishedTag() throws Exception {
Git git = getRepo().getGit();
Repository repository = git.getRepository();
ListTagCommand tags = git.tagList();
List<Ref> refs = tags.call();
for (int i=refs.size()-1; i >= 0; i--) {
Ref ref = refs.get(i);
// fetch all commits for this tag
LogCommand log = git.log();
log.setMaxCount(1);
Ref peeledRef = repository.peel(ref);
if(peeledRef.getPeeledObjectId() != null) {
log.add(peeledRef.getPeeledObjectId());
} else {
log.add(ref.getObjectId());
}
Iterable<RevCommit> logs = log.call();
for (RevCommit rev : logs) {
return rev;
}
}
return null;
}
/**
* Returns the status of the this target translation's published state
* @return
*/
public PublishStatus getPublishedStatus() {
try {
RevCommit lastTag = getLastPublishedTag();
if(null == lastTag) {
return PublishStatus.NOT_PUBLISHED;
}
RevCommit head = getGitHead(getRepo());
if(null == head) {
return PublishStatus.ERROR;
}
if(head.getCommitTime() > lastTag.getCommitTime()) {
return PublishStatus.NOT_CURRENT;
}
return PublishStatus.IS_CURRENT;
} catch (Exception e) {
Logger.w(this.getClass().toString(), "Error checking published status", e);
}
return PublishStatus.ERROR;
}
/**
* Sets the draft that is a parent of this target translation
* @param draftTranslation
*/
public void setParentDraft(SourceTranslation draftTranslation) {
JSONObject draftStatus = new JSONObject();
try {
draftStatus.put("resource_id", draftTranslation.resourceSlug);
draftStatus.put("checking_level", draftTranslation.getCheckingLevel());
draftStatus.put("version", draftTranslation.getVersion());
// TODO: 3/2/2016 need to update resource object to collect all info from api so we can include more detail here
manifest.put(FIELD_PARENT_DRAFT, draftStatus);
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* Returns the draft translation that is a parent of this target translation.
*/
public SourceTranslation getParentDraft () {
try {
JSONObject parentDraftStatus = manifest.getJSONObject(FIELD_PARENT_DRAFT);
if(Manifest.valueExists(parentDraftStatus, "resource_id")) {
// TODO: it would be handy to include the version of the actual parent draft so we can see if the one pulled has updates
return SourceTranslation.simple(getProjectId(), getTargetLanguageId(), parentDraftStatus.getString("resource_id"));
}
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
/**
* Merges a local repository into this one
* @param newDir
* @return boolean false if there were merge conflicts
* @throws Exception
*/
public boolean merge(File newDir) throws Exception {
// commit everything
TargetTranslation importedTargetTranslation = TargetTranslation.open(newDir);
if(importedTargetTranslation != null) {
importedTargetTranslation.commitSync();
}
commitSync();
Manifest importedManifest = Manifest.generate(newDir);
Repo repo = getRepo();
// attach remote
repo.deleteRemote("new");
repo.setRemote("new", newDir.getAbsolutePath());
FetchCommand fetch = repo.getGit().fetch();
fetch.setRemote("new");
FetchResult fetchResult = fetch.call();
// create branch for new changes
DeleteBranchCommand deleteBranch = repo.getGit().branchDelete();
deleteBranch.setBranchNames("new");
deleteBranch.setForce(true);
deleteBranch.call();
CreateBranchCommand branch = repo.getGit().branchCreate();
branch.setName("new");
branch.setStartPoint("new/master");
branch.call();
// perform merge
MergeCommand merge = repo.getGit().merge();
merge.setFastForward(MergeCommand.FastForwardMode.NO_FF);
merge.include(repo.getGit().getRepository().getRef("new"));
MergeResult result = merge.call();
// merge manifests
mergeManifests(manifest, importedManifest);
if (result.getMergeStatus().equals(MergeResult.MergeStatus.CONFLICTING)) {
System.out.println(result.getConflicts().toString());
return false;
}
return true;
}
/**
* Merges two manifest files together
* @param original
* @param imported
* @return
*/
public static Manifest mergeManifests(Manifest original, Manifest imported) {
// merge manifests
// TODO: 5/25/16 merge notes
original.join(imported.getJSONArray(FIELD_TRANSLATORS), FIELD_TRANSLATORS);
original.join(imported.getJSONArray(FIELD_FINISHED_CHUNKS), FIELD_FINISHED_CHUNKS);
original.join(imported.getJSONArray(FIELD_SOURCE_TRANSLATIONS), FIELD_SOURCE_TRANSLATIONS);
// add missing parent draft status
if((!original.has(FIELD_PARENT_DRAFT) || !Manifest.valueExists(original.getJSONObject(FIELD_PARENT_DRAFT), "resource_id"))
&& imported.has(FIELD_PARENT_DRAFT)) {
original.put(FIELD_PARENT_DRAFT, imported.getJSONObject(FIELD_PARENT_DRAFT));
}
return original;
}
public enum PublishStatus {
IS_CURRENT,
NOT_CURRENT,
NOT_PUBLISHED,
ERROR
}
/**
* Returns the repository for this target translation
* @return
*/
public Repo getRepo() {
return new Repo(targetTranslationDir.getAbsolutePath());
}
/**
* Returns the commit hash of the repo HEAD
* @return
* @throws Exception
*/
public String getCommitHash() throws Exception {
String tag = null;
RevCommit commit = getGitHead(getRepo());
if(commit != null) {
String[] pieces = commit.toString().split(" ");
tag = pieces[1];
} else {
tag = null;
}
return tag;
}
/**
* Returns the commit HEAD
* @param repo the repository who's HEAD is returned
* @return
* @throws GitAPIException, IOException
*/
@Nullable
private RevCommit getGitHead(Repo repo) throws GitAPIException, IOException {
Iterable<RevCommit> commits = repo.getGit().log().setMaxCount(1).call();
RevCommit commit = null;
for(RevCommit c : commits) {
commit = c;
}
return commit;
}
/**
* Stages a chapter reference to be saved
* @param chapterTranslation
* @param translatedText
*/
public void applyChapterReferenceTranslation(ChapterTranslation chapterTranslation, String translatedText) {
try {
saveChapterReferenceTranslation(chapterTranslation, translatedText);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Stages a chapter title to be saved
* @param chapterTranslation
* @param translatedText
*/
public void applyChapterTitleTranslation(ChapterTranslation chapterTranslation, String translatedText) {
try {
saveChapterTitleTranslation(chapterTranslation, translatedText);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Returns the number of items that have been translated
* @return
*/
public int numTranslated() {
int numFiles = 0;
File[] chapterDirs = targetTranslationDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory() && !pathname.getName().equals(".git") && !pathname.getName().equals("manifest.json");
}
});
if(chapterDirs != null) {
for (File dir : chapterDirs) {
String[] files = dir.list();
if (files != null) {
numFiles += files.length;
}
}
}
return numFiles;
}
/**
* Returns the number of items that have been marked as finished
* @return
*/
public int numFinished() {
if(manifest.has(FIELD_FINISHED_CHUNKS)) {
JSONArray finishedFrames = manifest.getJSONArray(FIELD_FINISHED_CHUNKS);
return finishedFrames.length();
} else {
return 0;
}
}
/**
* Returns an array of chapter translations
* @return
*/
public ChapterTranslation[] getChapterTranslations() {
String[] chapterSlugs = targetTranslationDir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return new File(dir, filename).isDirectory() && !filename.equals(".git");
}
});
Arrays.sort(chapterSlugs, new NumericStringComparator());
List<ChapterTranslation> chapterTranslations = new ArrayList<>();
if(chapterSlugs != null) {
for (String slug : chapterSlugs) {
ChapterTranslation c = getChapterTranslation(slug);
if (c != null) {
chapterTranslations.add(c);
}
}
}
return chapterTranslations.toArray(new ChapterTranslation[chapterTranslations.size()]);
}
// TODO: 2/15/2016 Once the new api (v3) is built we can base all the translatable items off a ChunkTranslation object so we just need one method in place of the 4 below
public FileHistory getFrameHistory(FrameTranslation frameTranslation) {
try {
return new FileHistory(getRepo(), getFrameFile(frameTranslation.getChapterId(), frameTranslation.getId()));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public FileHistory getChapterTitleHistory(ChapterTranslation chapterTranslation) {
try {
return new FileHistory(getRepo(), getChapterTitleFile(chapterTranslation.getId()));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public FileHistory getChapterReferenceHistory(ChapterTranslation chapterTranslation) {
try {
return new FileHistory(getRepo(), getChapterReferenceFile(chapterTranslation.getId()));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public FileHistory getProjectTitleHistory() {
try {
return new FileHistory(getRepo(), getProjectTitleFile());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* Returns an array of frame translations for the chapter
* @param chapterSlug
* @return
*/
public FrameTranslation[] getFrameTranslations(String chapterSlug, TranslationFormat frameTranslationformat) {
String[] frameFileNames = new File(targetTranslationDir, chapterSlug).list(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return !filename.equals("reference.txt") && !filename.equals("title.txt");
}
});
Arrays.sort(frameFileNames, new NumericStringComparator());
List<FrameTranslation> frameTranslations = new ArrayList<>();
if(frameFileNames != null) {
for (String fileName : frameFileNames) {
String[] slug = fileName.split("\\.txt");
if(slug.length == 1) {
FrameTranslation f = getFrameTranslation(chapterSlug, slug[0], frameTranslationformat);
if (f != null) {
frameTranslations.add(f);
}
}
}
}
return frameTranslations.toArray(new FrameTranslation[frameTranslations.size()]);
}
public interface OnCommitListener {
void onCommit(boolean success);
}
public interface OnPublishedListener {
void onSuccess();
void onFailed(Exception e);
}
}