/**************************************************************************
OmegaT - Computer Assisted Translation (CAT) tool
with fuzzy matching, translation memory, keyword search,
glossaries, and translation leveraging into updated projects.
Copyright (C) 2000-2006 Keith Godfrey and Maxym Mykhalchuk
2007-2011 Didier Briel
2013 Didier Briel, Aaron Madlon-Kay, Piotr Kulik, Alex Buloichik
2014 Piotr Kulik, Didier Briel, Aaron Madlon-Kay
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.filters3.xml.xliff;
import java.awt.Window;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.omegat.core.Core;
import org.omegat.core.data.ProtectedPart;
import org.omegat.filters2.FilterContext;
import org.omegat.filters2.Instance;
import org.omegat.filters3.xml.XMLFilter;
import org.omegat.filters3.xml.xliff.XLIFFOptions.ID_TYPE;
import org.omegat.util.Log;
import org.omegat.util.OStrings;
import org.omegat.util.StringUtil;
import org.xml.sax.Attributes;
/**
* Filter for XLIFF files.
*
* @author Didier Briel
* @author Aaron Madlon-Kay
* @author Piotr Kulik
* @author Alex Buloichik
*/
public class XLIFFFilter extends XMLFilter {
private String resname;
private boolean ignored;
private ArrayList<String> groupResname = new ArrayList<String>();
private int groupLevel;
private ArrayList<String> props = new ArrayList<String>();
private StringBuilder text = new StringBuilder();
private ArrayList<String> entryText = new ArrayList<String>();
private ArrayList<List<ProtectedPart>> protectedParts = new ArrayList<List<ProtectedPart>>();
private HashSet<String> altIDCache = new HashSet<String>();
private String id;
/**
* Sets whether alternative translations are identified by previous and next paragraphs or by <trans-unit> ID
*/
private ID_TYPE altTransIDType = ID_TYPE.CONTEXT;
/**
* Register plugin into OmegaT.
*/
public static void loadPlugins() {
Core.registerFilterClass(XLIFFFilter.class);
}
public static void unloadPlugins() {
}
/**
* Creates a new instance of XLIFFFilter
*/
public XLIFFFilter() {
super(new XLIFFDialect());
}
/**
* Human-readable name of the File Format this filter supports.
*
* @return File format name
*/
@Override
public String getFileFormatName() {
return OStrings.getString("XLIFF_FILTER_NAME");
}
/**
* The default list of filter instances that this filter class has. One
* filter class may have different filter instances, different by source
* file mask, encoding of the source file etc.
* <p>
* Note that the user may change the instances freely.
*
* @return Default filter instances
*/
@Override
public Instance[] getDefaultInstances() {
return new Instance[] { new Instance("*.xlf", null, null),
new Instance("*.xliff", null, null),
new Instance("*.sdlxliff", null, null), };
}
/**
* Either the encoding can be read, or it is UTF-8.
*
* @return <code>false</code>
*/
@Override
public boolean isSourceEncodingVariable() {
return false;
}
/**
* Yes, XLIFF may be written out in a variety of encodings.
*
* @return <code>true</code>
*/
@Override
public boolean isTargetEncodingVariable() {
return true;
}
@Override
protected boolean requirePrevNextFields() {
return altTransIDType == ID_TYPE.CONTEXT;
}
/**
* Returns true to indicate that the XLIFF filter has options.
*
* @return True, because the XLIFF filter has options.
*/
@Override
public boolean hasOptions() {
return true;
}
/**
* XLIFF Filter shows a <b>modal</b> dialog to edit its own options.
*
* @param currentOptions
* Current options to edit.
* @return Updated filter options if user confirmed the changes, and current options otherwise.
*/
@Override
public Map<String, String> changeOptions(Window parent, Map<String, String> currentOptions) {
try {
EditXLIFFOptionsDialog dialog = new EditXLIFFOptionsDialog(parent, currentOptions);
dialog.setVisible(true);
if (EditXLIFFOptionsDialog.RET_OK == dialog.getReturnStatus()) {
return dialog.getOptions().getOptionsMap();
} else {
return null;
}
} catch (Exception e) {
Log.logErrorRB("HTML_EXC_EDIT_OPTIONS");
Log.log(e);
return null;
}
}
/**
* We're not actually checking whether it is a valid XLIFF file; we just need a place to call defineDialect.
*/
@Override
public boolean isFileSupported(File inFile, Map<String, String> config, FilterContext context) {
boolean result = super.isFileSupported(inFile, config, context);
if (result) {
// Defining the actual dialect, because at this step
// we have the options
XLIFFDialect dialect = (XLIFFDialect) this.getDialect();
dialect.defineDialect(new XLIFFOptions(config));
try {
super.processFile(inFile, null, context);
} catch (Exception e) {
Log.log(e);
}
this.altTransIDType = dialect.altTransIDType;
}
return result;
}
/**
* Support of group and trans-unit resname attribute and trans-unit <note> as comment, based on ResXFilter code
*/
@Override
public void tagStart(String path, Attributes atts) {
if (atts != null && path.endsWith("trans-unit")) {
// resname may or may not be present.
resname = atts.getValue("resname");
id = atts.getValue("id");
}
// not all <group> tags have resname attribute
if (path.endsWith("/group")) {
// <group> only, it can be nested
groupLevel++;
groupResname.add(atts.getValue("resname"));
}
if ("/xliff/file/header".equals(path)) {
ignored = true;
}
text.setLength(0);
}
@Override
public void tagEnd(String path) {
if (path.endsWith("trans-unit/note")) {
// <trans-unit> <note>'s only
addProperty("note", text.toString());
} else if (path.endsWith("trans-unit")) {
if (entryParseCallback != null) {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < groupLevel; i++) {
String temp = groupResname.get(i);
if (temp != null) {
buf.append(temp);
// group1/group2/..
buf.append(i == (groupLevel - 1) ? "" : "/");
}
}
if (buf.length() > 0) {
addProperty("group", buf.toString());
}
if (resname != null) {
addProperty("resname", resname);
}
for (int i = 0; i < entryText.size(); i++) {
entryParseCallback.addEntryWithProperties(getSegID(), entryText.get(i), null, false,
finalizeProperties(), null, this, protectedParts.get(i));
}
}
id = null;
resname = null;
props.clear();
entryText.clear();
protectedParts.clear();
} else if (path.endsWith("/group")) {
groupResname.remove(--groupLevel);
} else if (path.endsWith("/file")) {
altIDCache.clear();
}
if ("/xliff/file/header".equals(path)) {
ignored = false;
}
}
private void addProperty(String key, String value) {
props.add(key);
props.add(value);
}
private String[] finalizeProperties() {
if (props.isEmpty()) {
return null;
}
return props.toArray(new String[props.size()]);
}
private String getSegID() {
String segID = null;
switch (altTransIDType) {
case ELEMENT_ID:
segID = id;
break;
case RESNAME_ATTR:
segID = resname == null ? id : resname;
break;
default:
// Leave key null
}
if (segID != null) {
segID = ensureUniqueID(segID);
}
return segID;
}
String ensureUniqueID(String id) {
int i = 0;
String tryID;
while (true) {
tryID = id + (i == 0 ? "" : "_" + i);
if (!altIDCache.contains(tryID)) {
altIDCache.add(tryID);
return tryID;
}
i++;
}
}
@Override
public boolean isInIgnored() {
return ignored;
}
@Override
public void text(String text) {
this.text.append(text);
}
@Override
public String translate(String entry, List<ProtectedPart> protectedParts) {
if (entryParseCallback != null) {
if (!StringUtil.isEmpty(entry)) {
entryText.add(entry);
this.protectedParts.add(protectedParts);
}
return entry;
} else if (entryTranslateCallback != null) {
String translation = StringUtil.isEmpty(entry) ? entry
: entryTranslateCallback.getTranslation(getSegID(), entry, null);
return translation != null ? translation : entry;
} else {
return entry;
}
}
}