/** * (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.jabylon.properties.xliff; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.jabylon.properties.PropertiesFactory; import org.jabylon.properties.Property; import org.jabylon.properties.PropertyFile; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * Converts a single XLIFF 1.2 Document (read from an {@link InputStream}) into {@link Property}s.<br> * * @author c.samulski (2016-02-08) */ public final class XliffReader implements XliffXMLConstants { private XliffReader() { } // no instantiation necessary /** * Converts the incoming XLIFF document into a {@link PropertyFile} POJO.<br> */ public static final PropertyWrapper read(InputStream in, String encoding) throws IOException, SAXException { /* * 1. Create Document, validate and parse. */ DocumentBuilder builder = getDocumentBuilder(); Element root = builder.parse(in).getDocumentElement(); Element fileElement = (Element) getChildNodeByTagName(root, TAG_FILE); /* * 2. Validate fileElement. If it's null, we throw a SAXException to produce a friendly UI * error (instead of throwing NPEs up the stack). Frankly we should be XSD-validating here, but * the official XLIFF 1.2 XSD's license terms are not exactly transparent. */ if (fileElement == null) { throw new SAXException("Element <file> null."); } /* * 2. Retrieve targetLocale. */ Locale locale = getTargetLocale(fileElement); /* * 3. Retrieve Properties, return. */ Element bodyElement = (Element) getChildNodeByTagName(fileElement, TAG_BODY); Map<String, Property> properties = readProperties(bodyElement); return new PropertyWrapper(locale, properties); } /** * Parses "TransUnit" {@link Element}s and Populates a {@link Map} of {@link Property}s. <br> * Sadly we have to perform "manual" validation here as the XLIFF 1.2 XSD's license terms are * ambiguous and we cannot simply do a schema validation when calling * {@link DocumentBuilder#parse(InputStream)} .<br> */ private static Map<String, Property> readProperties(Element bodyElement) { NodeList transUnits = bodyElement.getElementsByTagName(TAG_TRANS_UNIT); Map<String, Property> ret = new HashMap<>(); /* TransUnit sequence is minOccurs=0 */ if (transUnits == null) { return ret; } for (int i = 0; i < transUnits.getLength(); i++) { Element transUnit = (Element) transUnits.item(i); /* * 1. Retrieve target element, it's the only one we will import. */ Node targetElement = getChildNodeByTagName(transUnit, TAG_TARGET); if (targetElement == null) { continue; } /* * 2. Check if ID (NLS key) is set. */ String key = transUnit.getAttribute(ATT_ID); if (!hasValue(key)) { continue; } /** * 3. Check if our TransUnit element has text content. */ String value = getTranslationFromTargetElement(targetElement); if (!hasValue(value)) { continue; } /* * 4. Add the parsed property. */ Property property = newProperty(key, value); ret.put(property.getKey(), property); } return ret; } /** * Translation values may be found either under:<br> * trans-unit > target > "value" OR<br> * trans-unit > target > mrk > "value"<br> */ private static String getTranslationFromTargetElement(Node targetElement) { /* Check if target element itself has text content. */ String value = getNodeValue(targetElement); if (hasValue(value)) { return value; } /* Check if a direct "mrl" child exists, retrieve text content from that. */ Node mrkElement = getChildNodeByTagName(targetElement, TAG_MRK); if (mrkElement == null) { return null; } return getNodeValue(mrkElement); } /** * Creates and returns a new {@link Property} based on key and value inputs.<br> */ private static Property newProperty(String key, String value) { Property property = PropertiesFactory.eINSTANCE.createProperty(); property.setKey(key); property.setValue(value); return property; } /** * Retrieve {@link Locale} from this {@link Element}s * {@link XliffXMLConstants#ATT_TARGET_LANGUAGE} attribute.<br> */ private static Locale getTargetLocale(Element fileElement) throws IOException { String targetLanguage = fileElement.getAttribute(ATT_TARGET_LANGUAGE); if (targetLanguage.indexOf("_") != -1) { return parseLocale(targetLanguage, "_"); } if (targetLanguage.indexOf("-") != -1) { return parseLocale(targetLanguage, "-"); } return new Locale(targetLanguage); } /** * Return new {@link Locale} for an arbitrary language and country code string.<br> * TODO: Might want to use org.apache.commons.lang.LocaleUtils for this. Kind of dirty, but * covers most cases.<br> */ private static Locale parseLocale(String targetLanguage, String split) { String[] locale = targetLanguage.split(split); if (locale.length == 3) { // language, country, variant. return new Locale(locale[0], locale[1], locale[2]); } else if (locale.length == 2) { // language, country. return new Locale(locale[0], locale[1]); } else { return new Locale(locale[0]); } } /** * Helper to retrieve *first* child {@link Node} by specified tag name.<br> * Explicitly not calling {@link Element#getElementsByTagName(String)} as this would traverse * children until any {@link Element} is found with the given name.<br> */ private static Node getChildNodeByTagName(Node node, String tagName) { Node child = node.getFirstChild(); while (child != null) { if (tagName.equals(child.getNodeName())) { return child; } child = child.getNextSibling(); } return null; } /** * Helper to retrieve *this* {@link Node}s text content.<br> * Explicitly not calling {@link Element#getTextContent()} as this would traverse children until * any is found with text content.<br> */ private static String getNodeValue(Node node) { Node child = node.getFirstChild(); while (child != null) { if (child.getNodeType() == Node.TEXT_NODE) { return child.getNodeValue().trim(); } child = child.getNextSibling(); } return ""; } private static boolean hasValue(String value) { return value != null && !"".equals(value); } private static DocumentBuilder getDocumentBuilder() throws IOException { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setIgnoringComments(false); return factory.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new IOException(e); } } }