forked from Qortal/qortal
ADDED: TranslationXmlStreamReader + tests for XML based translation files
This commit is contained in:
parent
e9d8b3e6e3
commit
6bc0eeac4d
32
src/globalization/TranslationEntry.java
Normal file
32
src/globalization/TranslationEntry.java
Normal 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);
|
||||
}
|
||||
}
|
248
src/globalization/TranslationXmlStreamReader.java
Normal file
248
src/globalization/TranslationXmlStreamReader.java
Normal 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);
|
||||
}
|
||||
}
|
33
src/globalization/Translations.xsd
Normal file
33
src/globalization/Translations.xsd
Normal 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>
|
21
src/globalization/en-GB.xml
Normal file
21
src/globalization/en-GB.xml
Normal 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>
|
82
src/test/GlobalizationTests.java
Normal file
82
src/test/GlobalizationTests.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
21
src/test/utils/AssertExtensions.java
Normal file
21
src/test/utils/AssertExtensions.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
6
src/test/utils/EqualityComparer.java
Normal file
6
src/test/utils/EqualityComparer.java
Normal file
@ -0,0 +1,6 @@
|
||||
package test.utils;
|
||||
|
||||
public interface EqualityComparer<T> {
|
||||
boolean equals(T first, T second);
|
||||
int hashCode(T item);
|
||||
}
|
34
src/test/utils/EquatableWrapper.java
Normal file
34
src/test/utils/EquatableWrapper.java
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user