From 6bc0eeac4df70742b5bf183aa7d81b355c876e51 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 30 Sep 2018 23:57:27 +0200 Subject: [PATCH] ADDED: TranslationXmlStreamReader + tests for XML based translation files --- src/globalization/TranslationEntry.java | 32 +++ .../TranslationXmlStreamReader.java | 248 ++++++++++++++++++ src/globalization/Translations.xsd | 33 +++ src/globalization/en-GB.xml | 21 ++ src/test/GlobalizationTests.java | 82 ++++++ src/test/utils/AssertExtensions.java | 21 ++ src/test/utils/EqualityComparer.java | 6 + src/test/utils/EquatableWrapper.java | 34 +++ 8 files changed, 477 insertions(+) create mode 100644 src/globalization/TranslationEntry.java create mode 100644 src/globalization/TranslationXmlStreamReader.java create mode 100644 src/globalization/Translations.xsd create mode 100644 src/globalization/en-GB.xml create mode 100644 src/test/GlobalizationTests.java create mode 100644 src/test/utils/AssertExtensions.java create mode 100644 src/test/utils/EqualityComparer.java create mode 100644 src/test/utils/EquatableWrapper.java diff --git a/src/globalization/TranslationEntry.java b/src/globalization/TranslationEntry.java new file mode 100644 index 00000000..91a5114d --- /dev/null +++ b/src/globalization/TranslationEntry.java @@ -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); + } +} diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java new file mode 100644 index 00000000..903bad0e --- /dev/null +++ b/src/globalization/TranslationXmlStreamReader.java @@ -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 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 must be first in the document"); + + State state = new State(Locale.forLanguageTag("default"), "/"); + + List result = new ArrayList(); + 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 result) throws XMLStreamException { + assureStartElement(element, LOCALIZATION_TAG_NAME); + + Iterator 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 result) throws XMLStreamException { + assureStartElement(element, CONTEXT_TAG_NAME); + + Locale locale = state.locale; + String contextPath = state.path; + + Iterator 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 result) throws XMLStreamException { + assureStartElement(element, TRANSLATION_TAG_NAME); + + String path = null; + String template = null; + + Iterator 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() + ", 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); + } +} diff --git a/src/globalization/Translations.xsd b/src/globalization/Translations.xsd new file mode 100644 index 00000000..aff8ab76 --- /dev/null +++ b/src/globalization/Translations.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/globalization/en-GB.xml b/src/globalization/en-GB.xml new file mode 100644 index 00000000..581fe4ce --- /dev/null +++ b/src/globalization/en-GB.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/GlobalizationTests.java b/src/test/GlobalizationTests.java new file mode 100644 index 00000000..119a9d7e --- /dev/null +++ b/src/test/GlobalizationTests.java @@ -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 { + + @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 = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + List expected = new ArrayList(); + 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 actual = reader.ReadFrom(is); + + assertSetEquals(expected, actual, new TranslationEntryEqualityComparer()); + } + +} diff --git a/src/test/utils/AssertExtensions.java b/src/test/utils/AssertExtensions.java new file mode 100644 index 00000000..25ce1f55 --- /dev/null +++ b/src/test/utils/AssertExtensions.java @@ -0,0 +1,21 @@ +package test.utils; + +import java.util.HashSet; +import java.util.Set; +import org.junit.Assert; + +public class AssertExtensions { + + public static void assertSetEquals(Iterable expected, Iterable actual, EqualityComparer comparer) { + Set> expectedSet = new HashSet>(); + for(T item: expected) + expectedSet.add(new EquatableWrapper(item, comparer)); + + Set> actualSet = new HashSet>(); + for(T item: actual) + actualSet.add(new EquatableWrapper(item, comparer)); + + Assert.assertEquals(expectedSet, actualSet); + } + +} diff --git a/src/test/utils/EqualityComparer.java b/src/test/utils/EqualityComparer.java new file mode 100644 index 00000000..c560c9ce --- /dev/null +++ b/src/test/utils/EqualityComparer.java @@ -0,0 +1,6 @@ +package test.utils; + +public interface EqualityComparer { + boolean equals(T first, T second); + int hashCode(T item); +} diff --git a/src/test/utils/EquatableWrapper.java b/src/test/utils/EquatableWrapper.java new file mode 100644 index 00000000..b719cf32 --- /dev/null +++ b/src/test/utils/EquatableWrapper.java @@ -0,0 +1,34 @@ +package test.utils; + +class EquatableWrapper { + + private final T item; + private final EqualityComparer comparer; + + public EquatableWrapper(T item, EqualityComparer 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 otherWrapper = (EquatableWrapper)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(); + } +} \ No newline at end of file