ADDED: TranslationXmlStreamReader + tests for XML based translation files

This commit is contained in:
Kc 2018-09-30 23:57:27 +02:00
parent e9d8b3e6e3
commit 6bc0eeac4d
8 changed files with 477 additions and 0 deletions

View File

@ -0,0 +1,32 @@
package globalization;
import java.util.Locale;
public class TranslationEntry {
private Locale locale;
private String path;
private String template;
public TranslationEntry(Locale locale, String path, String template) {
this.locale = locale;
this.path = path;
this.template = template;
}
public Locale locale() {
return this.locale;
}
public String path() {
return this.path;
}
public String template() {
return this.template;
}
@Override
public String toString() {
return String.format("{locale: '%s', path: '%s', template: '%s'}", this.locale, this.path, this.template);
}
}

View File

@ -0,0 +1,248 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package globalization;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.*;
public class TranslationXmlStreamReader {
private class State {
public final Locale locale;
public final String path;
public State(Locale locale, String path) {
this.locale = locale;
this.path = path;
}
}
private static final String LOCALIZATION_TAG_NAME = "localization";
private static final String CONTEXT_TAG_NAME = "context";
private static final String CONTEXT_LOCALE_ATTRIBUTE_NAME = "locale";
private static final String CONTEXT_PATH_ATTRIBUTE_NAME = "path";
private static final String TRANSLATION_TAG_NAME = "translation";
private static final String TRANSLATION_KEY_ATTRIBUTE_NAME = "key";
private static final String TRANSLATION_TEMPLATE_ATTRIBUTE_NAME = "template";
public Iterable<TranslationEntry> ReadFrom(InputStream stream) throws XMLStreamException {
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
XMLEventReader eventReader = inputFactory.createXMLEventReader(stream);
XMLEvent element = eventReader.nextEvent();
if(!element.isStartDocument())
throw new javax.xml.stream.XMLStreamException("XML declaration <?xml ... ?> must be first in the document");
State state = new State(Locale.forLanguageTag("default"), "/");
List<TranslationEntry> result = new ArrayList<TranslationEntry>();
if (eventReader.hasNext())
{
XMLEvent event = eventReader.nextTag();
if (isStartElement(event, LOCALIZATION_TAG_NAME))
{
processLocalization(eventReader, (StartElement)event, state, result);
} else {
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
}
}
while (eventReader.hasNext())
{
XMLEvent event = eventReader.nextEvent();
switch(event.getEventType()) {
case XMLEvent.COMMENT:
break;
case XMLEvent.CHARACTERS:
if(!event.asCharacters().isIgnorableWhiteSpace())
throw new javax.xml.stream.XMLStreamException("Unexpected content after end of root element: " + event.toString());
break;
case XMLEvent.END_DOCUMENT:
return result;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected content after end of root element: " + event.toString());
}
}
throw new javax.xml.stream.XMLStreamException("End of document not found");
}
private void processLocalization(XMLEventReader eventReader, StartElement element, State state, List<TranslationEntry> result) throws XMLStreamException {
assureStartElement(element, LOCALIZATION_TAG_NAME);
Iterator<Attribute> attributes = element.getAttributes();
while (attributes.hasNext())
{
Attribute attribute = attributes.next();
QName name = attribute.getName();
throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
}
XMLEvent event;
while(!(event = eventReader.nextTag()).isEndElement()) {
if(event.isStartElement()) {
StartElement childElement = (StartElement)event;
switch(childElement.getName().toString()) {
case CONTEXT_TAG_NAME:
processContext(eventReader, childElement, state, result);
break;
case TRANSLATION_TAG_NAME:
processTranslation(eventReader, childElement, state, result);
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
}
} else {
throw new javax.xml.stream.XMLStreamException("Unexpected content: " + event.toString());
}
}
assureEndElement(event, LOCALIZATION_TAG_NAME);
}
private void processContext(XMLEventReader eventReader, StartElement element, State state, List<TranslationEntry> result) throws XMLStreamException {
assureStartElement(element, CONTEXT_TAG_NAME);
Locale locale = state.locale;
String contextPath = state.path;
Iterator<Attribute> attributes = element.getAttributes();
while (attributes.hasNext())
{
Attribute attribute = attributes.next();
QName name = attribute.getName();
String value = attribute.getValue();
switch(name.toString()) {
case CONTEXT_LOCALE_ATTRIBUTE_NAME:
locale = Locale.forLanguageTag(value);
break;
case CONTEXT_PATH_ATTRIBUTE_NAME:
assureIsValidPathExtension(value);
contextPath = combinePaths(contextPath, value);
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
}
}
state = new State(locale, contextPath);
XMLEvent event;
while(!(event = eventReader.nextTag()).isEndElement()) {
if(event.isStartElement()) {
StartElement childElement = (StartElement)event;
switch(childElement.getName().toString()) {
case CONTEXT_TAG_NAME:
processContext(eventReader, childElement, state, result);
break;
case TRANSLATION_TAG_NAME:
processTranslation(eventReader, childElement, state, result);
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
}
} else {
throw new javax.xml.stream.XMLStreamException("Unexpected content: " + event.toString());
}
}
assureEndElement(event, CONTEXT_TAG_NAME);
}
private void processTranslation(XMLEventReader eventReader, StartElement element, State state, List<TranslationEntry> result) throws XMLStreamException {
assureStartElement(element, TRANSLATION_TAG_NAME);
String path = null;
String template = null;
Iterator<Attribute> attributes = element.getAttributes();
while (attributes.hasNext())
{
Attribute attribute = attributes.next();
QName name = attribute.getName();
String value = attribute.getValue();
switch(name.toString()) {
case TRANSLATION_KEY_ATTRIBUTE_NAME:
assureIsValidPathExtension(value);
path = combinePaths(state.path, value);
break;
case TRANSLATION_TEMPLATE_ATTRIBUTE_NAME:
template = value;
break;
default:
throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name);
}
}
XMLEvent event;
while(!(event = eventReader.nextTag()).isEndElement()) {
if(event.isStartElement()) {
throw new javax.xml.stream.XMLStreamException("Unexpected element: " + event.toString());
} else if(event.isCharacters()) {
if(template != null)
throw new javax.xml.stream.XMLStreamException("Content must be empty if 'template' attribute is used");
template = event.asCharacters().getData();
}
}
assureEndElement(event, TRANSLATION_TAG_NAME);
if(path == null)
throw new javax.xml.stream.XMLStreamException("Missing attribute: " + TRANSLATION_KEY_ATTRIBUTE_NAME);
if(template == null)
throw new javax.xml.stream.XMLStreamException("Missing attribute: " + TRANSLATION_TEMPLATE_ATTRIBUTE_NAME);
result.add(new TranslationEntry(state.locale, path, template));
}
private void assureIsValidPathExtension(String value) throws XMLStreamException {
for(String part : value.split("/")) {
if(part.equalsIgnoreCase(".."))
throw new javax.xml.stream.XMLStreamException("Parent reference .. is not allowed");
}
}
private String combinePaths(String left, String right) {
return Paths.get(left, right).toString();
}
private void assureStartElement(XMLEvent event, String name) throws XMLStreamException {
if(!isStartElement(event, name))
throw new javax.xml.stream.XMLStreamException("Unexpected start element: " + event.toString() + ", <" + name + "> expected");
}
private void assureEndElement(XMLEvent event, String name) throws XMLStreamException {
if(!isEndElement(event, name))
throw new javax.xml.stream.XMLStreamException("Unexpected end element: " + event.toString() + ", </" + name + "> expected");
}
private boolean isStartElement(XMLEvent event, String name) {
if(!event.isStartElement())
return false;
StartElement element = ((StartElement)event);
return element.getName().toString().equals(name);
}
private boolean isEndElement(XMLEvent event, String name) {
if(!event.isEndElement())
return false;
EndElement element = ((EndElement)event);
return element.getName().toString().equals(name);
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0"?>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<xs:schema version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified">
<xs:complexType name="localizationType">
<xs:all>
<xs:element name="context" minOccurs="1" maxOccurs="unbounded" />
</xs:all>
</xs:complexType>
<xs:complexType name="contextType">
<xs:attribute name="path" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:attribute name="locale" type="xs:string" minOccurs="0" maxOccurs="1" />
<xs:all>
<xs:element type="translation" minOccurs="0" maxOccurs="unbounded" />
<xs:element type="context" minOccurs="0" maxOccurs="unbounded" />
</xs:all>
</xs:complexType>
<xs:complexType name="translationType">
<xs:attribute name="keyPath" type="xs:string" minOccurs="1" maxOccurs="1" />
<xs:attribute name="template" type="xs:string" minOccurs="1" maxOccurs="1" />
</xs:complexType>
<xs:element name="localization" type="localizationType" />
</xs:schema>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<localization>
<context locale="en-GB">
<context path="Api">
<context path="ApiError">
<translation key="0" template="unknown error" />
<translation key="1" template="failed to parse json message" />
<translation key="2" template="not enough balance" />
<translation key="3" template="that feature is not yet released" />
</context>
<context path="BlocksResource">
<context path="GET byheight">
<translation key="success.description" template="Test" />
</context>
</context>
</context>
</context>
</localization>

View File

@ -0,0 +1,82 @@
package test;
import globalization.TranslationEntry;
import globalization.TranslationXmlStreamReader;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.xml.stream.XMLStreamException;
import org.junit.Assert;
import static org.junit.Assert.*;
import static test.utils.AssertExtensions.*;
import org.junit.Test;
import test.utils.EqualityComparer;
public class GlobalizationTests {
private class TranslationEntryEqualityComparer implements EqualityComparer<TranslationEntry> {
@Override
public boolean equals(TranslationEntry first, TranslationEntry second) {
if(first == null && second == null)
return true;
if(first == null && second != null || first != null && second == null)
return false;
if(!first.locale().equals(second.locale()))
return false;
if(!first.path().equals(second.path()))
return false;
if(!first.template().equals(second.template()))
return false;
return true;
}
@Override
public int hashCode(TranslationEntry item) {
int hash = 17;
final int multiplier = 59;
hash = hash * multiplier + item.locale().hashCode();
hash = hash * multiplier + item.path().hashCode();
hash = hash * multiplier + item.template().hashCode();
return hash;
}
}
@Test
public void TestTranslationXmlReader() throws XMLStreamException {
String xml =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<localization>\n" +
" <context locale=\"en-GB\">\n" +
" <context path=\"path1\">\n" +
" <context path=\"path2/path3\">\n" +
" <translation key=\"key1\" template=\"1\" />\n" +
" <translation key=\"key2\" template=\"2\" />\n" +
" </context>\n" +
" </context>\n" +
" </context>\n" +
"</localization>\n";
List<TranslationEntry> expected = new ArrayList<TranslationEntry>();
expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path2/path3/key1", "1"));
expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path2/path3/key2", "2"));
InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8")));
TranslationXmlStreamReader reader = new TranslationXmlStreamReader();
Iterable<TranslationEntry> actual = reader.ReadFrom(is);
assertSetEquals(expected, actual, new TranslationEntryEqualityComparer());
}
}

View File

@ -0,0 +1,21 @@
package test.utils;
import java.util.HashSet;
import java.util.Set;
import org.junit.Assert;
public class AssertExtensions {
public static <T> void assertSetEquals(Iterable<T> expected, Iterable<T> actual, EqualityComparer<T> comparer) {
Set<EquatableWrapper<T>> expectedSet = new HashSet<EquatableWrapper<T>>();
for(T item: expected)
expectedSet.add(new EquatableWrapper<T>(item, comparer));
Set<EquatableWrapper<T>> actualSet = new HashSet<EquatableWrapper<T>>();
for(T item: actual)
actualSet.add(new EquatableWrapper<T>(item, comparer));
Assert.assertEquals(expectedSet, actualSet);
}
}

View File

@ -0,0 +1,6 @@
package test.utils;
public interface EqualityComparer<T> {
boolean equals(T first, T second);
int hashCode(T item);
}

View File

@ -0,0 +1,34 @@
package test.utils;
class EquatableWrapper<T> {
private final T item;
private final EqualityComparer<T> comparer;
public EquatableWrapper(T item, EqualityComparer<T> comparer) {
this.item = item;
this.comparer = comparer;
}
@Override
public boolean equals(Object obj) {
if(obj == null)
return false;
if (!(this.getClass().isInstance(obj)))
return false;
EquatableWrapper<T> otherWrapper = (EquatableWrapper<T>)obj;
if (otherWrapper.item == this.item)
return true;
return this.comparer.equals(this.item, otherWrapper.item);
}
@Override
public int hashCode() {
return this.comparer.hashCode(this.item);
}
@Override
public String toString() {
return this.item.toString();
}
}