/*
* Copyright 2013, Red Hat, Inc. and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.zanata.model.tm;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.xml.XMLConstants;
import lombok.extern.slf4j.Slf4j;
import nu.xom.Attribute;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import org.codehaus.jackson.map.ObjectMapper;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.zanata.util.TMXConstants;
import org.zanata.util.TMXParseException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.ibm.icu.util.ULocale;
/**
* Adapts TMX metadata to the generic translation memory objects.
*
* @author Sean Flanigan <a
* href="mailto:sflaniga@redhat.com">sflaniga@redhat.com</a>
*
*/
@Slf4j
public class TMXMetadataHelper {
private static final String EMPTY_NAMESPACE = XMLConstants.NULL_NS_URI;
private static final String TMX_ELEMENT_CHILDREN =
"__TMX_ELEMENT_CHILDREN__";
private static final DateTimeFormatter ISO8601Z = DateTimeFormat
.forPattern("yyyyMMdd'T'HHmmss'Z").withZoneUTC();
private static final ObjectMapper jsonMapper = new ObjectMapper();
// TMX attributes which we store as fields (*not* in the generic metadata
// map):
private static final String CREATION_DATE = "creationdate";
private static final String CHANGE_DATE = "changedate";
private static final String SRC_LANG = TMXConstants.SRCLANG;
private static final String XML_LANG = "xml:lang";
private static final String TUID = "tuid";
private static List<String> getChildrenXml(Map<String, Object> metadata) {
List<String> children =
(List<String>) metadata.get(TMX_ELEMENT_CHILDREN);
if (children == null) {
return Collections.emptyList();
}
return children;
}
public static ImmutableList<Element> getChildren(HasTMMetadata fromEntity) {
try {
String metadataString =
fromEntity.getMetadata(TMMetadataType.TMX14);
if (metadataString == null) {
return ImmutableList.of();
}
Map<String, Object> metadata =
jsonMapper.readValue(metadataString, Map.class);
List<String> children = getChildrenXml(metadata);
Builder<Element> result = ImmutableList.builder();
for (String childXml : children) {
Document doc = new nu.xom.Builder().build(childXml, null);
Element elem = (Element) doc.getRootElement().copy();
result.add(elem);
}
return result.build();
} catch (Exception e) {
// error parsing XML or json, which "shouldn't happen"
throw new RuntimeException(e);
}
}
/**
* Gets all the entity's metadata in a single Map.
*
* @param tu
* @return
*/
public static @Nonnull
ImmutableMap<String, String> getAttributes(TransMemory fromTm) {
ImmutableMap.Builder<String, String> m = ImmutableMap.builder();
m.putAll(getSharedMetadata(fromTm));
String srclang = fromTm.getSourceLanguage();
if (srclang != null) {
m.put(SRC_LANG, srclang);
}
return m.build();
}
/**
* Gets all the entity's metadata in a single Map.
*
* @param fromTu
* @return
*/
public static @Nonnull
ImmutableMap<String, String> getAttributes(TransMemoryUnit fromTu) {
ImmutableMap.Builder<String, String> m = ImmutableMap.builder();
m.putAll(getSharedMetadata(fromTu));
String tuid = fromTu.getTransUnitId();
if (tuid != null) {
m.put(TUID, tuid);
}
String srclang = fromTu.getSourceLanguage();
if (srclang != null) {
m.put(SRC_LANG, srclang);
}
return m.build();
}
/**
* Gets all the entity's metadata in a single Map.
*
* @param tu
* @return
*/
public static @Nonnull
ImmutableMap<String, String> getAttributes(TransMemoryUnitVariant fromTuv) {
ImmutableMap.Builder<String, String> m = ImmutableMap.builder();
m.putAll(getSharedMetadata(fromTuv));
String lang = fromTuv.getLanguage();
assert lang != null;
m.put(XML_LANG, lang);
return m.build();
}
private static @Nonnull
ImmutableMap<String, String> getSharedMetadata(HasTMMetadata fromEntity) {
ImmutableMap.Builder<String, String> m = ImmutableMap.builder();
m.putAll(getGenericMetadata(fromEntity));
Date creationDate = fromEntity.getCreationDate();
if (creationDate != null) {
m.put(CREATION_DATE, toString(creationDate));
}
Date lastChanged = fromEntity.getLastChanged();
if (lastChanged != null) {
m.put(CHANGE_DATE, toString(lastChanged));
}
return m.build();
}
@SuppressWarnings("null")
private static @Nonnull
Map<String, String> getGenericMetadata(HasTMMetadata fromEntity) {
String metadataString = fromEntity.getMetadata(TMMetadataType.TMX14);
if (metadataString == null) {
return ImmutableMap.of();
}
try {
Map<String, String> map =
jsonMapper.readValue(metadataString, Map.class);
map.remove(TMX_ELEMENT_CHILDREN);
return map;
} catch (Exception e) {
// error parsing json, which "shouldn't happen"
throw new RuntimeException(e);
}
}
/**
* Sets all the Translation Memory's metadata (attributes and children)
*
* @param toTransMemory
* @param fromHeaderElem
* @throws TMXParseException
*/
public static void setMetadata(TransMemory toTransMemory,
@Nonnull Element fromHeaderElem) throws TMXParseException {
Map<String, Object> metadata = buildMetadata(fromHeaderElem);
String srclang = (String) metadata.remove(SRC_LANG);
if (srclang != null) {
toTransMemory.setSourceLanguage(getValidLang(srclang));
} else {
throw new TMXParseException("missing srclang in header");
}
setSharedMetadata(toTransMemory, metadata);
}
/**
* Sets all the TU's metadata (attributes and children)
*
* @param toTransUnit
* @param fromTuElem
* @param tmSrcLang
* srclang to use if the TU does not specify srclang
*/
public static void setMetadata(TransMemoryUnit toTransUnit,
@Nonnull Element fromTuElem, String tmSrcLang) {
Map<String, Object> metadata = buildMetadata(fromTuElem);
String tuid = (String) metadata.remove(TUID);
if (tuid != null) {
toTransUnit.setTransUnitId(tuid);
}
String srclang = (String) metadata.remove(SRC_LANG);
if (srclang != null) {
if (srclang.equalsIgnoreCase(TMXConstants.ALL_LOCALE)) {
toTransUnit.setSourceLanguage(null);
} else {
toTransUnit.setSourceLanguage(getValidLang(srclang));
}
} else {
toTransUnit.setSourceLanguage(tmSrcLang);
}
setSharedMetadata(toTransUnit, metadata);
}
/**
* Sets all the TUV's metadata (attributes and children)
*
* @throws TMXParseException
*/
public static void setMetadata(TransMemoryUnitVariant toTuv,
Element fromTuvElem) throws TMXParseException {
Map<String, Object> metadata = buildMetadata(fromTuvElem);
String lang = (String) metadata.remove(XML_LANG);
if (lang != null) {
toTuv.setLanguage(getValidLang(lang));
} else {
throw new TMXParseException("missing xml:lang in tuv: "
+ fromTuvElem.toXML());
}
setSharedMetadata(toTuv, metadata);
}
/**
* Throws IllegalArgumentException if lang is not a valid code (loose BCP-47
* check)
*
* @param lang
* @return lang
*/
private static String getValidLang(final String lang) {
return ULocale.canonicalize(lang).replace('_', '-');
}
private static void setSharedMetadata(HasTMMetadata toEntity,
Map<String, Object> fromMetadata) {
String creationdate = (String) fromMetadata.remove(CREATION_DATE);
if (creationdate != null) {
toEntity.setCreationDate(toDate(creationdate));
}
String changedate = (String) fromMetadata.remove(CHANGE_DATE);
if (changedate != null) {
toEntity.setLastChanged(toDate(changedate));
}
setGenericMetadata(toEntity, fromMetadata);
}
private static void setGenericMetadata(HasTMMetadata toEntity,
Map<String, Object> fromMetadata) {
try {
String json = jsonMapper.writeValueAsString(fromMetadata);
toEntity.setMetadata(TMMetadataType.TMX14, json);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("null")
public static @Nonnull
Date toDate(String dateString) {
return ISO8601Z.parseDateTime(dateString).toDate();
}
@SuppressWarnings("null")
public static @Nonnull
String toString(Date date) {
return ISO8601Z.print(date.getTime());
}
private static Map<String, Object> buildMetadata(Element fromElem) {
Map<String, Object> metadata = Maps.newHashMap();
for (int i = 0; i < fromElem.getAttributeCount(); i++) {
Attribute attr = fromElem.getAttribute(i);
String uri = attr.getNamespaceURI();
String name = attr.getLocalName();
if (inTmxNamespace(uri)) {
String value = attr.getValue();
metadata.put(name, value);
} else if (attr.getQualifiedName().equals(XML_LANG)) {
String value = attr.getValue();
metadata.put(attr.getQualifiedName(), value);
}
}
List<String> childrenXml = getChildrenAsXml(fromElem);
metadata.put(TMX_ELEMENT_CHILDREN, childrenXml);
return metadata;
}
/**
* Build a list of supported child Elements in XML string form
*
* @param fromElem
* @return
*/
private static List<String> getChildrenAsXml(Element fromElem) {
Builder<String> childrenXml = ImmutableList.builder();
Elements childElements = fromElem.getChildElements();
for (int i = 0; i < childElements.size(); i++) {
Element child = childElements.get(i);
addChildIfSupported(child, childrenXml);
}
return childrenXml.build();
}
/**
* Supported children are currently {@code <prop>} and {@code <note>}.
*
* @param child
* @param childrenXml
*/
private static void addChildIfSupported(Element child,
Builder<String> childrenXml) {
String uri = child.getNamespaceURI();
String name = child.getLocalName();
if (inTmxNamespace(uri) && (name.equals("prop") || name.equals("note"))) {
Element copy = (Element) child.copy();
copy.setNamespacePrefix("");
copy.setNamespaceURI("");
childrenXml.add(copy.toXML());
}
}
private static boolean inTmxNamespace(String uri) {
return uri.equals(EMPTY_NAMESPACE)
|| uri.equals(TMXConstants.TMX14_NAMESPACE);
}
}