From 9a3eb186ccfefa605eef3574859bacd5c72a47b7 Mon Sep 17 00:00:00 2001 From: Kc Date: Sun, 14 Oct 2018 20:35:49 +0200 Subject: [PATCH] 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);