/************************************************************************** OmegaT - Computer Assisted Translation (CAT) tool with fuzzy matching, translation memory, keyword search, glossaries, and translation leveraging into updated projects. Copyright (C) 2012 Alex Buloichik 2013-2014 Aaron Madlon-Kay, Alex Buloichik Home page: http://www.omegat.org/ Support center: http://groups.yahoo.com/group/OmegaT/ This file is part of OmegaT. OmegaT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. OmegaT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. **************************************************************************/ package org.omegat.core.data; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; import java.util.stream.Stream; import org.omegat.core.Core; import org.omegat.util.FileUtil; import org.omegat.util.Language; import org.omegat.util.Log; import org.omegat.util.OConsts; import org.omegat.util.Preferences; import org.omegat.util.StringUtil; import org.omegat.util.TMXReader2; import org.omegat.util.TMXWriter2; /** * Class for store data from project_save.tmx. * * Orphaned or non-orphaned translation calculated by RealProject. * * @author Alex Buloichik (alex73mail@gmail.com) * @author Aaron Madlon-Kay */ public class ProjectTMX { protected static final String PROP_FILE = "file"; protected static final String PROP_ID = "id"; protected static final String PROP_PREV = "prev"; protected static final String PROP_NEXT = "next"; protected static final String PROP_PATH = "path"; protected static final String PROP_XICE = "x-ice"; protected static final String PROP_X100PC = "x-100pc"; protected static final String PROP_XAUTO = "x-auto"; /** * Storage for default translations for current project. * * It must be used with synchronization around ProjectTMX. */ Map<String, TMXEntry> defaults; /** * Storage for alternative translations for current project. * * It must be used with synchronization around ProjectTMX. */ Map<EntryKey, TMXEntry> alternatives; final CheckOrphanedCallback checkOrphanedCallback; public ProjectTMX(Language sourceLanguage, Language targetLanguage, boolean isSentenceSegmentingEnabled, File file, CheckOrphanedCallback callback) throws Exception { this.checkOrphanedCallback = callback; alternatives = new HashMap<EntryKey, TMXEntry>(); defaults = new HashMap<String, TMXEntry>(); if (file == null || !file.exists()) { // file not exist - new project return; } new TMXReader2().readTMX( file, sourceLanguage, targetLanguage, isSentenceSegmentingEnabled, false, true, Preferences.isPreference(Preferences.EXT_TMX_USE_SLASH), new Loader(sourceLanguage, targetLanguage, isSentenceSegmentingEnabled)); } /** * Constructor for TMX delta. */ public ProjectTMX() { alternatives = new HashMap<EntryKey, TMXEntry>(); defaults = new HashMap<String, TMXEntry>(); checkOrphanedCallback = null; } /** * Check TMX for empty. */ public boolean isEmpty() { return defaults.isEmpty() && alternatives.isEmpty(); } /** * It saves current translation into file. */ public void save(ProjectProperties props, String translationFile, boolean translationUpdatedByUser) throws Exception { if (!translationUpdatedByUser) { if (new File(translationFile).exists()) { // if there is no file - need to save it Log.logInfoRB("LOG_DATAENGINE_SAVE_NONEED"); return; } } File newFile = new File(translationFile + OConsts.NEWFILE_EXTENSION); // Save data into '*.new' file exportTMX(props, newFile, false, false, true); File backup = new File(translationFile + OConsts.BACKUP_EXTENSION); File orig = new File(translationFile); if (backup.exists()) { if (!backup.delete()) { throw new IOException("Error delete backup file"); } } // Rename existing project file in case a fatal error // is encountered during the write procedure - that way // everything won't be lost if (orig.exists()) { FileUtil.rename(orig, backup); } // Rename new file into TMX file FileUtil.rename(newFile, orig); } public void exportTMX(ProjectProperties props, File outFile, final boolean forceValidTMX, final boolean levelTwo, final boolean useOrphaned) throws Exception { TMXWriter2 wr = new TMXWriter2(outFile, props.getSourceLanguage(), props.getTargetLanguage(), props.isSentenceSegmentingEnabled(), levelTwo, forceValidTMX); try { Map<String, TMXEntry> tempDefaults = new TreeMap<String, TMXEntry>(); Map<EntryKey, TMXEntry> tempAlternatives = new TreeMap<EntryKey, TMXEntry>(); synchronized (this) { if (useOrphaned) { // fast call - just copy tempDefaults.putAll(defaults); tempAlternatives.putAll(alternatives); } else { // slow call - copy non-orphaned only for(Map.Entry<String, TMXEntry> en:defaults.entrySet()) { if (checkOrphanedCallback.existSourceInProject(en.getKey())) { tempDefaults.put(en.getKey(), en.getValue()); } } for(Map.Entry<EntryKey, TMXEntry> en:alternatives.entrySet()) { if (checkOrphanedCallback.existEntryInProject(en.getKey())) { tempAlternatives.put(en.getKey(), en.getValue()); } } } } List<String> p=new ArrayList<String>(); wr.writeComment(" Default translations "); for (Map.Entry<String, TMXEntry> en : new TreeMap<String, TMXEntry>(tempDefaults).entrySet()) { p.clear(); if (Preferences.isPreferenceDefault(Preferences.SAVE_AUTO_STATUS, false)) { if (en.getValue().linked == TMXEntry.ExternalLinked.xAUTO) { p.add(PROP_XAUTO); p.add("auto"); } } wr.writeEntry(en.getKey(), en.getValue().translation, en.getValue(), p); } wr.writeComment(" Alternative translations "); for (Map.Entry<EntryKey, TMXEntry> en : new TreeMap<EntryKey, TMXEntry>(tempAlternatives) .entrySet()) { EntryKey k = en.getKey(); p.clear(); p.add(PROP_FILE); p.add(k.file); p.add(PROP_ID); p.add(k.id); p.add(PROP_PREV); p.add(k.prev); p.add(PROP_NEXT); p.add(k.next); p.add(PROP_PATH); p.add(k.path); if (Preferences.isPreferenceDefault(Preferences.SAVE_AUTO_STATUS, false)) { if (en.getValue().linked == TMXEntry.ExternalLinked.xICE) { p.add(PROP_XICE); p.add(k.id); } else if (en.getValue().linked == TMXEntry.ExternalLinked.x100PC) { p.add(PROP_X100PC); p.add(k.id); } } wr.writeEntry(en.getKey().sourceText, en.getValue().translation, en.getValue(), p); } } finally { wr.close(); } } /** * Get default translation or null if not exist. */ public TMXEntry getDefaultTranslation(String source) { synchronized (this) { return defaults.get(source); } } /** * Get multiple translation or null if not exist. */ public TMXEntry getMultipleTranslation(EntryKey ek) { synchronized (this) { return alternatives.get(ek); } } /** * Set new translation. */ public void setTranslation(SourceTextEntry ste, TMXEntry te, boolean isDefault) { synchronized (this) { if (te == null) { if (isDefault) { defaults.remove(ste.getKey().sourceText); } else { alternatives.remove(ste.getKey()); } } else { if (!ste.getSrcText().equals(te.source)) { throw new IllegalArgumentException("Source must be the same as in SourceTextEntry"); } if (isDefault != te.defaultTranslation) { throw new IllegalArgumentException("Default/alternative must be the same"); } if (isDefault) { defaults.put(ste.getKey().sourceText, te); } else { alternatives.put(ste.getKey(), te); } } } } private class Loader implements TMXReader2.LoadCallback { private final Language sourceLang; private final Language targetLang; private final boolean sentenceSegmentingEnabled; public Loader(Language sourceLang, Language targetLang, boolean sentenceSegmentingEnabled) { this.sourceLang = sourceLang; this.targetLang = targetLang; this.sentenceSegmentingEnabled = sentenceSegmentingEnabled; } public boolean onEntry(TMXReader2.ParsedTu tu, TMXReader2.ParsedTuv tuvSource, TMXReader2.ParsedTuv tuvTarget, boolean isParagraphSegtype) { if (tuvSource == null) { // source Tuv not found return false; } String creator = null; long created = 0; String changer = null; long changed = 0; String translation = null; if (tuvTarget != null) { creator = StringUtil.nvl(tuvTarget.creationid, tu.creationid); created = StringUtil.nvlLong(tuvTarget.creationdate, tu.creationdate); changer = StringUtil.nvl(tuvTarget.changeid, tuvTarget.creationid, tu.changeid, tu.creationid); changed = StringUtil.nvlLong(tuvTarget.changedate, tuvTarget.creationdate, tu.changedate, tu.creationdate); translation = tuvTarget.text; } List<String> sources = new ArrayList<String>(); List<String> targets = new ArrayList<String>(); Core.getSegmenter().segmentEntries(sentenceSegmentingEnabled && isParagraphSegtype, sourceLang, tuvSource.text, targetLang, translation, sources, targets); synchronized (this) { for (int i = 0; i < sources.size(); i++) { String segmentSource = sources.get(i); String segmentTranslation = targets.get(i); PrepareTMXEntry te = new PrepareTMXEntry(); te.source = segmentSource; te.translation = segmentTranslation; te.changer = changer; te.changeDate = changed; te.creator = creator; te.creationDate = created; te.note = tu.note; te.otherProperties = tu.props; EntryKey key = new EntryKey(te.getPropValue(PROP_FILE), te.source, te.getPropValue(PROP_ID), te.getPropValue(PROP_PREV), te.getPropValue(PROP_NEXT), te.getPropValue(PROP_PATH)); TMXEntry.ExternalLinked externalLinkedMode = calcExternalLinkedMode(te); boolean defaultTranslation = key.file == null; if (te.otherProperties != null && te.otherProperties.isEmpty()) { te.otherProperties = null; } if (defaultTranslation) { // default translation defaults.put(segmentSource, new TMXEntry(te, true, externalLinkedMode)); } else { // multiple translation alternatives.put(key, new TMXEntry(te, false, externalLinkedMode)); } } } return true; } }; private TMXEntry.ExternalLinked calcExternalLinkedMode(PrepareTMXEntry te) { String id = te.getPropValue(PROP_ID); TMXEntry.ExternalLinked externalLinked = null; if (externalLinked == null && te.hasPropValue(PROP_XICE, id)) { externalLinked = TMXEntry.ExternalLinked.xICE; } if (externalLinked == null && te.hasPropValue(PROP_X100PC, id)) { externalLinked = TMXEntry.ExternalLinked.x100PC; } if (externalLinked == null && te.hasPropValue(PROP_XAUTO, null)) { externalLinked = TMXEntry.ExternalLinked.xAUTO; } return externalLinked; } /** * Returns the collection of TMX entries that have a default translation */ public Collection<TMXEntry> getDefaults() { return defaults.values(); } /** * Returns the collection of TMX entries that have an alternative translation * @return */ public Collection<TMXEntry> getAlternatives() { return alternatives.values(); } public interface CheckOrphanedCallback { boolean existEntryInProject(EntryKey key); boolean existSourceInProject(String src); } public void replaceContent(ProjectTMX tmx) { synchronized (this) { defaults = tmx.defaults; alternatives = tmx.alternatives; } } @Override public String toString() { return "[" + Stream.concat(defaults.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).map(e -> e.getKey() + ": " + e.getValue().translation), alternatives.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().sourceText)).map(e -> e.getKey().sourceText + ": " + e.getValue().translation)) .collect(Collectors.joining(", ")) + "]"; } }