From 6bc0eeac4df70742b5bf183aa7d81b355c876e51 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 30 Sep 2018 23:57:27 +0200 Subject: [PATCH 01/19] 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 From d24f1de36a5d7781dfc9f6ae2058bedfa931201b Mon Sep 17 00:00:00 2001 From: Kc Date: Wed, 3 Oct 2018 01:06:50 +0200 Subject: [PATCH 02/19] CHANGED: added translation context path normalization --- src/globalization/TranslationXmlStreamReader.java | 9 +++++++-- src/test/GlobalizationTests.java | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java index 903bad0e..50cc86e0 100644 --- a/src/globalization/TranslationXmlStreamReader.java +++ b/src/globalization/TranslationXmlStreamReader.java @@ -179,7 +179,7 @@ public class TranslationXmlStreamReader { String value = attribute.getValue(); switch(name.toString()) { case TRANSLATION_KEY_ATTRIBUTE_NAME: - assureIsValidPathExtension(value); + assureIsValidKey(value); path = combinePaths(state.path, value); break; case TRANSLATION_TEMPLATE_ATTRIBUTE_NAME: @@ -218,8 +218,13 @@ public class TranslationXmlStreamReader { } } + private void assureIsValidKey(String value) throws XMLStreamException { + if(value.contains("/")) + throw new javax.xml.stream.XMLStreamException("Key must not contain /"); + } + private String combinePaths(String left, String right) { - return Paths.get(left, right).toString(); + return Paths.get(left, right).normalize().toString(); } private void assureStartElement(XMLEvent event, String name) throws XMLStreamException { diff --git a/src/test/GlobalizationTests.java b/src/test/GlobalizationTests.java index 119a9d7e..e38e290d 100644 --- a/src/test/GlobalizationTests.java +++ b/src/test/GlobalizationTests.java @@ -59,9 +59,9 @@ public class GlobalizationTests { "\n" + "\n" + " \n" + - " \n" + - " \n" + - " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + " \n" + @@ -69,7 +69,7 @@ public class GlobalizationTests { "\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/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"))); From aa7bdaf71325421b202c348c2cdd0e24ac96254a Mon Sep 17 00:00:00 2001 From: Kc Date: Thu, 4 Oct 2018 16:15:46 +0200 Subject: [PATCH 03/19] CHANGED: read translations from XML files --- .../globalization => globalization}/en-GB.xml | 0 src/Start.java | 6 +- src/api/BlocksResource.java | 2 +- src/globalization/ContextPaths.java | 23 +++ .../TranslationXmlStreamReader.java | 12 +- src/globalization/Translator.java | 144 +++++++++++++++--- src/settings/Settings.java | 27 +++- 7 files changed, 176 insertions(+), 38 deletions(-) rename {src/globalization => globalization}/en-GB.xml (100%) create mode 100644 src/globalization/ContextPaths.java diff --git a/src/globalization/en-GB.xml b/globalization/en-GB.xml similarity index 100% rename from src/globalization/en-GB.xml rename to globalization/en-GB.xml diff --git a/src/Start.java b/src/Start.java index 585f65c6..5afe3589 100644 --- a/src/Start.java +++ b/src/Start.java @@ -18,8 +18,8 @@ public class Start { apiService.start(); //// testing the API client - //ApiClient client = ApiClient.getInstance(); - //String test = client.executeCommand("GET blocks/height"); - //System.out.println(test); + ApiClient client = ApiClient.getInstance(); + String test = client.executeCommand("GET blocks/height"); + System.out.println(test); } } diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index e4f7f09b..de4a30e9 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -27,7 +27,7 @@ public class BlocksResource { private ApiErrorFactory apiErrorFactory; public BlocksResource() { - this(new ApiErrorFactory(new Translator())); + this(new ApiErrorFactory(Translator.getInstance())); } public BlocksResource(ApiErrorFactory apiErrorFactory) { diff --git a/src/globalization/ContextPaths.java b/src/globalization/ContextPaths.java new file mode 100644 index 00000000..6f6a35a0 --- /dev/null +++ b/src/globalization/ContextPaths.java @@ -0,0 +1,23 @@ +package globalization; + +import java.nio.file.Paths; + +public class ContextPaths { + + public static boolean isValidKey(String value) { + return !value.contains("/"); + } + + public static String combinePaths(String left, String right) { + return Paths.get("/", left, right).normalize().toString(); + } + + public static String getParent(String path) { + return combinePaths(path, ".."); + } + + public static boolean isRoot(String path) { + return path.equals("/"); + } + +} diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java index 50cc86e0..45a95a6b 100644 --- a/src/globalization/TranslationXmlStreamReader.java +++ b/src/globalization/TranslationXmlStreamReader.java @@ -134,7 +134,7 @@ public class TranslationXmlStreamReader { break; case CONTEXT_PATH_ATTRIBUTE_NAME: assureIsValidPathExtension(value); - contextPath = combinePaths(contextPath, value); + contextPath = ContextPaths.combinePaths(contextPath, value); break; default: throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name); @@ -180,7 +180,7 @@ public class TranslationXmlStreamReader { switch(name.toString()) { case TRANSLATION_KEY_ATTRIBUTE_NAME: assureIsValidKey(value); - path = combinePaths(state.path, value); + path = ContextPaths.combinePaths(state.path, value); break; case TRANSLATION_TEMPLATE_ATTRIBUTE_NAME: template = value; @@ -219,14 +219,10 @@ public class TranslationXmlStreamReader { } private void assureIsValidKey(String value) throws XMLStreamException { - if(value.contains("/")) - throw new javax.xml.stream.XMLStreamException("Key must not contain /"); + if(!ContextPaths.isValidKey(value)) + throw new javax.xml.stream.XMLStreamException("Key is not valid"); } - private String combinePaths(String left, String right) { - return Paths.get(left, right).normalize().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"); diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java index da61c595..fa9a17f2 100644 --- a/src/globalization/Translator.java +++ b/src/globalization/Translator.java @@ -1,13 +1,85 @@ package globalization; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.InputStream; import java.util.AbstractMap; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.xml.stream.XMLStreamException; import org.apache.commons.text.StringSubstitutor; +import settings.Settings; + public class Translator { + Map> translations = new HashMap>(); + + //XXX: replace singleton pattern by dependency injection? + private static Translator instance; + + private Translator() { + InitializeTranslations(); + } + + public static Translator getInstance() { + if (instance == null) { + instance = new Translator(); + } + + return instance; + } + + private Settings settings() { + return Settings.getInstance(); + } + + private void InitializeTranslations() { + String path = this.settings().translationsPath(); + File dir = new File(path); + File [] files = dir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".xml"); + } + }); + + Map> translations = new HashMap<>(); + TranslationXmlStreamReader translationReader = new TranslationXmlStreamReader(); + for (File file : files) { + Iterable entries = null; + try { + InputStream stream = new FileInputStream(file); + entries = translationReader.ReadFrom(stream); + } catch (FileNotFoundException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Translation file not found: %s", file), ex); + } catch (XMLStreamException ex) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Error in translation file: %s", file), ex); + } + + for(TranslationEntry entry : entries) { + Map localTranslations = translations.get(entry.locale()); + if(localTranslations == null) { + localTranslations = new HashMap<>(); + translations.put(entry.locale(), localTranslations); + } + + if(localTranslations.containsKey(entry.path())) { + Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Duplicate entry for locale '%s' and path '%s' in translation file '%s'. Falling back to default translations.", entry.locale(), entry.path(), file)); + return; + } + } + } + + // everything is fine, so we store all read translations + this.translations = translations; + } + private Map createMap(Map.Entry[] entries) { HashMap map = new HashMap<>(); for (AbstractMap.Entry entry : entries) { @@ -16,37 +88,59 @@ public class Translator { return map; } - //XXX: replace singleton pattern by dependency injection? - private static Translator instance; + public String translate(Locale locale, String contextPath, String templateKey, AbstractMap.Entry... templateValues) { + Map map = createMap(templateValues); + return translate(locale, contextPath, templateKey, map); + } - public static Translator getInstance() { - if (instance == null) { - instance = new Translator(); + public String translate(Locale locale, String contextPath, String templateKey, Map templateValues) { + return translate(locale, contextPath, templateKey, null, templateValues); + } + + public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { + Map map = createMap(templateValues); + return translate(locale, contextPath, templateKey, defaultTemplate, map); + } + + public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, Map templateValues) { + // look for requested language + String template = getTemplateFromNearestPath(locale, contextPath, templateKey); + + if(template == null) { + // scan default languages + for(String language : this.settings().translationsDefaultLocales()) { + Locale defaultLocale = Locale.forLanguageTag(language); + template = getTemplateFromNearestPath(defaultLocale, contextPath, templateKey); + if(template != null) + break; + } } - - return instance; - } - - public String translate(Locale locale, String templateKey, AbstractMap.Entry... templateValues) { - Map map = createMap(templateValues); - return translate(locale, templateKey, map); - } - - public String translate(Locale locale, String templateKey, Map templateValues) { - return translate(locale, templateKey, null, templateValues); - } - - public String translate(Locale locale, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { - Map map = createMap(templateValues); - return translate(locale, templateKey, defaultTemplate, map); - } - - public String translate(Locale locale, String templateKey, String defaultTemplate, Map templateValues) { - String template = defaultTemplate; // TODO: get template for the given locale if available + + if(template == null) + template = defaultTemplate; // fallback template StringSubstitutor sub = new StringSubstitutor(templateValues); String result = sub.replace(template); return result; } + + private String getTemplateFromNearestPath(Locale locale, String contextPath, String templateKey) { + Map localTranslations = this.translations.get(locale); + if(localTranslations == null) + return null; + + String template = null; + while(true) { + String path = ContextPaths.combinePaths(contextPath, templateKey); + template = localTranslations.get(path); + if(template != null) + break; // found template + if(ContextPaths.isRoot(contextPath)) + break; // nothing found + contextPath = ContextPaths.getParent(contextPath); + } + + return template; + } } diff --git a/src/settings/Settings.java b/src/settings/Settings.java index 288020b6..46df2c29 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -24,11 +24,15 @@ public class Settings { private int maxBytePerFee = 1024; private String userpath = ""; - //RPC + // RPC private int rpcPort = 9085; private List rpcAllowed = new ArrayList(Arrays.asList("127.0.0.1", "::1")); // ipv4, ipv6 private boolean rpcEnabled = true; + // Globalization + private String translationsPath = "globalization/"; + private String[] translationsDefaultLocales = {"en-GB"}; + // Constants private static final String SETTINGS_FILENAME = "settings.json"; @@ -129,6 +133,17 @@ public class Settings { { this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue(); } + + // Globalization + if(json.containsKey("translationspath")) + { + this.translationsPath = ((String) json.get("translationspath")); + } + + if(json.containsKey("translationsdefaultlocales")) + { + this.translationsDefaultLocales = ((String[]) json.get("translationsdefaultlocales")); + } } public boolean isTestNet() { @@ -163,4 +178,14 @@ public class Settings { { return this.rpcEnabled; } + + public String translationsPath() + { + return this.translationsPath; + } + + public String[] translationsDefaultLocales() + { + return this.translationsDefaultLocales; + } } From 730b5033d17156fb995d487d0f0394c6d223b9cf Mon Sep 17 00:00:00 2001 From: Kc Date: Thu, 4 Oct 2018 22:58:04 +0200 Subject: [PATCH 04/19] CHANGED: switched to JUnit5 CHANGED: globalization tests --- pom.xml | 10 ++ src/Start.java | 6 +- src/globalization/ContextPaths.java | 11 +- .../TranslationXmlStreamReader.java | 6 +- src/test/ATTests.java | 5 +- src/test/BlockTests.java | 9 +- src/test/BlockchainTests.java | 3 +- src/test/Common.java | 10 +- src/test/CompatibilityTests.java | 5 +- src/test/CryptoTests.java | 9 +- src/test/ExceptionTests.java | 4 +- src/test/GenesisTests.java | 15 +- src/test/GlobalizationTests.java | 105 ++++++++++++- src/test/LoadTests.java | 29 ++-- src/test/NavigationTests.java | 15 +- src/test/RepositoryTests.java | 4 +- src/test/SaveTests.java | 3 +- src/test/SerializationTests.java | 11 +- src/test/SignatureTests.java | 5 +- src/test/TransactionTests.java | 142 +++++++++--------- src/test/utils/AssertExtensions.java | 43 +++++- 21 files changed, 294 insertions(+), 156 deletions(-) diff --git a/pom.xml b/pom.xml index 72da220b..53655871 100644 --- a/pom.xml +++ b/pom.xml @@ -118,5 +118,15 @@ 2.4.1 test + + org.junit.jupiter + junit-jupiter-engine + 5.3.1 + + + org.hamcrest + hamcrest-library + 1.3 + \ No newline at end of file diff --git a/src/Start.java b/src/Start.java index 5afe3589..585f65c6 100644 --- a/src/Start.java +++ b/src/Start.java @@ -18,8 +18,8 @@ public class Start { apiService.start(); //// testing the API client - ApiClient client = ApiClient.getInstance(); - String test = client.executeCommand("GET blocks/height"); - System.out.println(test); + //ApiClient client = ApiClient.getInstance(); + //String test = client.executeCommand("GET blocks/height"); + //System.out.println(test); } } diff --git a/src/globalization/ContextPaths.java b/src/globalization/ContextPaths.java index 6f6a35a0..e924b25d 100644 --- a/src/globalization/ContextPaths.java +++ b/src/globalization/ContextPaths.java @@ -1,13 +1,22 @@ package globalization; import java.nio.file.Paths; +import javax.xml.stream.XMLStreamException; public class ContextPaths { public static boolean isValidKey(String value) { - return !value.contains("/"); + return !value.contains("/") && !ContextPaths.containsParentReference(value); } + public static boolean containsParentReference(String value) { + for(String part : value.split("/")) { + if(part.equalsIgnoreCase("..")) + return true; + } + return false; + } + public static String combinePaths(String left, String right) { return Paths.get("/", left, right).normalize().toString(); } diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java index 45a95a6b..db4f76be 100644 --- a/src/globalization/TranslationXmlStreamReader.java +++ b/src/globalization/TranslationXmlStreamReader.java @@ -212,10 +212,8 @@ public class TranslationXmlStreamReader { } 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"); - } + if(ContextPaths.containsParentReference(value)) + throw new javax.xml.stream.XMLStreamException("Parent reference .. is not allowed"); } private void assureIsValidKey(String value) throws XMLStreamException { diff --git a/src/test/ATTests.java b/src/test/ATTests.java index 076bb927..4d2942eb 100644 --- a/src/test/ATTests.java +++ b/src/test/ATTests.java @@ -1,12 +1,11 @@ package test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; import java.util.Arrays; -import org.junit.Test; - import com.google.common.hash.HashCode; import data.block.BlockData; diff --git a/src/test/BlockTests.java b/src/test/BlockTests.java index b0485c3a..f70370f8 100644 --- a/src/test/BlockTests.java +++ b/src/test/BlockTests.java @@ -1,11 +1,10 @@ package test; -import static org.junit.Assert.*; - import java.math.BigDecimal; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import data.block.BlockData; import data.transaction.TransactionData; @@ -67,7 +66,7 @@ public class BlockTests extends Common { // Block 949 has lots of varied transactions // Blocks 390 & 754 have only payment transactions BlockData blockData = repository.getBlockRepository().fromHeight(754); - assertNotNull("Block 754 is required for this test", blockData); + assertNotNull(blockData, "Block 754 is required for this test"); Block block = new Block(repository, blockData); assertTrue(block.isSignatureValid()); @@ -108,7 +107,7 @@ public class BlockTests extends Common { // Block 949 has lots of varied transactions // Blocks 390 & 754 have only payment transactions BlockData blockData = repository.getBlockRepository().fromHeight(754); - assertNotNull("Block 754 is required for this test", blockData); + assertNotNull(blockData, "Block 754 is required for this test"); Block block = new Block(repository, blockData); assertTrue(block.isSignatureValid()); diff --git a/src/test/BlockchainTests.java b/src/test/BlockchainTests.java index 264f8773..bdb2662f 100644 --- a/src/test/BlockchainTests.java +++ b/src/test/BlockchainTests.java @@ -1,6 +1,7 @@ package test; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import qora.block.BlockChain; import repository.DataException; diff --git a/src/test/Common.java b/src/test/Common.java index e3734218..fe1788ef 100644 --- a/src/test/Common.java +++ b/src/test/Common.java @@ -1,7 +1,9 @@ package test; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterAll; import repository.DataException; import repository.RepositoryFactory; @@ -13,13 +15,13 @@ public class Common { // public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true;sql.pad_space=false"; public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true"; - @BeforeClass + @BeforeAll public static void setRepository() throws DataException { RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); RepositoryManager.setRepositoryFactory(repositoryFactory); } - @AfterClass + @AfterAll public static void closeRepository() throws DataException { RepositoryManager.closeRepositoryFactory(); } diff --git a/src/test/CompatibilityTests.java b/src/test/CompatibilityTests.java index 3c630697..d5c3e154 100644 --- a/src/test/CompatibilityTests.java +++ b/src/test/CompatibilityTests.java @@ -1,8 +1,7 @@ package test; -import static org.junit.Assert.*; - -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import com.google.common.hash.HashCode; diff --git a/src/test/CryptoTests.java b/src/test/CryptoTests.java index dfe7299b..b90e335a 100644 --- a/src/test/CryptoTests.java +++ b/src/test/CryptoTests.java @@ -1,8 +1,7 @@ package test; -import static org.junit.Assert.*; - -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import com.google.common.hash.HashCode; @@ -16,7 +15,7 @@ public class CryptoTests { byte[] digest = Crypto.digest(input); byte[] expected = HashCode.fromString("6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d").asBytes(); - assertArrayEquals(digest, expected); + assertArrayEquals(expected, digest); } @Test @@ -25,7 +24,7 @@ public class CryptoTests { byte[] digest = Crypto.doubleDigest(input); byte[] expected = HashCode.fromString("1406e05881e299367766d313e26c05564ec91bf721d31726bd6e46e60689539a").asBytes(); - assertArrayEquals(digest, expected); + assertArrayEquals(expected, digest); } @Test diff --git a/src/test/ExceptionTests.java b/src/test/ExceptionTests.java index 37100d53..b0029a8b 100644 --- a/src/test/ExceptionTests.java +++ b/src/test/ExceptionTests.java @@ -1,8 +1,8 @@ package test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; -import org.junit.Test; import qora.block.Block; public class ExceptionTests { diff --git a/src/test/GenesisTests.java b/src/test/GenesisTests.java index aa2f002b..504c6c93 100644 --- a/src/test/GenesisTests.java +++ b/src/test/GenesisTests.java @@ -1,13 +1,12 @@ package test; -import static org.junit.Assert.*; - import java.math.BigDecimal; import java.util.List; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.AfterAll; import data.transaction.TransactionData; import qora.account.Account; @@ -26,13 +25,13 @@ public class GenesisTests { public static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true"; - @BeforeClass + @BeforeAll public static void setRepository() throws DataException { RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); RepositoryManager.setRepositoryFactory(repositoryFactory); } - @AfterClass + @AfterAll public static void closeRepository() throws DataException { RepositoryManager.closeRepositoryFactory(); } @@ -40,7 +39,7 @@ public class GenesisTests { @Test public void testGenesisBlockTransactions() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); + assertEquals(0, repository.getBlockRepository().getBlockchainHeight(), "Blockchain should be empty for this test"); GenesisBlock block = new GenesisBlock(repository); diff --git a/src/test/GlobalizationTests.java b/src/test/GlobalizationTests.java index e38e290d..d1e385de 100644 --- a/src/test/GlobalizationTests.java +++ b/src/test/GlobalizationTests.java @@ -11,11 +11,14 @@ 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 org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.*; + +import static test.utils.AssertExtensions.*; import test.utils.EqualityComparer; public class GlobalizationTests { @@ -54,7 +57,7 @@ public class GlobalizationTests { } @Test - public void TestTranslationXmlReader() throws XMLStreamException { + public void TestTranslationXmlReaderContextPaths() throws XMLStreamException { String xml = "\n" + "\n" + @@ -64,6 +67,9 @@ public class GlobalizationTests { " \n" + " \n" + " \n" + + " \n" + + " \n" + + " \n" + " \n" + " \n" + "\n"; @@ -71,12 +77,99 @@ public class GlobalizationTests { List expected = new ArrayList(); expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/key1", "1")); expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path2/path3/key2", "2")); + expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path4/key3", "3")); InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8"))); TranslationXmlStreamReader reader = new TranslationXmlStreamReader(); Iterable actual = reader.ReadFrom(is); - assertSetEquals(expected, actual, new TranslationEntryEqualityComparer()); + for(TranslationEntry i:expected)System.out.println(i);for(TranslationEntry i:actual)System.out.println(i); + assertItemsEqual(expected, actual, new TranslationEntryEqualityComparer()); } + @Test + public void TestTranslationXmlReaderLocales() throws XMLStreamException { + String xml = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + List expected = new ArrayList(); + expected.add(new TranslationEntry(Locale.forLanguageTag("default"), "/key1", "1")); + expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/key2", "2")); + expected.add(new TranslationEntry(Locale.forLanguageTag("de-DE"), "/path1/path2/key3", "3")); + + InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8"))); + TranslationXmlStreamReader reader = new TranslationXmlStreamReader(); + Iterable actual = reader.ReadFrom(is); + + assertItemsEqual(expected, actual, new TranslationEntryEqualityComparer()); + } + + @Test + public void TestTranslationXmlReader_BadPath() { + String xml = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8"))); + TranslationXmlStreamReader reader = new TranslationXmlStreamReader(); + + assertThrows(XMLStreamException.class, () -> reader.ReadFrom(is)); + } + + @Test + public void TestTranslationXmlReader_BadKey1() { + String xml = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8"))); + TranslationXmlStreamReader reader = new TranslationXmlStreamReader(); + + assertThrows(XMLStreamException.class, () -> reader.ReadFrom(is)); + } + + @Test + public void TestTranslationXmlReader_BadKey2() { + String xml = + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + InputStream is = new ByteArrayInputStream(xml.getBytes(Charset.forName("UTF-8"))); + TranslationXmlStreamReader reader = new TranslationXmlStreamReader(); + + assertThrows(XMLStreamException.class, () -> reader.ReadFrom(is)); + } } diff --git a/src/test/LoadTests.java b/src/test/LoadTests.java index aa617985..ab86a7c7 100644 --- a/src/test/LoadTests.java +++ b/src/test/LoadTests.java @@ -1,8 +1,7 @@ package test; -import static org.junit.Assert.*; - -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import data.transaction.PaymentTransactionData; import data.transaction.TransactionData; @@ -21,25 +20,25 @@ public class LoadTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { TransactionRepository transactionRepository = repository.getTransactionRepository(); - assertTrue("Migrate from old database to at least block 49778 before running this test", - repository.getBlockRepository().getBlockchainHeight() >= 49778); + assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778, + "Migrate from old database to at least block 49778 before running this test"); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); TransactionData transactionData = transactionRepository.fromSignature(signature); - assertNotNull("Transaction data not loaded from repository", transactionData); - assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType()); - assertEquals(PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey()), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); + assertNotNull(transactionData, "Transaction data not loaded from repository"); + assertEquals(TransactionType.PAYMENT, transactionData.getType(), "Transaction data not PAYMENT type"); + assertEquals("QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E", PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey())); PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData; assertNotNull(paymentTransactionData); - assertEquals(PublicKeyAccount.getAddress(paymentTransactionData.getSenderPublicKey()), "QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E"); - assertEquals(paymentTransactionData.getRecipient(), "QZsv8vbJ6QfrBNba4LMp5UtHhAzhrxvVUU"); - assertEquals(paymentTransactionData.getTimestamp(), 1416209264000L); - assertEquals(Base58.encode(paymentTransactionData.getReference()), - "31dC6kHHBeG5vYb8LMaZDjLEmhc9kQB2VUApVd8xWncSRiXu7yMejdprjYFMP2rUnzZxWd4KJhkq6LsV7rQvU1kY"); + assertEquals("QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E", PublicKeyAccount.getAddress(paymentTransactionData.getSenderPublicKey())); + assertEquals("QZsv8vbJ6QfrBNba4LMp5UtHhAzhrxvVUU", paymentTransactionData.getRecipient()); + assertEquals(1416209264000L, paymentTransactionData.getTimestamp()); + assertEquals("31dC6kHHBeG5vYb8LMaZDjLEmhc9kQB2VUApVd8xWncSRiXu7yMejdprjYFMP2rUnzZxWd4KJhkq6LsV7rQvU1kY", + Base58.encode(paymentTransactionData.getReference())); } } @@ -48,8 +47,8 @@ public class LoadTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { TransactionRepository transactionRepository = repository.getTransactionRepository(); - assertTrue("Migrate from old database to at least block 49778 before running this test", - repository.getBlockRepository().getBlockchainHeight() >= 49778); + assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778, + "Migrate from old database to at least block 49778 before running this test"); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); diff --git a/src/test/NavigationTests.java b/src/test/NavigationTests.java index 42cfdcb6..504b7d1b 100644 --- a/src/test/NavigationTests.java +++ b/src/test/NavigationTests.java @@ -1,8 +1,7 @@ package test; -import static org.junit.Assert.*; - -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import data.block.BlockData; import data.transaction.TransactionData; @@ -20,8 +19,8 @@ public class NavigationTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { TransactionRepository transactionRepository = repository.getTransactionRepository(); - assertTrue("Migrate from old database to at least block 49778 before running this test", - repository.getBlockRepository().getBlockchainHeight() >= 49778); + assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778, + "Migrate from old database to at least block 49778 before running this test"); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); @@ -29,11 +28,11 @@ public class NavigationTests extends Common { System.out.println("Navigating to Block from transaction " + signature58); TransactionData transactionData = transactionRepository.fromSignature(signature); - assertNotNull("Transaction data not loaded from repository", transactionData); - assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType()); + assertNotNull(transactionData, "Transaction data not loaded from repository"); + assertEquals(TransactionType.PAYMENT, transactionData.getType(), "Transaction data not PAYMENT type"); BlockData blockData = transactionRepository.getBlockDataFromSignature(signature); - assertNotNull("Block 49778 not loaded from database", blockData); + assertNotNull(blockData, "Block 49778 not loaded from database"); System.out.println("Block " + blockData.getHeight() + ", signature: " + Base58.encode(blockData.getSignature())); diff --git a/src/test/RepositoryTests.java b/src/test/RepositoryTests.java index a37b71c6..0658d98a 100644 --- a/src/test/RepositoryTests.java +++ b/src/test/RepositoryTests.java @@ -1,10 +1,10 @@ package test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.junit.Test; import repository.DataException; import repository.Repository; diff --git a/src/test/SaveTests.java b/src/test/SaveTests.java index 1338e3b8..80bbf74e 100644 --- a/src/test/SaveTests.java +++ b/src/test/SaveTests.java @@ -3,7 +3,8 @@ package test; import java.math.BigDecimal; import java.time.Instant; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import data.transaction.PaymentTransactionData; import qora.account.PublicKeyAccount; diff --git a/src/test/SerializationTests.java b/src/test/SerializationTests.java index ede95240..61a1b0e8 100644 --- a/src/test/SerializationTests.java +++ b/src/test/SerializationTests.java @@ -1,12 +1,11 @@ package test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.util.Arrays; import java.util.List; -import org.junit.Test; - import data.block.BlockData; import data.transaction.GenesisTransactionData; import data.transaction.TransactionData; @@ -61,15 +60,15 @@ public class SerializationTests extends Common { TransactionData parsedTransactionData = TransactionTransformer.fromBytes(bytes); - assertTrue("Transaction signature mismatch", Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature())); + assertTrue(Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature()), "Transaction signature mismatch"); - assertEquals("Data length mismatch", TransactionTransformer.getDataLength(transactionData), bytes.length); + assertEquals(bytes.length, TransactionTransformer.getDataLength(transactionData), "Data length mismatch"); } private void testSpecificBlockTransactions(int height, TransactionType type) throws DataException, TransformationException { try (final Repository repository = RepositoryManager.getRepository()) { BlockData blockData = repository.getBlockRepository().fromHeight(height); - assertNotNull("Block " + height + " is required for this test", blockData); + assertNotNull(blockData, "Block " + height + " is required for this test"); Block block = new Block(repository, blockData); diff --git a/src/test/SignatureTests.java b/src/test/SignatureTests.java index bffc7144..54aeafa4 100644 --- a/src/test/SignatureTests.java +++ b/src/test/SignatureTests.java @@ -1,11 +1,10 @@ package test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; -import org.junit.Test; - import data.block.BlockData; import qora.account.PrivateKeyAccount; import qora.block.Block; diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index f976c59f..3d491db0 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -1,6 +1,8 @@ package test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.AfterEach; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; @@ -10,8 +12,6 @@ import java.util.Arrays; import java.util.List; import org.json.simple.JSONObject; -import org.junit.After; -import org.junit.Test; import com.google.common.hash.HashCode; @@ -97,7 +97,7 @@ public class TransactionTests { RepositoryManager.setRepositoryFactory(repositoryFactory); try (final Repository repository = RepositoryManager.getRepository()) { - assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); + assertEquals(0, repository.getBlockRepository().getBlockchainHeight(), "Blockchain should be empty for this test"); } // [Un]set genesis timestamp as required by test @@ -136,7 +136,7 @@ public class TransactionTests { repository.saveChanges(); } - @After + @AfterEach public void closeRepository() throws DataException { RepositoryManager.closeRepositoryFactory(); } @@ -176,8 +176,8 @@ public class TransactionTests { block.addTransaction(paymentTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -185,21 +185,21 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(amount).subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); // Amount should be in recipient's balance expectedBalance = amount; actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); - assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect"); // Check recipient's reference byte[] recipientsReference = recipient.getLastReference(); - assertTrue("Recipient's new reference incorrect", Arrays.equals(paymentTransaction.getTransactionData().getSignature(), recipientsReference)); + assertTrue(Arrays.equals(paymentTransaction.getTransactionData().getSignature(), recipientsReference), "Recipient's new reference incorrect"); // Orphan block block.orphan(); @@ -207,11 +207,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0); + assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0); + assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); } @Test @@ -237,8 +237,8 @@ public class TransactionTests { block.addTransaction(registerNameTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -246,19 +246,19 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); // Check name was registered NameData actualNameData = this.repository.getNameRepository().fromName(name); assertNotNull(actualNameData); // Check sender's reference - assertTrue("Sender's new reference incorrect", Arrays.equals(registerNameTransactionData.getSignature(), sender.getLastReference())); + assertTrue(Arrays.equals(registerNameTransactionData.getSignature(), sender.getLastReference()), "Sender's new reference incorrect"); // Update variables for use by other tests reference = sender.getLastReference(); @@ -293,8 +293,8 @@ public class TransactionTests { block.addTransaction(updateNameTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -338,8 +338,8 @@ public class TransactionTests { block.addTransaction(sellNameTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -389,8 +389,8 @@ public class TransactionTests { block.addTransaction(cancelSellNameTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -455,8 +455,8 @@ public class TransactionTests { block.addTransaction(buyNameTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -508,8 +508,8 @@ public class TransactionTests { block.addTransaction(createPollTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -517,19 +517,19 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); // Check poll was created PollData actualPollData = this.repository.getVotingRepository().fromPollName(pollName); assertNotNull(actualPollData); // Check sender's reference - assertTrue("Sender's new reference incorrect", Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference())); + assertTrue(Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference()), "Sender's new reference incorrect"); // Update variables for use by other tests reference = sender.getLastReference(); @@ -567,8 +567,8 @@ public class TransactionTests { block.addTransaction(voteOnPollTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -588,10 +588,10 @@ public class TransactionTests { List votes = repository.getVotingRepository().getVotes(pollName); assertNotNull(votes); - assertEquals("Only one vote expected", 1, votes.size()); + assertEquals(1, votes.size(), "Only one vote expected"); - assertEquals("Wrong vote option index", pollOptionsSize - 1, votes.get(0).getOptionIndex()); - assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey())); + assertEquals(pollOptionsSize - 1, votes.get(0).getOptionIndex(), "Wrong vote option index"); + assertTrue(Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()), "Wrong voter public key"); // Orphan last block BlockData lastBlockData = repository.getBlockRepository().getLastBlock(); @@ -603,10 +603,10 @@ public class TransactionTests { votes = repository.getVotingRepository().getVotes(pollName); assertNotNull(votes); - assertEquals("Only one vote expected", 1, votes.size()); + assertEquals(1, votes.size(), "Only one vote expected"); - assertEquals("Wrong vote option index", pollOptionsSize - 1 - 1, votes.get(0).getOptionIndex()); - assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey())); + assertEquals(pollOptionsSize - 1 - 1, votes.get(0).getOptionIndex(), "Wrong vote option index"); + assertTrue(Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()), "Wrong voter public key"); } @Test @@ -634,8 +634,8 @@ public class TransactionTests { block.addTransaction(issueAssetTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -643,12 +643,12 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); // Check we now have an assetId Long assetId = issueAssetTransactionData.getAssetId(); @@ -672,11 +672,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0); + assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's reverted balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0); + assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's reverted balance incorrect"); // Check asset no longer exists assertFalse(assetRepo.assetExists(assetId)); @@ -724,8 +724,8 @@ public class TransactionTests { block.addTransaction(transferAssetTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -733,12 +733,12 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = originalSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); // Fee should be in generator's balance expectedBalance = originalGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); // Check asset balances BigDecimal actualSenderAssetBalance = sender.getConfirmedBalance(assetId); @@ -756,11 +756,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's reverted balance incorrect", originalSenderBalance.compareTo(actualBalance) == 0); + assertTrue(originalSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's reverted balance incorrect", originalGeneratorBalance.compareTo(actualBalance) == 0); + assertTrue(originalGeneratorBalance.compareTo(actualBalance) == 0, "Generator's reverted balance incorrect"); // Check asset balances actualSenderAssetBalance = sender.getConfirmedBalance(assetId); @@ -828,8 +828,8 @@ public class TransactionTests { block.addTransaction(createOrderTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -909,8 +909,8 @@ public class TransactionTests { block.addTransaction(cancelOrderTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -984,8 +984,8 @@ public class TransactionTests { block.addTransaction(createOrderTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -998,7 +998,7 @@ public class TransactionTests { // Check order has trades List trades = assetRepo.getOrdersTrades(orderId); assertNotNull(trades); - assertEquals("Trade didn't happen", 1, trades.size()); + assertEquals(1, trades.size(), "Trade didn't happen"); TradeData tradeData = trades.get(0); // Check trade has correct values @@ -1093,20 +1093,20 @@ public class TransactionTests { block.addTransaction(multiPaymentTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); // Check sender's balance BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedSenderBalance.compareTo(actualBalance) == 0); + assertTrue(expectedSenderBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); // Fee should be in generator's balance BigDecimal expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); // Check recipients for (int i = 0; i < payments.size(); ++i) { @@ -1114,12 +1114,12 @@ public class TransactionTests { Account recipient = new Account(this.repository, paymentData.getRecipient()); byte[] recipientsReference = recipient.getLastReference(); - assertTrue("Recipient's new reference incorrect", Arrays.equals(multiPaymentTransaction.getTransactionData().getSignature(), recipientsReference)); + assertTrue(Arrays.equals(multiPaymentTransaction.getTransactionData().getSignature(), recipientsReference), "Recipient's new reference incorrect"); // Amount should be in recipient's balance expectedBalance = paymentData.getAmount(); actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); - assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect"); } @@ -1129,11 +1129,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0); + assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0); + assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); } @Test @@ -1163,8 +1163,8 @@ public class TransactionTests { block.addTransaction(messageTransactionData); block.sign(); - assertTrue("Block signatures invalid", block.isSignatureValid()); - assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + assertTrue(block.isSignatureValid(), "Block signatures invalid"); + assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); block.process(); repository.saveChanges(); @@ -1172,17 +1172,17 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(amount).subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); // Amount should be in recipient's balance expectedBalance = amount; actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); - assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect"); } } \ No newline at end of file diff --git a/src/test/utils/AssertExtensions.java b/src/test/utils/AssertExtensions.java index 25ce1f55..bc45db0c 100644 --- a/src/test/utils/AssertExtensions.java +++ b/src/test/utils/AssertExtensions.java @@ -1,21 +1,54 @@ package test.utils; +import com.google.common.collect.Iterables; +import java.lang.reflect.Array; +import java.lang.Class; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; -import org.junit.Assert; + +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; +import static org.hamcrest.MatcherAssert.assertThat; public class AssertExtensions { + + public static void assertItemsEqual(Iterable expected, Iterable actual, EqualityComparer comparer) { + assertItemsEqual(expected, actual, comparer, (String)null); + } - public static void assertSetEquals(Iterable expected, Iterable actual, EqualityComparer comparer) { - Set> expectedSet = new HashSet>(); + public static void assertItemsEqual(Iterable expected, Iterable actual, EqualityComparer comparer, String message) { + List> expectedSet = new ArrayList>(); for(T item: expected) expectedSet.add(new EquatableWrapper(item, comparer)); - Set> actualSet = new HashSet>(); + List> actualSet = new ArrayList>(); for(T item: actual) actualSet.add(new EquatableWrapper(item, comparer)); - Assert.assertEquals(expectedSet, actualSet); + assertItemsEqual(expectedSet, actualSet, message); } + public static void assertItemsEqual(Iterable expected, Iterable actual) { + assertItemsEqual(expected, actual, (String)null); + } + + public static void assertItemsEqual(Iterable expected, Iterable actual, String message) { + List list = new ArrayList(); + T[] expectedArray = getArray(expected); + assertThat(message, actual, containsInAnyOrder(expectedArray)); + } + + private static T[] getArray(Iterable iterable) { + // XXX: What a horrific way to create an array from an iterable. + // Isn't there a better solution? + List list = new ArrayList(); + for(T item : iterable) + list.add(item); + @SuppressWarnings("unchecked") + T[] result = (T[])new Object[list.size()]; + for(int i = 0; i < list.size(); i++) + result[i] = list.get(i); + return result; + } } From af84cc8575b8eae9d2b45757eb8c006115d01f18 Mon Sep 17 00:00:00 2001 From: Kc Date: Wed, 10 Oct 2018 00:16:02 +0200 Subject: [PATCH 05/19] CHANGED: simplified AssertExtensions --- src/test/GlobalizationTests.java | 6 +----- src/test/utils/AssertExtensions.java | 24 ++++++------------------ 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/test/GlobalizationTests.java b/src/test/GlobalizationTests.java index d1e385de..967cba08 100644 --- a/src/test/GlobalizationTests.java +++ b/src/test/GlobalizationTests.java @@ -6,16 +6,12 @@ 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.jupiter.api.Test; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.*; import static test.utils.AssertExtensions.*; @@ -74,7 +70,7 @@ public class GlobalizationTests { " \n" + "\n"; - List expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/key1", "1")); expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path2/path3/key2", "2")); expected.add(new TranslationEntry(Locale.forLanguageTag("en-GB"), "/path1/path4/key3", "3")); diff --git a/src/test/utils/AssertExtensions.java b/src/test/utils/AssertExtensions.java index bc45db0c..fabce6a0 100644 --- a/src/test/utils/AssertExtensions.java +++ b/src/test/utils/AssertExtensions.java @@ -4,6 +4,7 @@ import com.google.common.collect.Iterables; import java.lang.reflect.Array; import java.lang.Class; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -13,11 +14,11 @@ import static org.hamcrest.MatcherAssert.assertThat; public class AssertExtensions { - public static void assertItemsEqual(Iterable expected, Iterable actual, EqualityComparer comparer) { + public static void assertItemsEqual(Collection expected, Iterable actual, EqualityComparer comparer) { assertItemsEqual(expected, actual, comparer, (String)null); } - public static void assertItemsEqual(Iterable expected, Iterable actual, EqualityComparer comparer, String message) { + public static void assertItemsEqual(Collection expected, Iterable actual, EqualityComparer comparer, String message) { List> expectedSet = new ArrayList>(); for(T item: expected) expectedSet.add(new EquatableWrapper(item, comparer)); @@ -29,26 +30,13 @@ public class AssertExtensions { assertItemsEqual(expectedSet, actualSet, message); } - public static void assertItemsEqual(Iterable expected, Iterable actual) { + public static void assertItemsEqual(Collection expected, Iterable actual) { assertItemsEqual(expected, actual, (String)null); } - public static void assertItemsEqual(Iterable expected, Iterable actual, String message) { + public static void assertItemsEqual(Collection expected, Iterable actual, String message) { List list = new ArrayList(); - T[] expectedArray = getArray(expected); + T[] expectedArray = (T[])expected.toArray(); assertThat(message, actual, containsInAnyOrder(expectedArray)); } - - private static T[] getArray(Iterable iterable) { - // XXX: What a horrific way to create an array from an iterable. - // Isn't there a better solution? - List list = new ArrayList(); - for(T item : iterable) - list.add(item); - @SuppressWarnings("unchecked") - T[] result = (T[])new Object[list.size()]; - for(int i = 0; i < list.size(); i++) - result[i] = list.get(i); - return result; - } } From b57881bc505d784bac30bf43cb38cd8c605586ec Mon Sep 17 00:00:00 2001 From: Kc Date: Thu, 11 Oct 2018 08:56:25 +0200 Subject: [PATCH 06/19] ADDED: post processor stub for API resource annotations --- src/api/AnnotationPostProcessor.java | 20 ++++++++++++++++++++ src/api/ApiService.java | 3 ++- src/api/BlocksResource.java | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/api/AnnotationPostProcessor.java diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java new file mode 100644 index 00000000..0de6cb3a --- /dev/null +++ b/src/api/AnnotationPostProcessor.java @@ -0,0 +1,20 @@ + +package api; + +import io.swagger.v3.jaxrs2.Reader; +import io.swagger.v3.jaxrs2.ReaderListener; +import io.swagger.v3.oas.models.OpenAPI; + +public class AnnotationPostProcessor implements ReaderListener { + + @Override + public void beforeScan(Reader reader, OpenAPI openAPI) {} + + @Override + public void afterScan(Reader reader, OpenAPI openAPI) { + // TODO: use context path and keys from "x-translation" extension annotations + // to translate "descriptions" and finally remove "x-translation" extensions + // from output. + } + +} diff --git a/src/api/ApiService.java b/src/api/ApiService.java index afa236ec..9e3c5012 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -23,8 +23,9 @@ public class ApiService { this.resources = new HashSet>(); this.resources.add(BlocksResource.class); this.resources.add(OpenApiResource.class); // swagger + this.resources.add(AnnotationPostProcessor.class); // for API resource annotations ResourceConfig config = new ResourceConfig(this.resources); - + // create RPC server this.server = new Server(Settings.getInstance().getRpcPort()); diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index de4a30e9..37c2d2a8 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -1,7 +1,10 @@ package api; import globalization.Translator; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -19,6 +22,11 @@ import repository.RepositoryManager; @Path("blocks") @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) +@OpenAPIDefinition( + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="/BlocksResource"), + }) +) public class BlocksResource { @Context @@ -37,6 +45,10 @@ public class BlocksResource { @GET @Operation( description = "Returns an array of the 50 last blocks generated by your accounts", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="getBlocks"), + @ExtensionProperty(name="key", value="description") + }), responses = { @ApiResponse( description = "The blocks" @@ -45,6 +57,9 @@ public class BlocksResource { @ApiResponse( responseCode = "422", description = "Error: 201 - Wallet does not exist", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="key", value="ApiError/201") + }), content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) ) } From 2d0ced5a72d3e207b0c5f33b4e619b97f0dd79a5 Mon Sep 17 00:00:00 2001 From: Kc Date: Fri, 12 Oct 2018 08:21:44 +0200 Subject: [PATCH 07/19] CHANGED: finished AnnotationPostProcessor for translating swagger annotations CHANGED: fixed Translator bug that would ignore all translation templates --- globalization/{en-GB.xml => english.xml} | 4 +- src/api/AnnotationPostProcessor.java | 182 ++++++++++++++++++++++- src/api/BlocksResource.java | 6 +- src/globalization/Translator.java | 1 + 4 files changed, 186 insertions(+), 7 deletions(-) rename globalization/{en-GB.xml => english.xml} (86%) diff --git a/globalization/en-GB.xml b/globalization/english.xml similarity index 86% rename from globalization/en-GB.xml rename to globalization/english.xml index 581fe4ce..7637b7db 100644 --- a/globalization/en-GB.xml +++ b/globalization/english.xml @@ -1,12 +1,14 @@ - + + + diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java index 0de6cb3a..b72cfe49 100644 --- a/src/api/AnnotationPostProcessor.java +++ b/src/api/AnnotationPostProcessor.java @@ -1,20 +1,196 @@ package api; +import globalization.ContextPaths; +import globalization.Translator; import io.swagger.v3.jaxrs2.Reader; import io.swagger.v3.jaxrs2.ReaderListener; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.responses.ApiResponse; +import static java.util.Arrays.asList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; public class AnnotationPostProcessor implements ReaderListener { + private interface TranslatableProperty { + public String keyName(); + public void setValue(T item, String translation); + public String getValue(T item); + } + + private class ContextInformation { + public String path; + public Map keys; + } + + private static final String TRANSLATION_EXTENTION_NAME = "x-translation"; + + private static final List> translatableInfoProperties = asList( + new TranslatableProperty() { + @Override public String keyName() { return "description.key"; } + @Override public void setValue(Info item, String translation) { item.setDescription(translation); } + @Override public String getValue(Info item) { return item.getDescription(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return "title.key"; } + @Override public void setValue(Info item, String translation) { item.setTitle(translation); } + @Override public String getValue(Info item) { return item.getTitle(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return "termsOfService.key"; } + @Override public void setValue(Info item, String translation) { item.setTermsOfService(translation); } + @Override public String getValue(Info item) { return item.getTermsOfService(); } + } + ); + + private static final List> translatablePathItemProperties = asList( + new TranslatableProperty() { + @Override public String keyName() { return "description.key"; } + @Override public void setValue(PathItem item, String translation) { item.setDescription(translation); } + @Override public String getValue(PathItem item) { return item.getDescription(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return "summary.key"; } + @Override public void setValue(PathItem item, String translation) { item.setSummary(translation); } + @Override public String getValue(PathItem item) { return item.getSummary(); } + } + ); + + private static final List> translatableOperationProperties = asList( + new TranslatableProperty() { + @Override public String keyName() { return "description.key"; } + @Override public void setValue(Operation item, String translation) { item.setDescription(translation); } + @Override public String getValue(Operation item) { return item.getDescription(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return "summary.key"; } + @Override public void setValue(Operation item, String translation) { item.setSummary(translation); } + @Override public String getValue(Operation item) { return item.getSummary(); } + } + ); + + private static final List> translatableApiResponseProperties = asList( + new TranslatableProperty() { + @Override public String keyName() { return "description.key"; } + @Override public void setValue(ApiResponse item, String translation) { item.setDescription(translation); } + @Override public String getValue(ApiResponse item) { return item.getDescription(); } + } + ); + + private final Translator translator; + + public AnnotationPostProcessor() { + this(Translator.getInstance()); + } + + public AnnotationPostProcessor(Translator translator) { + this.translator = translator; + + + } + @Override public void beforeScan(Reader reader, OpenAPI openAPI) {} @Override public void afterScan(Reader reader, OpenAPI openAPI) { - // TODO: use context path and keys from "x-translation" extension annotations - // to translate "descriptions" and finally remove "x-translation" extensions - // from output. + // use context path and keys from "x-translation" extension annotations + // to translate supported annotations and finally remove "x-translation" extensions + Info resourceInfo = openAPI.getInfo(); + ContextInformation resourceContext = getContextInformation(openAPI.getExtensions()); + removeTranslationAnnotations(openAPI.getExtensions()); + TranslateProperty(translatableInfoProperties, resourceContext, resourceInfo); + + for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) + { + PathItem pathItem = pathEntry.getValue(); + ContextInformation pathContext = getContextInformation(pathItem.getExtensions(), resourceContext); + removeTranslationAnnotations(pathItem.getExtensions()); + TranslateProperty(translatablePathItemProperties, pathContext, pathItem); + + for (Operation operation : pathItem.readOperations()) { + ContextInformation operationContext = getContextInformation(operation.getExtensions(), pathContext); + removeTranslationAnnotations(operation.getExtensions()); + TranslateProperty(translatableOperationProperties, operationContext, operation); + + for (Map.Entry responseEntry : operation.getResponses().entrySet()) { + ApiResponse response = responseEntry.getValue(); + ContextInformation responseContext = getContextInformation(response.getExtensions(), operationContext); + removeTranslationAnnotations(response.getExtensions()); + TranslateProperty(translatableApiResponseProperties, responseContext, response); + } + } + } + } + + private void TranslateProperty(List> translatableProperties, ContextInformation context, T item) { + if(context.keys != null) { + Map keys = context.keys; + for(TranslatableProperty prop : translatableProperties) { + String key = keys.get(prop.keyName()); + if(key != null) { + String originalValue = prop.getValue(item); + String translation = translator.translate(Locale.ENGLISH, context.path, key, originalValue); + prop.setValue(item, translation); + } + } + } + } + + private ContextInformation getContextInformation(Map extensions) { + return getContextInformation(extensions, null); } + private ContextInformation getContextInformation(Map extensions, ContextInformation base) { + if(extensions != null) { + Map translationDefinitions = (Map)extensions.get(TRANSLATION_EXTENTION_NAME); + if(translationDefinitions != null) { + ContextInformation result = new ContextInformation(); + result.path = getAbsolutePath(base, (String)translationDefinitions.get("path")); + result.keys = getTranslationKeys(translationDefinitions); + return result; + } + } + + if(base != null) { + ContextInformation result = new ContextInformation(); + result.path = base.path; + return result; + } + + return null; + } + + private void removeTranslationAnnotations(Map extensions) { + if(extensions == null) + return; + + extensions.remove(TRANSLATION_EXTENTION_NAME); + } + + private Map getTranslationKeys(Map translationDefinitions) { + Map result = new HashMap<>(); + + for(TranslatableProperty prop : translatableInfoProperties) { + String key = (String)translationDefinitions.get(prop.keyName()); + if(key != null) + result.put(prop.keyName(), key); + } + + return result; + } + + private String getAbsolutePath(ContextInformation base, String path) { + String result = (base != null) ? base.path : "/"; + path = (path != null) ? path : ""; + result = ContextPaths.combinePaths(result, path); + return result; + } } diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index 37c2d2a8..b9110212 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -24,7 +24,7 @@ import repository.RepositoryManager; @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @OpenAPIDefinition( extensions = @Extension(name = "translation", properties = { - @ExtensionProperty(name="path", value="/BlocksResource"), + @ExtensionProperty(name="path", value="/Api/BlocksResource") }) ) public class BlocksResource { @@ -47,7 +47,7 @@ public class BlocksResource { description = "Returns an array of the 50 last blocks generated by your accounts", extensions = @Extension(name = "translation", properties = { @ExtensionProperty(name="path", value="getBlocks"), - @ExtensionProperty(name="key", value="description") + @ExtensionProperty(name="description.key", value="description") }), responses = { @ApiResponse( @@ -58,7 +58,7 @@ public class BlocksResource { responseCode = "422", description = "Error: 201 - Wallet does not exist", extensions = @Extension(name = "translation", properties = { - @ExtensionProperty(name="key", value="ApiError/201") + @ExtensionProperty(name="description.key", value="ApiError/201") }), content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) ) diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java index fa9a17f2..91d4bb6b 100644 --- a/src/globalization/Translator.java +++ b/src/globalization/Translator.java @@ -73,6 +73,7 @@ public class Translator { Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, String.format("Duplicate entry for locale '%s' and path '%s' in translation file '%s'. Falling back to default translations.", entry.locale(), entry.path(), file)); return; } + localTranslations.put(entry.path(), entry.template()); } } From 2eb808a0b7e1c10c177d3950fadf5176ee3c5cf4 Mon Sep 17 00:00:00 2001 From: Kc Date: Fri, 12 Oct 2018 08:27:49 +0200 Subject: [PATCH 08/19] CHANGED: added comment CHANGED: default locale --- src/api/AnnotationPostProcessor.java | 1 + src/settings/Settings.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java index b72cfe49..51ffcc27 100644 --- a/src/api/AnnotationPostProcessor.java +++ b/src/api/AnnotationPostProcessor.java @@ -137,6 +137,7 @@ public class AnnotationPostProcessor implements ReaderListener { String key = keys.get(prop.keyName()); if(key != null) { String originalValue = prop.getValue(item); + // XXX: use configurable or browser locale instead english? String translation = translator.translate(Locale.ENGLISH, context.path, key, originalValue); prop.setValue(item, translation); } diff --git a/src/settings/Settings.java b/src/settings/Settings.java index 46df2c29..090f4a21 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -31,7 +31,7 @@ public class Settings { // Globalization private String translationsPath = "globalization/"; - private String[] translationsDefaultLocales = {"en-GB"}; + private String[] translationsDefaultLocales = {"en"}; // Constants private static final String SETTINGS_FILENAME = "settings.json"; From 9a3eb186ccfefa605eef3574859bacd5c72a47b7 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 14 Oct 2018 20:35:49 +0200 Subject: [PATCH 09/19] CHANGED: translation support for API resources --- globalization/Api.de.xml | 104 ++++++ globalization/Api.en.xml | 108 ++++++ globalization/BlocksResource.de.xml | 60 +++ globalization/BlocksResource.en.xml | 60 +++ globalization/english.xml | 23 -- src/api/AnnotationPostProcessor.java | 93 +---- src/api/ApiClient.java | 113 +++++- src/api/ApiError.java | 1 + src/api/ApiErrorFactory.java | 7 +- src/api/BlocksResource.java | 352 ++++++++++++++---- src/api/Constants.java | 74 ++++ src/api/TranslatableProperty.java | 7 + src/globalization/ContextPaths.java | 3 +- .../TranslationXmlStreamReader.java | 7 +- src/globalization/Translator.java | 47 ++- 15 files changed, 860 insertions(+), 199 deletions(-) create mode 100644 globalization/Api.de.xml create mode 100644 globalization/Api.en.xml create mode 100644 globalization/BlocksResource.de.xml create mode 100644 globalization/BlocksResource.en.xml delete mode 100644 globalization/english.xml create mode 100644 src/api/Constants.java create mode 100644 src/api/TranslatableProperty.java diff --git a/globalization/Api.de.xml b/globalization/Api.de.xml new file mode 100644 index 00000000..b188fa51 --- /dev/null +++ b/globalization/Api.de.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/globalization/Api.en.xml b/globalization/Api.en.xml new file mode 100644 index 00000000..3becbd6e --- /dev/null +++ b/globalization/Api.en.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/globalization/BlocksResource.de.xml b/globalization/BlocksResource.de.xml new file mode 100644 index 00000000..4d3c8ab8 --- /dev/null +++ b/globalization/BlocksResource.de.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/globalization/BlocksResource.en.xml b/globalization/BlocksResource.en.xml new file mode 100644 index 00000000..2d39c70e --- /dev/null +++ b/globalization/BlocksResource.en.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/globalization/english.xml b/globalization/english.xml deleted file mode 100644 index 7637b7db..00000000 --- a/globalization/english.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java index 51ffcc27..94b66b30 100644 --- a/src/api/AnnotationPostProcessor.java +++ b/src/api/AnnotationPostProcessor.java @@ -8,82 +8,19 @@ import io.swagger.v3.jaxrs2.ReaderListener; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.Paths; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.responses.ApiResponse; -import static java.util.Arrays.asList; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; public class AnnotationPostProcessor implements ReaderListener { - private interface TranslatableProperty { - public String keyName(); - public void setValue(T item, String translation); - public String getValue(T item); - } - private class ContextInformation { public String path; public Map keys; } - private static final String TRANSLATION_EXTENTION_NAME = "x-translation"; - - private static final List> translatableInfoProperties = asList( - new TranslatableProperty() { - @Override public String keyName() { return "description.key"; } - @Override public void setValue(Info item, String translation) { item.setDescription(translation); } - @Override public String getValue(Info item) { return item.getDescription(); } - }, - new TranslatableProperty() { - @Override public String keyName() { return "title.key"; } - @Override public void setValue(Info item, String translation) { item.setTitle(translation); } - @Override public String getValue(Info item) { return item.getTitle(); } - }, - new TranslatableProperty() { - @Override public String keyName() { return "termsOfService.key"; } - @Override public void setValue(Info item, String translation) { item.setTermsOfService(translation); } - @Override public String getValue(Info item) { return item.getTermsOfService(); } - } - ); - - private static final List> translatablePathItemProperties = asList( - new TranslatableProperty() { - @Override public String keyName() { return "description.key"; } - @Override public void setValue(PathItem item, String translation) { item.setDescription(translation); } - @Override public String getValue(PathItem item) { return item.getDescription(); } - }, - new TranslatableProperty() { - @Override public String keyName() { return "summary.key"; } - @Override public void setValue(PathItem item, String translation) { item.setSummary(translation); } - @Override public String getValue(PathItem item) { return item.getSummary(); } - } - ); - - private static final List> translatableOperationProperties = asList( - new TranslatableProperty() { - @Override public String keyName() { return "description.key"; } - @Override public void setValue(Operation item, String translation) { item.setDescription(translation); } - @Override public String getValue(Operation item) { return item.getDescription(); } - }, - new TranslatableProperty() { - @Override public String keyName() { return "summary.key"; } - @Override public void setValue(Operation item, String translation) { item.setSummary(translation); } - @Override public String getValue(Operation item) { return item.getSummary(); } - } - ); - - private static final List> translatableApiResponseProperties = asList( - new TranslatableProperty() { - @Override public String keyName() { return "description.key"; } - @Override public void setValue(ApiResponse item, String translation) { item.setDescription(translation); } - @Override public String getValue(ApiResponse item) { return item.getDescription(); } - } - ); - private final Translator translator; public AnnotationPostProcessor() { @@ -92,8 +29,6 @@ public class AnnotationPostProcessor implements ReaderListener { public AnnotationPostProcessor(Translator translator) { this.translator = translator; - - } @Override @@ -106,25 +41,25 @@ public class AnnotationPostProcessor implements ReaderListener { Info resourceInfo = openAPI.getInfo(); ContextInformation resourceContext = getContextInformation(openAPI.getExtensions()); removeTranslationAnnotations(openAPI.getExtensions()); - TranslateProperty(translatableInfoProperties, resourceContext, resourceInfo); + TranslateProperty(Constants.TRANSLATABLE_INFO_PROPERTIES, resourceContext, resourceInfo); for (Map.Entry pathEntry : openAPI.getPaths().entrySet()) { PathItem pathItem = pathEntry.getValue(); ContextInformation pathContext = getContextInformation(pathItem.getExtensions(), resourceContext); removeTranslationAnnotations(pathItem.getExtensions()); - TranslateProperty(translatablePathItemProperties, pathContext, pathItem); + TranslateProperty(Constants.TRANSLATABLE_PATH_ITEM_PROPERTIES, pathContext, pathItem); for (Operation operation : pathItem.readOperations()) { ContextInformation operationContext = getContextInformation(operation.getExtensions(), pathContext); removeTranslationAnnotations(operation.getExtensions()); - TranslateProperty(translatableOperationProperties, operationContext, operation); + TranslateProperty(Constants.TRANSLATABLE_OPERATION_PROPERTIES, operationContext, operation); for (Map.Entry responseEntry : operation.getResponses().entrySet()) { ApiResponse response = responseEntry.getValue(); ContextInformation responseContext = getContextInformation(response.getExtensions(), operationContext); removeTranslationAnnotations(response.getExtensions()); - TranslateProperty(translatableApiResponseProperties, responseContext, response); + TranslateProperty(Constants.TRANSLATABLE_API_RESPONSE_PROPERTIES, responseContext, response); } } } @@ -137,8 +72,8 @@ public class AnnotationPostProcessor implements ReaderListener { String key = keys.get(prop.keyName()); if(key != null) { String originalValue = prop.getValue(item); - // XXX: use configurable or browser locale instead english? - String translation = translator.translate(Locale.ENGLISH, context.path, key, originalValue); + // XXX: use browser locale instead default? + String translation = translator.translate(context.path, key, originalValue); prop.setValue(item, translation); } } @@ -151,10 +86,10 @@ public class AnnotationPostProcessor implements ReaderListener { private ContextInformation getContextInformation(Map extensions, ContextInformation base) { if(extensions != null) { - Map translationDefinitions = (Map)extensions.get(TRANSLATION_EXTENTION_NAME); + Map translationDefinitions = (Map)extensions.get("x-" + Constants.TRANSLATION_EXTENSION_NAME); if(translationDefinitions != null) { ContextInformation result = new ContextInformation(); - result.path = getAbsolutePath(base, (String)translationDefinitions.get("path")); + result.path = combinePaths(base, (String)translationDefinitions.get(Constants.TRANSLATION_PATH_EXTENSION_NAME)); result.keys = getTranslationKeys(translationDefinitions); return result; } @@ -173,13 +108,13 @@ public class AnnotationPostProcessor implements ReaderListener { if(extensions == null) return; - extensions.remove(TRANSLATION_EXTENTION_NAME); + extensions.remove("x-" + Constants.TRANSLATION_EXTENSION_NAME); } private Map getTranslationKeys(Map translationDefinitions) { Map result = new HashMap<>(); - for(TranslatableProperty prop : translatableInfoProperties) { + for(TranslatableProperty prop : Constants.TRANSLATABLE_INFO_PROPERTIES) { String key = (String)translationDefinitions.get(prop.keyName()); if(key != null) result.put(prop.keyName(), key); @@ -188,10 +123,8 @@ public class AnnotationPostProcessor implements ReaderListener { return result; } - private String getAbsolutePath(ContextInformation base, String path) { - String result = (base != null) ? base.path : "/"; - path = (path != null) ? path : ""; - result = ContextPaths.combinePaths(result, path); - return result; + private String combinePaths(ContextInformation base, String path) { + String basePath = (base != null) ? base.path : null; + return ContextPaths.combinePaths(basePath, path); } } diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java index d43a63f2..67f1f859 100644 --- a/src/api/ApiClient.java +++ b/src/api/ApiClient.java @@ -1,16 +1,20 @@ package api; +import globalization.ContextPaths; import globalization.Translator; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.extensions.Extension; +import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.ws.rs.Path; @@ -49,6 +53,8 @@ public class ApiClient { } } + private static final String TRANSLATION_CONTEXT_PATH = "/Api/ApiClient"; + private static final Pattern COMMAND_PATTERN = Pattern.compile("^ *(?GET|POST|PUT|PATCH|DELETE) *(?.*)$"); private static final Pattern HELP_COMMAND_PATTERN = Pattern.compile("^ *help *(?.*)$", Pattern.CASE_INSENSITIVE); private static final List> HTTP_METHOD_ANNOTATIONS = Arrays.asList( @@ -59,8 +65,8 @@ public class ApiClient { DELETE.class ); + private final Translator translator; ApiService apiService; - private Translator translator; List helpInfos; public ApiClient(ApiService apiService, Translator translator) { @@ -94,18 +100,28 @@ public class ApiClient { if (resourcePath == null) { continue; } - String resourcePathString = resourcePath.value(); + // get translation context path for resource + String resourceContextPath = "/"; + OpenAPIDefinition openAPIDefinition = resource.getDeclaredAnnotation(OpenAPIDefinition.class); + if(openAPIDefinition != null) + resourceContextPath = getContextPath(openAPIDefinition.extensions()); + // scan each method for (Method method : resource.getDeclaredMethods()) { Operation operationAnnotation = method.getAnnotation(Operation.class); - if (operationAnnotation == null) { + if (operationAnnotation == null) continue; - } String description = operationAnnotation.description(); + // translate + String operationContextPath = ContextPaths.combinePaths(resourceContextPath, getContextPath(operationAnnotation.extensions())); + String operationDescriptionKey = getDescriptionTranslationKey(operationAnnotation.extensions()); + if(operationDescriptionKey != null) + description = translator.translate(operationContextPath, operationDescriptionKey, description); + // extract responses ArrayList success = new ArrayList(); ArrayList errors = new ArrayList(); @@ -114,6 +130,20 @@ public class ApiClient { if(StringUtils.isBlank(responseDescription)) continue; // ignore responses without description + // translate + String responseContextPath = ContextPaths.combinePaths(operationContextPath, getContextPath(response.extensions())); + String responseDescriptionKey = getDescriptionTranslationKey(response.extensions()); + if(responseDescriptionKey != null) + responseDescription = translator.translate(responseContextPath, responseDescriptionKey, responseDescription); + + String apiErrorCode = getApiErrorCode(response.extensions()); + if(apiErrorCode != null) { + responseDescription = translator.translate(TRANSLATION_CONTEXT_PATH, "API error response", "(API error: ${ERROR_CODE}) ${DESCRIPTION}", + new AbstractMap.SimpleEntry<>("ERROR_CODE", apiErrorCode), + new AbstractMap.SimpleEntry<>("DESCRIPTION", responseDescription) + ); + } + try { // try to identify response type by status code int responseCode = Integer.parseInt(response.responseCode()); @@ -164,6 +194,50 @@ public class ApiClient { return result; } + + private String getApiErrorCode(Extension[] extensions) { + if(extensions == null) + return null; + + for(Extension extension : extensions) { + if(extension.name() != null && !extension.name().isEmpty()) + continue; + + for(ExtensionProperty prop : extension.properties()) { + if(Constants.API_ERROR_CODE_EXTENSION_NAME.equals(prop.name())) { + return prop.value(); + } + } + } + + return null; + } + + private String getContextPath(Extension[] extensions) { + return getTranslationExtensionValue(extensions, Constants.TRANSLATION_PATH_EXTENSION_NAME); + } + + private String getDescriptionTranslationKey(Extension[] extensions) { + return getTranslationExtensionValue(extensions, Constants.TRANSLATION_ANNOTATION_DESCRIPTION_KEY); + } + + private String getTranslationExtensionValue(Extension[] extensions, String key) { + if(extensions == null) + return null; + + for(Extension extension : extensions) { + if(!Constants.TRANSLATION_EXTENSION_NAME.equals(extension.name())) + continue; + + for(ExtensionProperty prop : extension.properties()) { + if(key.equals(prop.name())) { + return prop.value(); + } + } + } + + return null; + } private String getHelpPatternForPath(String path) { path = path @@ -205,7 +279,7 @@ public class ApiClient { match = COMMAND_PATTERN.matcher(command); if(!match.matches()) - return this.translator.translate(Locale.getDefault(), "ApiClient: INVALID_COMMAND", "Invalid command! \nType help to get a list of commands."); + return this.translator.translate(TRANSLATION_CONTEXT_PATH, "invalid command", "Invalid command! \nType 'help all' to get a list of commands."); // send the command to the API service String method = match.group("method"); @@ -225,11 +299,22 @@ public class ApiClient { if(status >= 400) { result.append("HTTP Status "); result.append(status); - if(!StringUtils.isBlank(body)) { - result.append(": "); - result.append(body); + if(StringUtils.isBlank(body)) { + result.append( + this.translator.translate(TRANSLATION_CONTEXT_PATH, "error without body", "HTTP Status ${STATUS}", + new AbstractMap.SimpleEntry<>("STATUS", status) + ) + ); + }else{ + result.append( + this.translator.translate(TRANSLATION_CONTEXT_PATH, "error with body", "HTTP Status ${STATUS}: ${BODY}", + new AbstractMap.SimpleEntry<>("STATUS", status), + new AbstractMap.SimpleEntry<>("BODY", body) + ) + ); } - result.append("\nType help to get a list of commands."); + result.append("\n"); + result.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "error footer", "Type 'help all' to get a list of commands.")); } else { result.append(body); } @@ -240,13 +325,17 @@ public class ApiClient { builder.append(help.fullPath + "\n"); builder.append(" " + help.description + "\n"); if(help.success != null && help.success.size() > 0) { - builder.append(" On success returns:\n"); + builder.append(" "); + builder.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "help: success responses", "On success returns:")); + builder.append("\n"); for(String content : help.success) { builder.append(" " + content + "\n"); } } if(help.errors != null && help.errors.size() > 0) { - builder.append(" On failure returns:\n"); + builder.append(" "); + builder.append(this.translator.translate(TRANSLATION_CONTEXT_PATH, "help: failure responses", "On failure returns:")); + builder.append("\n"); for(String content : help.errors) { builder.append(" " + content + "\n"); } diff --git a/src/api/ApiError.java b/src/api/ApiError.java index 471fa9f2..d1e8b69d 100644 --- a/src/api/ApiError.java +++ b/src/api/ApiError.java @@ -117,4 +117,5 @@ public enum ApiError { int getStatus() { return this.status; } + } \ No newline at end of file diff --git a/src/api/ApiErrorFactory.java b/src/api/ApiErrorFactory.java index 784a3476..07b88e04 100644 --- a/src/api/ApiErrorFactory.java +++ b/src/api/ApiErrorFactory.java @@ -101,16 +101,16 @@ public class ApiErrorFactory { // this.errorMessages.put(ApiError.NAME_FOR_SALE, createErrorMessageEntry(ApiError.NAME_FOR_SALE, NameResult.NAME_FOR_SALE.getStatusMessage())); // this.errorMessages.put(ApiError.NAME_WITH_SPACE, createErrorMessageEntry(ApiError.NAME_WITH_SPACE, NameResult.NAME_WITH_SPACE.getStatusMessage())); //AT - this.errorMessages.put(ApiError.INVALID_CREATION_BYTES, createErrorMessageEntry(ApiError.INVALID_CREATION_BYTES, "error in creation bytes")); // TODO // this.errorMessages.put(ApiError.INVALID_DESC_LENGTH, createErrorMessageEntry(ApiError.INVALID_DESC_LENGTH, // "invalid description length. max length ${MAX_LENGTH}", // new AbstractMap.SimpleEntry("MAX_LENGTH", AT_Constants.DESC_MAX_LENGTH)); this.errorMessages.put(ApiError.EMPTY_CODE, createErrorMessageEntry(ApiError.EMPTY_CODE, "code is empty")); this.errorMessages.put(ApiError.DATA_SIZE, createErrorMessageEntry(ApiError.DATA_SIZE, "invalid data length")); + this.errorMessages.put(ApiError.NULL_PAGES, createErrorMessageEntry(ApiError.NULL_PAGES, "invalid pages")); this.errorMessages.put(ApiError.INVALID_TYPE_LENGTH, createErrorMessageEntry(ApiError.INVALID_TYPE_LENGTH, "invalid type length")); this.errorMessages.put(ApiError.INVALID_TAGS_LENGTH, createErrorMessageEntry(ApiError.INVALID_TAGS_LENGTH, "invalid tags length")); - this.errorMessages.put(ApiError.NULL_PAGES, createErrorMessageEntry(ApiError.NULL_PAGES, "invalid pages")); + this.errorMessages.put(ApiError.INVALID_CREATION_BYTES, createErrorMessageEntry(ApiError.INVALID_CREATION_BYTES, "error in creation bytes")); //BLOG this.errorMessages.put(ApiError.BODY_EMPTY, createErrorMessageEntry(ApiError.BODY_EMPTY, "invalid body it must not be empty")); @@ -166,8 +166,7 @@ public class ApiErrorFactory { } public ApiException createError(ApiError error, Throwable throwable) { - Locale locale = Locale.ENGLISH; // default locale - return createError(locale, error, throwable); + return createError(null, error, throwable); } public ApiException createError(Locale locale, ApiError error, Throwable throwable) { diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index b9110212..5367b999 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -44,23 +44,33 @@ public class BlocksResource { @GET @Operation( - description = "Returns an array of the 50 last blocks generated by your accounts", + description = "returns an array of the 50 last blocks generated by your accounts", extensions = @Extension(name = "translation", properties = { - @ExtensionProperty(name="path", value="getBlocks"), - @ExtensionProperty(name="description.key", value="description") + @ExtensionProperty(name="path", value="GET"), + @ExtensionProperty(name="description.key", value="operation:description") }), responses = { @ApiResponse( - description = "The blocks" + description = "the blocks", //content = @Content(schema = @Schema(implementation = ???)) + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ), @ApiResponse( responseCode = "422", - description = "Error: 201 - Wallet does not exist", - extensions = @Extension(name = "translation", properties = { - @ExtensionProperty(name="description.key", value="ApiError/201") - }), - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "wallet does not exist", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="201") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/201") + }) + } ) } ) @@ -73,26 +83,59 @@ public class BlocksResource { @GET @Path("/address/{address}") @Operation( - description = "Returns an array of the 50 last blocks generated by a specific address in your wallet", + description = "returns an array of the 50 last blocks generated by a specific address in your wallet", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET address:address"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The blocks" - //content = @Content(schema = @Schema(implementation = ???)) + description = "the blocks", + //content = @Content(schema = @Schema(implementation = ???)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ), @ApiResponse( responseCode = "400", - description = "102 - Invalid address", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "invalid address", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="102") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/102") + }) + } ), @ApiResponse( responseCode = "422", - description = "201 - Wallet does not exist", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "wallet does not exist", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="201") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/201") + }) + } ), @ApiResponse( responseCode = "422", - description = "202 - Address does not exist in wallet", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "address does not exist in wallet", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="202") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/202") + }) + } ) } ) @@ -105,21 +148,46 @@ public class BlocksResource { @GET @Path("/{signature}") @Operation( - description = "Returns the block that matches the given signature", + description = "returns the block that matches the given signature", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) + description = "the block", + //content = @Content(schema = @Schema(implementation = ???)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ), @ApiResponse( responseCode = "400", - description = "101 - Invalid signature", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "invalid signature", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="101") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/101") + }) + } ), @ApiResponse( responseCode = "422", - description = "301 - Block does not exist", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "block does not exist", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="301") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/301") + }) + } ) } ) @@ -132,11 +200,20 @@ public class BlocksResource { @GET @Path("/first") @Operation( - description = "Returns the genesis block", + description = "returns the genesis block", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET first"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) + description = "the block", + //content = @Content(schema = @Schema(implementation = ???)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ) } ) @@ -149,11 +226,20 @@ public class BlocksResource { @GET @Path("/last") @Operation( - description = "Returns the last valid block", + description = "returns the last valid block", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET last"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) + description = "the block", + //content = @Content(schema = @Schema(implementation = ???)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ) } ) @@ -166,21 +252,46 @@ public class BlocksResource { @GET @Path("/child/{signature}") @Operation( - description = "Returns the child block of the block that matches the given signature", + description = "returns the child block of the block that matches the given signature", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET child:signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) + description = "the block", + //content = @Content(schema = @Schema(implementation = ???)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ), @ApiResponse( responseCode = "400", - description = "101 - Invalid signature", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "invalid signature", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="101") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/101") + }) + } ), @ApiResponse( responseCode = "422", - description = "301 - Block does not exist", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "block does not exist", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="301") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/301") + }) + } ) } ) @@ -193,11 +304,20 @@ public class BlocksResource { @GET @Path("/generatingbalance") @Operation( - description = "Calculates the generating balance of the block that will follow the last block", + description = "calculates the generating balance of the block that will follow the last block", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET generatingbalance"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The generating balance", - content = @Content(schema = @Schema(implementation = long.class)) + description = "the generating balance", + content = @Content(schema = @Schema(implementation = long.class)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ) } ) @@ -210,21 +330,46 @@ public class BlocksResource { @GET @Path("/generatingbalance/{signature}") @Operation( - description = "Calculates the generating balance of the block that will follow the block that matches the signature", + description = "calculates the generating balance of the block that will follow the block that matches the signature", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET generatingbalance:signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The block", - content = @Content(schema = @Schema(implementation = long.class)) + description = "the block", + content = @Content(schema = @Schema(implementation = long.class)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ), @ApiResponse( responseCode = "400", - description = "101 - Invalid signature", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "invalid signature", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="101") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/101") + }) + } ), @ApiResponse( responseCode = "422", - description = "301 - Block does not exist", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "block does not exist", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="301") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/301") + }) + } ) } ) @@ -237,11 +382,20 @@ public class BlocksResource { @GET @Path("/time") @Operation( - description = "Calculates the time it should take for the network to generate the next block", + description = "calculates the time it should take for the network to generate the next block", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET time"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The time", // in seconds? - content = @Content(schema = @Schema(implementation = long.class)) + description = "the time", // in seconds? + content = @Content(schema = @Schema(implementation = long.class)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ) } ) @@ -254,11 +408,20 @@ public class BlocksResource { @GET @Path("/time/{generatingbalance}") @Operation( - description = "Calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance", + description = "calculates the time it should take for the network to generate blocks when the current generating balance in the network is the specified generating balance", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET time:generatingbalance"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The time", // in seconds? - content = @Content(schema = @Schema(implementation = long.class)) + description = "the time", // in seconds? + content = @Content(schema = @Schema(implementation = long.class)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ) } ) @@ -271,11 +434,20 @@ public class BlocksResource { @GET @Path("/height") @Operation( - description = "Returns the block height of the last block.", + description = "returns the block height of the last block.", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET height"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The height", - content = @Content(schema = @Schema(implementation = int.class)) + description = "the height", + content = @Content(schema = @Schema(implementation = int.class)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ) } ) @@ -292,21 +464,46 @@ public class BlocksResource { @GET @Path("/height/{signature}") @Operation( - description = "Returns the block height of the block that matches the given signature", + description = "returns the block height of the block that matches the given signature", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET height:signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The height", - content = @Content(schema = @Schema(implementation = int.class)) + description = "the height", + content = @Content(schema = @Schema(implementation = int.class)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ), @ApiResponse( responseCode = "400", - description = "101 - Invalid signature", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "invalid signature", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="101") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/101") + }) + } ), @ApiResponse( responseCode = "422", - description = "301 - Block does not exist", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "block does not exist", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="301") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/301") + }) + } ) } ) @@ -319,16 +516,33 @@ public class BlocksResource { @GET @Path("/byheight/{height}") @Operation( - description = "Returns the block whith given height", + description = "returns the block whith given height", + extensions = @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET byheight:height"), + @ExtensionProperty(name="description.key", value="operation:description") + }), responses = { @ApiResponse( - description = "The block" - //content = @Content(schema = @Schema(implementation = ???)) + description = "the block", + //content = @Content(schema = @Schema(implementation = ???)), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="success_response:description") + }) + } ), @ApiResponse( responseCode = "422", - description = "301 - Block does not exist", - content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)) + description = "block does not exist", + content = @Content(schema = @Schema(implementation = ApiErrorMessage.class)), + extensions = { + @Extension(properties = { + @ExtensionProperty(name="apiErrorCode", value="301") + }), + @Extension(name = "translation", properties = { + @ExtensionProperty(name="description.key", value="ApiError/301") + }) + } ) } ) diff --git a/src/api/Constants.java b/src/api/Constants.java new file mode 100644 index 00000000..b2d4708b --- /dev/null +++ b/src/api/Constants.java @@ -0,0 +1,74 @@ +package api; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.responses.ApiResponse; +import static java.util.Arrays.asList; +import java.util.List; + +class Constants { + + public static final String TRANSLATION_EXTENSION_NAME = "translation"; + public static final String TRANSLATION_PATH_EXTENSION_NAME = "path"; + + public static final String TRANSLATION_ANNOTATION_DESCRIPTION_KEY = "description.key"; + public static final String TRANSLATION_ANNOTATION_SUMMARY_KEY = "summary.key"; + public static final String TRANSLATION_ANNOTATION_TITLE_KEY = "title.key"; + public static final String TRANSLATION_ANNOTATION_TERMS_OF_SERVICE_KEY = "termsOfService.key"; + + public static final String API_ERROR_CODE_EXTENSION_NAME = "apiErrorCode"; + + + public static final List> TRANSLATABLE_INFO_PROPERTIES = asList( + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; } + @Override public void setValue(Info item, String translation) { item.setDescription(translation); } + @Override public String getValue(Info item) { return item.getDescription(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_TITLE_KEY; } + @Override public void setValue(Info item, String translation) { item.setTitle(translation); } + @Override public String getValue(Info item) { return item.getTitle(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_TERMS_OF_SERVICE_KEY; } + @Override public void setValue(Info item, String translation) { item.setTermsOfService(translation); } + @Override public String getValue(Info item) { return item.getTermsOfService(); } + } + ); + + public static final List> TRANSLATABLE_PATH_ITEM_PROPERTIES = asList( + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; } + @Override public void setValue(PathItem item, String translation) { item.setDescription(translation); } + @Override public String getValue(PathItem item) { return item.getDescription(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_SUMMARY_KEY; } + @Override public void setValue(PathItem item, String translation) { item.setSummary(translation); } + @Override public String getValue(PathItem item) { return item.getSummary(); } + } + ); + + public static final List> TRANSLATABLE_OPERATION_PROPERTIES = asList( + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; } + @Override public void setValue(Operation item, String translation) { item.setDescription(translation); } + @Override public String getValue(Operation item) { return item.getDescription(); } + }, + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_SUMMARY_KEY; } + @Override public void setValue(Operation item, String translation) { item.setSummary(translation); } + @Override public String getValue(Operation item) { return item.getSummary(); } + } + ); + + public static final List> TRANSLATABLE_API_RESPONSE_PROPERTIES = asList( + new TranslatableProperty() { + @Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; } + @Override public void setValue(ApiResponse item, String translation) { item.setDescription(translation); } + @Override public String getValue(ApiResponse item) { return item.getDescription(); } + } + ); +} diff --git a/src/api/TranslatableProperty.java b/src/api/TranslatableProperty.java new file mode 100644 index 00000000..8dd90e9a --- /dev/null +++ b/src/api/TranslatableProperty.java @@ -0,0 +1,7 @@ +package api; + +interface TranslatableProperty { + public String keyName(); + public void setValue(T item, String translation); + public String getValue(T item); +} diff --git a/src/globalization/ContextPaths.java b/src/globalization/ContextPaths.java index e924b25d..8743d438 100644 --- a/src/globalization/ContextPaths.java +++ b/src/globalization/ContextPaths.java @@ -1,7 +1,6 @@ package globalization; import java.nio.file.Paths; -import javax.xml.stream.XMLStreamException; public class ContextPaths { @@ -18,6 +17,8 @@ public class ContextPaths { } public static String combinePaths(String left, String right) { + left = (left != null) ? left : ""; + right = (right != null) ? right : ""; return Paths.get("/", left, right).normalize().toString(); } diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java index db4f76be..790df7f3 100644 --- a/src/globalization/TranslationXmlStreamReader.java +++ b/src/globalization/TranslationXmlStreamReader.java @@ -18,6 +18,7 @@ import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.*; +import org.apache.commons.text.StringEscapeUtils; public class TranslationXmlStreamReader { @@ -183,7 +184,7 @@ public class TranslationXmlStreamReader { path = ContextPaths.combinePaths(state.path, value); break; case TRANSLATION_TEMPLATE_ATTRIBUTE_NAME: - template = value; + template = unescape(value); break; default: throw new javax.xml.stream.XMLStreamException("Unexpected attribute: " + name); @@ -211,6 +212,10 @@ public class TranslationXmlStreamReader { result.add(new TranslationEntry(state.locale, path, template)); } + private String unescape(String value) { + return StringEscapeUtils.unescapeJava(value); + } + private void assureIsValidPathExtension(String value) throws XMLStreamException { if(ContextPaths.containsParentReference(value)) throw new javax.xml.stream.XMLStreamException("Parent reference .. is not allowed"); diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java index 91d4bb6b..d3d73747 100644 --- a/src/globalization/Translator.java +++ b/src/globalization/Translator.java @@ -94,32 +94,61 @@ public class Translator { return translate(locale, contextPath, templateKey, map); } + public String translate(String contextPath, String templateKey, AbstractMap.Entry... templateValues) { + Map map = createMap(templateValues); + return translate(contextPath, templateKey, map); + } + public String translate(Locale locale, String contextPath, String templateKey, Map templateValues) { return translate(locale, contextPath, templateKey, null, templateValues); } + public String translate(String contextPath, String templateKey, Map templateValues) { + return translate(contextPath, templateKey, null, templateValues); + } + public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { Map map = createMap(templateValues); return translate(locale, contextPath, templateKey, defaultTemplate, map); } + public String translate(String contextPath, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { + Map map = createMap(templateValues); + return translate(contextPath, templateKey, defaultTemplate, map); + } + public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, Map templateValues) { // look for requested language - String template = getTemplateFromNearestPath(locale, contextPath, templateKey); + String template = null; + if(locale != null) + template = getTemplateFromNearestPath(locale, contextPath, templateKey); - if(template == null) { - // scan default languages - for(String language : this.settings().translationsDefaultLocales()) { - Locale defaultLocale = Locale.forLanguageTag(language); - template = getTemplateFromNearestPath(defaultLocale, contextPath, templateKey); - if(template != null) - break; - } + if(template != null) + return substitute(template, templateValues); + + return translate(contextPath, templateKey, defaultTemplate, templateValues); + } + + public String translate(String contextPath, String templateKey, String defaultTemplate, Map templateValues) { + // scan default languages + String template = null; + for(String language : this.settings().translationsDefaultLocales()) { + Locale defaultLocale = Locale.forLanguageTag(language); + template = getTemplateFromNearestPath(defaultLocale, contextPath, templateKey); + if(template != null) + break; } if(template == null) template = defaultTemplate; // fallback template + + return substitute(template, templateValues); + } + private String substitute(String template, Map templateValues) { + if(templateValues == null) + return template; + StringSubstitutor sub = new StringSubstitutor(templateValues); String result = sub.replace(template); From 74a49baaf3c9ab29eea4e5d3a3a6e4d7e6f1e823 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 14 Oct 2018 20:49:32 +0200 Subject: [PATCH 10/19] CHANGED: removed obsolete comment --- src/api/ApiClient.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java index 67f1f859..6f14522d 100644 --- a/src/api/ApiClient.java +++ b/src/api/ApiClient.java @@ -89,8 +89,6 @@ public class ApiClient { private List getHelpInfos(Iterable> resources) { List result = new ArrayList<>(); - // TODO: need some way to realize translation from resource annotations - // scan each resource class for (Class resource : resources) { if (OpenApiResource.class.isAssignableFrom(resource)) { From aff81c2806b7bcba1984ea144c9844fc11e33505 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 14 Oct 2018 22:00:27 +0200 Subject: [PATCH 11/19] CHANGED: removed double output --- src/api/ApiClient.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/api/ApiClient.java b/src/api/ApiClient.java index 6f14522d..9421a23b 100644 --- a/src/api/ApiClient.java +++ b/src/api/ApiClient.java @@ -295,8 +295,6 @@ public class ApiClient { final int status = response.getStatus(); StringBuilder result = new StringBuilder(); if(status >= 400) { - result.append("HTTP Status "); - result.append(status); if(StringUtils.isBlank(body)) { result.append( this.translator.translate(TRANSLATION_CONTEXT_PATH, "error without body", "HTTP Status ${STATUS}", From 23b8fcc96e76280626b6c3404f2191965fc95839 Mon Sep 17 00:00:00 2001 From: Kc Date: Mon, 15 Oct 2018 15:11:22 +0200 Subject: [PATCH 12/19] CHANGED: implemented more BlocksResource methods CHANGED: added dependency to javax.mail for because of strange "java.lang.NoClassDefFoundError: javax/mail/internet/MimeMultipart" exception when serializing data objects in API resources. --- pom.xml | 10 +++ src/Start.java | 4 +- src/api/BlocksResource.java | 136 ++++++++++++++++++++++++++++++---- src/data/block/BlockData.java | 5 +- 4 files changed, 138 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 53655871..1a0f6740 100644 --- a/pom.xml +++ b/pom.xml @@ -128,5 +128,15 @@ hamcrest-library 1.3 + + org.glassfish.jersey.media + jersey-media-multipart + 2.27 + + + javax.mail + mail + 1.5.0-b01 + \ No newline at end of file diff --git a/src/Start.java b/src/Start.java index 585f65c6..cbab3830 100644 --- a/src/Start.java +++ b/src/Start.java @@ -8,7 +8,7 @@ import repository.hsqldb.HSQLDBRepositoryFactory; public class Start { - private static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true"; + private static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true"; public static void main(String args[]) throws DataException { RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); @@ -19,7 +19,7 @@ public class Start { //// testing the API client //ApiClient client = ApiClient.getInstance(); - //String test = client.executeCommand("GET blocks/height"); + //String test = client.executeCommand("GET blocks/first"); //System.out.println(test); } } diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index 5367b999..74db659c 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -1,5 +1,6 @@ package api; +import data.block.BlockData; import globalization.Translator; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.Operation; @@ -19,6 +20,7 @@ import javax.ws.rs.core.MediaType; import repository.Repository; import repository.RepositoryManager; +import utils.Base58; @Path("blocks") @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @@ -156,7 +158,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - //content = @Content(schema = @Schema(implementation = ???)), + content = @Content(schema = @Schema(implementation = BlockData.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -191,10 +193,32 @@ public class BlocksResource { ) } ) - public String getBlock(@PathParam("signature") String signature) { + public BlockData getBlock(@PathParam("signature") String signature) { Security.checkApiCallAllowed("GET blocks", request); - throw new UnsupportedOperationException(); + // decode signature + byte[] signatureBytes; + try + { + signatureBytes = Base58.decode(signature); + } + catch(Exception e) + { + throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); + + // check if block exists + if(blockData == null) + throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + + return blockData; + + } catch (Exception e) { + throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); + } } @GET @@ -208,7 +232,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - //content = @Content(schema = @Schema(implementation = ???)), + content = @Content(schema = @Schema(implementation = BlockData.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -217,10 +241,21 @@ public class BlocksResource { ) } ) - public String getFirstBlock() { + public BlockData getFirstBlock() { Security.checkApiCallAllowed("GET blocks/first", request); - throw new UnsupportedOperationException(); + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromHeight(1); + + // check if block exists + if(blockData == null) + throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + + return blockData; + + } catch (Exception e) { + throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); + } } @GET @@ -234,7 +269,7 @@ public class BlocksResource { responses = { @ApiResponse( description = "the block", - //content = @Content(schema = @Schema(implementation = ???)), + content = @Content(schema = @Schema(implementation = BlockData.class)), extensions = { @Extension(name = "translation", properties = { @ExtensionProperty(name="description.key", value="success_response:description") @@ -243,10 +278,21 @@ public class BlocksResource { ) } ) - public String getLastBlock() { + public BlockData getLastBlock() { Security.checkApiCallAllowed("GET blocks/last", request); - throw new UnsupportedOperationException(); + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().getLastBlock(); + + // check if block exists + if(blockData == null) + throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + + return blockData; + + } catch (Exception e) { + throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); + } } @GET @@ -295,10 +341,39 @@ public class BlocksResource { ) } ) - public String getChild(@PathParam("signature") String signature) { + public BlockData getChild(@PathParam("signature") String signature) { Security.checkApiCallAllowed("GET blocks/child", request); - throw new UnsupportedOperationException(); + // decode signature + byte[] signatureBytes; + try + { + signatureBytes = Base58.decode(signature); + } + catch(Exception e) + { + throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); + + // check if block exists + if(blockData == null) + throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + + int height = blockData.getHeight(); + BlockData childBlockData = repository.getBlockRepository().fromHeight(height + 1); + + // check if child exists + if(childBlockData == null) + throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + + return childBlockData; + + } catch (Exception e) { + throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); + } } @GET @@ -510,7 +585,29 @@ public class BlocksResource { public int getHeight(@PathParam("signature") String signature) { Security.checkApiCallAllowed("GET blocks/height", request); - throw new UnsupportedOperationException(); + // decode signature + byte[] signatureBytes; + try + { + signatureBytes = Base58.decode(signature); + } + catch(Exception e) + { + throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes); + + // check if block exists + if(blockData == null) + throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + + return blockData.getHeight(); + + } catch (Exception e) { + throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); + } } @GET @@ -546,9 +643,20 @@ public class BlocksResource { ) } ) - public String getbyHeight(@PathParam("height") int height) { + public BlockData getbyHeight(@PathParam("height") int height) { Security.checkApiCallAllowed("GET blocks/byheight", request); - throw new UnsupportedOperationException(); + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().fromHeight(height); + + // check if block exists + if(blockData == null) + throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS); + + return blockData; + + } catch (Exception e) { + throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e); + } } } diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java index 8c49b71b..ec98c594 100644 --- a/src/data/block/BlockData.java +++ b/src/data/block/BlockData.java @@ -3,8 +3,9 @@ package data.block; import java.math.BigDecimal; import com.google.common.primitives.Bytes; +import java.io.Serializable; -public class BlockData { +public class BlockData implements Serializable { private byte[] signature; private int version; @@ -20,6 +21,8 @@ public class BlockData { private byte[] atBytes; private BigDecimal atFees; + private BlockData() {} // necessary for JAX-RS serialization + public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, int height, long timestamp, BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, byte[] atBytes, BigDecimal atFees) { this.version = version; From a07570588502845ea36cb0d16706c9a5832b5197 Mon Sep 17 00:00:00 2001 From: Kc Date: Thu, 18 Oct 2018 20:29:07 +0200 Subject: [PATCH 13/19] ADDED: Swagger UI for API documentation --- pom.xml | 101 ++++++++++++++++++++++++++++++++++++---- src/api/ApiService.java | 22 ++++++++- 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 1a0f6740..b20f47ad 100644 --- a/pom.xml +++ b/pom.xml @@ -4,6 +4,9 @@ org.qora qora-core 2.0.0-SNAPSHOT + + 3.19.0 + src @@ -15,6 +18,74 @@ 1.8 + + + maven-dependency-plugin + + + swagger ui + generate-resources + + unpack + + + + + org.webjars + swagger-ui + ${swagger-ui.version} + + + ${project.build.directory}/swagger-ui.unpacked + + + + + + + com.google.code.maven-replacer-plugin + replacer + 1.5.3 + + + generate-resources + + replace + + + + + ${project.build.directory}/swagger-ui.unpacked/META-INF/resources/webjars/swagger-ui/${swagger-ui.version}/index.html + + + https://petstore.swagger.io/v2/swagger.json + /openapi.json + + + + + + + maven-resources-plugin + 3.1.0 + + + copy-resources + generate-resources + + copy-resources + + + ${project.build.directory}/classes/resources/swagger-ui + + + ${project.build.directory}/swagger-ui.unpacked/META-INF/resources/webjars/swagger-ui/${swagger-ui.version} + + + + + + @@ -128,15 +199,25 @@ hamcrest-library 1.3 - - org.glassfish.jersey.media - jersey-media-multipart - 2.27 - - - javax.mail - mail - 1.5.0-b01 - + + org.glassfish.jersey.media + jersey-media-multipart + 2.27 + + + javax.mail + mail + 1.5.0-b01 + + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.eclipse.jetty + jetty-rewrite + 9.4.11.v20180605 + \ No newline at end of file diff --git a/src/api/ApiService.java b/src/api/ApiService.java index 9e3c5012..aa2b2726 100644 --- a/src/api/ApiService.java +++ b/src/api/ApiService.java @@ -1,11 +1,15 @@ package api; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; +import java.io.File; import java.util.HashSet; import java.util.Set; +import org.eclipse.jetty.rewrite.handler.RedirectPatternRule; +import org.eclipse.jetty.rewrite.handler.RewriteHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.InetAccessHandler; +import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.glassfish.jersey.server.ResourceConfig; @@ -36,16 +40,30 @@ public class ApiService { } this.server.setHandler(accessHandler); + // url rewriting + RewriteHandler rewriteHandler = new RewriteHandler(); + accessHandler.setHandler(rewriteHandler); + // context ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS); context.setContextPath("/"); - accessHandler.setHandler(context); - + rewriteHandler.setHandler(context); + // API servlet ServletContainer container = new ServletContainer(config); ServletHolder apiServlet = new ServletHolder(container); apiServlet.setInitOrder(1); context.addServlet(apiServlet, "/*"); + + // Swagger-UI static content + ClassLoader loader = this.getClass().getClassLoader(); + File swaggerUIResourceLocation = new File(loader.getResource("resources/swagger-ui/").getFile()); + ServletHolder swaggerUIServlet = new ServletHolder("static-swagger-ui", DefaultServlet.class); + swaggerUIServlet.setInitParameter("resourceBase", swaggerUIResourceLocation.getAbsolutePath()); + swaggerUIServlet.setInitParameter("dirAllowed","true"); + swaggerUIServlet.setInitParameter("pathInfoOnly","true"); + context.addServlet(swaggerUIServlet,"/api-documentation/*"); + rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/index.html")); // redirect to swagger ui start page } //XXX: replace singleton pattern by dependency injection? From d2aab4b446cc4dfb13cbda17732a516991bd57dd Mon Sep 17 00:00:00 2001 From: Kc Date: Fri, 19 Oct 2018 12:35:15 +0200 Subject: [PATCH 14/19] CHANGED: removed obsolete API methods from BlocksResource CHANGED: added missing response schemas --- globalization/BlocksResource.de.xml | 10 +-- globalization/BlocksResource.en.xml | 10 +-- src/api/BlocksResource.java | 107 +--------------------------- 3 files changed, 8 insertions(+), 119 deletions(-) diff --git a/globalization/BlocksResource.de.xml b/globalization/BlocksResource.de.xml index 4d3c8ab8..b9610a74 100644 --- a/globalization/BlocksResource.de.xml +++ b/globalization/BlocksResource.de.xml @@ -4,13 +4,9 @@ - - - - -