From 6590863201b59ee5e29af29e7ec199cf50b940b2 Mon Sep 17 00:00:00 2001 From: Kc Date: Sat, 20 Oct 2018 01:29:20 +0200 Subject: [PATCH] CHANGED: simplified API error annotations in API resources FIXED: ApiErrorFactory used no context path and wrong translation key CHANGED: renamed parameters in Translator for consistency --- src/api/AnnotationPostProcessor.java | 107 +++++++++- src/api/ApiError.java | 9 + src/api/ApiErrorFactory.java | 8 +- src/api/BlocksResource.java | 182 +++++------------- src/api/Constants.java | 6 +- .../TranslationXmlStreamReader.java | 2 +- src/globalization/Translator.java | 38 ++-- 7 files changed, 183 insertions(+), 169 deletions(-) diff --git a/src/api/AnnotationPostProcessor.java b/src/api/AnnotationPostProcessor.java index 94b66b30..224c7a8e 100644 --- a/src/api/AnnotationPostProcessor.java +++ b/src/api/AnnotationPostProcessor.java @@ -1,19 +1,27 @@ package api; +import com.fasterxml.jackson.databind.node.ArrayNode; import globalization.ContextPaths; import globalization.Translator; +import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.jaxrs2.Reader; import io.swagger.v3.jaxrs2.ReaderListener; +import io.swagger.v3.oas.models.media.Content; 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.examples.Example; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.responses.ApiResponse; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; + public class AnnotationPostProcessor implements ReaderListener { private class ContextInformation { @@ -22,13 +30,15 @@ public class AnnotationPostProcessor implements ReaderListener { } private final Translator translator; + private final ApiErrorFactory apiErrorFactory; public AnnotationPostProcessor() { - this(Translator.getInstance()); + this(Translator.getInstance(), ApiErrorFactory.getInstance()); } - public AnnotationPostProcessor(Translator translator) { + public AnnotationPostProcessor(Translator translator, ApiErrorFactory apiErrorFactory) { this.translator = translator; + this.apiErrorFactory = apiErrorFactory; } @Override @@ -41,31 +51,60 @@ public class AnnotationPostProcessor implements ReaderListener { Info resourceInfo = openAPI.getInfo(); ContextInformation resourceContext = getContextInformation(openAPI.getExtensions()); removeTranslationAnnotations(openAPI.getExtensions()); - TranslateProperty(Constants.TRANSLATABLE_INFO_PROPERTIES, resourceContext, resourceInfo); + TranslateProperties(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(Constants.TRANSLATABLE_PATH_ITEM_PROPERTIES, pathContext, pathItem); + TranslateProperties(Constants.TRANSLATABLE_PATH_ITEM_PROPERTIES, pathContext, pathItem); for (Operation operation : pathItem.readOperations()) { ContextInformation operationContext = getContextInformation(operation.getExtensions(), pathContext); removeTranslationAnnotations(operation.getExtensions()); - TranslateProperty(Constants.TRANSLATABLE_OPERATION_PROPERTIES, operationContext, operation); + TranslateProperties(Constants.TRANSLATABLE_OPERATION_PROPERTIES, operationContext, operation); + addApiErrorResponses(operation); + removeApiErrorsAnnotations(operation.getExtensions()); + for (Map.Entry responseEntry : operation.getResponses().entrySet()) { ApiResponse response = responseEntry.getValue(); ContextInformation responseContext = getContextInformation(response.getExtensions(), operationContext); removeTranslationAnnotations(response.getExtensions()); - TranslateProperty(Constants.TRANSLATABLE_API_RESPONSE_PROPERTIES, responseContext, response); + TranslateProperties(Constants.TRANSLATABLE_API_RESPONSE_PROPERTIES, responseContext, response); } } } } - private void TranslateProperty(List> translatableProperties, ContextInformation context, T item) { + private void addApiErrorResponses(Operation operation) { + List apiErrors = getApiErrors(operation.getExtensions()); + if(apiErrors != null) { + for(ApiError apiError : apiErrors) { + String statusCode = Integer.toString(apiError.getStatus()); + ApiResponse apiResponse = operation.getResponses().get(statusCode); + if(apiResponse == null) { + Schema errorMessageSchema = ModelConverters.getInstance().readAllAsResolvedSchema(ApiErrorMessage.class).schema; + MediaType mediaType = new MediaType().schema(errorMessageSchema); + Content content = new Content().addMediaType(javax.ws.rs.core.MediaType.APPLICATION_JSON, mediaType); + apiResponse = new ApiResponse().content(content); + operation.getResponses().addApiResponse(statusCode, apiResponse); + } + + int apiErrorCode = apiError.getCode(); + ApiErrorMessage apiErrorMessage = new ApiErrorMessage(apiErrorCode, this.apiErrorFactory.getErrorMessage(apiError)); + Example example = new Example().value(apiErrorMessage); + + // XXX: addExamples(..) is not working in Swagger 2.0.4. This bug is referenced in https://github.com/swagger-api/swagger-ui/issues/2651 + // Replace the call to .setExample(..) by .addExamples(..) when the bug is fixed. + apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).setExample(example); + //apiResponse.getContent().get(javax.ws.rs.core.MediaType.APPLICATION_JSON).addExamples(Integer.toString(apiErrorCode), example); + } + } + } + + private void TranslateProperties(List> translatableProperties, ContextInformation context, T item) { if(context.keys != null) { Map keys = context.keys; for(TranslatableProperty prop : translatableProperties) { @@ -80,6 +119,48 @@ public class AnnotationPostProcessor implements ReaderListener { } } + private List getApiErrors(Map extensions) { + if(extensions == null) + return null; + + List apiErrorStrings = new ArrayList(); + try { + ArrayNode apiErrorsNode = (ArrayNode)extensions.get("x-" + Constants.API_ERRORS_EXTENSION_NAME); + if(apiErrorsNode == null) + return null; + + for(int i = 0; i < apiErrorsNode.size(); i++) { + String errorString = apiErrorsNode.get(i).asText(); + apiErrorStrings.add(errorString); + } + } catch(Exception e) { + // TODO: error logging + return null; + } + + List result = new ArrayList<>(); + for(String apiErrorString : apiErrorStrings) { + ApiError apiError = null; + try { + apiError = ApiError.valueOf(apiErrorString); + } catch(IllegalArgumentException e) { + try { + int errorCodeInt = Integer.parseInt(apiErrorString); + apiError = ApiError.fromCode(errorCodeInt); + } catch (NumberFormatException ex) { + return null; + } + } + + if(apiError == null) + return null; + + result.add(apiError); + } + + return result; + } + private ContextInformation getContextInformation(Map extensions) { return getContextInformation(extensions, null); } @@ -104,11 +185,21 @@ public class AnnotationPostProcessor implements ReaderListener { return null; } + private void removeApiErrorsAnnotations(Map extensions) { + String extensionName = Constants.API_ERRORS_EXTENSION_NAME; + removeExtension(extensions, extensionName); + } + private void removeTranslationAnnotations(Map extensions) { + String extensionName = Constants.TRANSLATION_EXTENSION_NAME; + removeExtension(extensions, extensionName); + } + + private void removeExtension(Map extensions, String extensionName) { if(extensions == null) return; - extensions.remove("x-" + Constants.TRANSLATION_EXTENSION_NAME); + extensions.remove("x-" + extensionName); } private Map getTranslationKeys(Map translationDefinitions) { diff --git a/src/api/ApiError.java b/src/api/ApiError.java index d1e8b69d..3291fbdc 100644 --- a/src/api/ApiError.java +++ b/src/api/ApiError.java @@ -110,6 +110,15 @@ public enum ApiError { this.status = status; } + public static ApiError fromCode(int code) { + for(ApiError apiError : ApiError.values()) { + if(apiError.code == code) + return apiError; + } + + return null; + } + int getCode() { return this.code; } diff --git a/src/api/ApiErrorFactory.java b/src/api/ApiErrorFactory.java index 07b88e04..32b1e40f 100644 --- a/src/api/ApiErrorFactory.java +++ b/src/api/ApiErrorFactory.java @@ -147,13 +147,17 @@ public class ApiErrorFactory { } private ErrorMessageEntry createErrorMessageEntry(ApiError errorCode, String defaultTemplate, AbstractMap.SimpleEntry... templateValues) { - String templateKey = String.format("%s: ApiError.%s message", ApiErrorFactory.class.getSimpleName(), errorCode.name()); + String templateKey = String.format(Constants.APIERROR_KEY, errorCode.name()); return new ErrorMessageEntry(templateKey, defaultTemplate, templateValues); } + + public String getErrorMessage(ApiError error) { + return getErrorMessage(null, error); + } public String getErrorMessage(Locale locale, ApiError error) { ErrorMessageEntry errorMessage = this.errorMessages.get(error); - String message = this.translator.translate(locale, errorMessage.templateKey, errorMessage.defaultTemplate, errorMessage.templateValues); + String message = this.translator.translate(locale, Constants.APIERROR_CONTEXT_PATH, errorMessage.templateKey, errorMessage.defaultTemplate, errorMessage.templateValues); return message; } diff --git a/src/api/BlocksResource.java b/src/api/BlocksResource.java index 6bfd85b6..72648535 100644 --- a/src/api/BlocksResource.java +++ b/src/api/BlocksResource.java @@ -48,10 +48,15 @@ public class BlocksResource { @Path("/{signature}") @Operation( 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") - }), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), + @Extension(properties = { + @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true), + }) + }, responses = { @ApiResponse( description = "the block", @@ -61,32 +66,6 @@ public class BlocksResource { @ExtensionProperty(name="description.key", value="success_response:description") }) } - ), - @ApiResponse( - responseCode = "400", - 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 = "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") - }) - } ) } ) @@ -196,10 +175,15 @@ public class BlocksResource { @Path("/child/{signature}") @Operation( 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") - }), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET child:signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), + @Extension(properties = { + @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true), + }) + }, responses = { @ApiResponse( description = "the block", @@ -209,32 +193,6 @@ public class BlocksResource { @ExtensionProperty(name="description.key", value="success_response:description") }) } - ), - @ApiResponse( - responseCode = "400", - 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 = "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") - }) - } ) } ) @@ -303,10 +261,15 @@ public class BlocksResource { @Path("/generatingbalance/{signature}") @Operation( 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") - }), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET generatingbalance:signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), + @Extension(properties = { + @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true), + }) + }, responses = { @ApiResponse( description = "the block", @@ -316,32 +279,6 @@ public class BlocksResource { @ExtensionProperty(name="description.key", value="success_response:description") }) } - ), - @ApiResponse( - responseCode = "400", - 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 = "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") - }) - } ) } ) @@ -437,10 +374,15 @@ public class BlocksResource { @Path("/height/{signature}") @Operation( 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") - }), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET height:signature"), + @ExtensionProperty(name="description.key", value="operation:description") + }), + @Extension(properties = { + @ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true), + }) + }, responses = { @ApiResponse( description = "the height", @@ -450,32 +392,6 @@ public class BlocksResource { @ExtensionProperty(name="description.key", value="success_response:description") }) } - ), - @ApiResponse( - responseCode = "400", - 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 = "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") - }) - } ) } ) @@ -511,10 +427,15 @@ public class BlocksResource { @Path("/byheight/{height}") @Operation( 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") - }), + extensions = { + @Extension(name = "translation", properties = { + @ExtensionProperty(name="path", value="GET byheight:height"), + @ExtensionProperty(name="description.key", value="operation:description") + }), + @Extension(properties = { + @ExtensionProperty(name="apiErrors", value="[\"BLOCK_NO_EXISTS\"]", parseValue = true), + }) + }, responses = { @ApiResponse( description = "the block", @@ -524,19 +445,6 @@ public class BlocksResource { @ExtensionProperty(name="description.key", value="success_response:description") }) } - ), - @ApiResponse( - responseCode = "422", - 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 index b2d4708b..60886dfd 100644 --- a/src/api/Constants.java +++ b/src/api/Constants.java @@ -8,7 +8,9 @@ import static java.util.Arrays.asList; import java.util.List; class Constants { - + public static final String APIERROR_CONTEXT_PATH = "/Api"; + public static final String APIERROR_KEY = "ApiError/%s"; + public static final String TRANSLATION_EXTENSION_NAME = "translation"; public static final String TRANSLATION_PATH_EXTENSION_NAME = "path"; @@ -17,8 +19,8 @@ class Constants { 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_ERRORS_EXTENSION_NAME = "apiErrors"; public static final String API_ERROR_CODE_EXTENSION_NAME = "apiErrorCode"; - public static final List> TRANSLATABLE_INFO_PROPERTIES = asList( new TranslatableProperty() { diff --git a/src/globalization/TranslationXmlStreamReader.java b/src/globalization/TranslationXmlStreamReader.java index 790df7f3..1712a9f6 100644 --- a/src/globalization/TranslationXmlStreamReader.java +++ b/src/globalization/TranslationXmlStreamReader.java @@ -53,7 +53,7 @@ public class TranslationXmlStreamReader { State state = new State(Locale.forLanguageTag("default"), "/"); - List result = new ArrayList(); + List result = new ArrayList<>(); if (eventReader.hasNext()) { XMLEvent event = eventReader.nextTag(); diff --git a/src/globalization/Translator.java b/src/globalization/Translator.java index d3d73747..c6c4569d 100644 --- a/src/globalization/Translator.java +++ b/src/globalization/Translator.java @@ -89,52 +89,52 @@ public class Translator { return map; } - public String translate(Locale locale, String contextPath, String templateKey, AbstractMap.Entry... templateValues) { + public String translate(Locale locale, String contextPath, String keyPath, AbstractMap.Entry... templateValues) { Map map = createMap(templateValues); - return translate(locale, contextPath, templateKey, map); + return translate(locale, contextPath, keyPath, map); } - public String translate(String contextPath, String templateKey, AbstractMap.Entry... templateValues) { + public String translate(String contextPath, String keyPath, AbstractMap.Entry... templateValues) { Map map = createMap(templateValues); - return translate(contextPath, templateKey, map); + return translate(contextPath, keyPath, map); } - 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 keyPath, Map templateValues) { + return translate(locale, contextPath, keyPath, null, templateValues); } - public String translate(String contextPath, String templateKey, Map templateValues) { - return translate(contextPath, templateKey, null, templateValues); + public String translate(String contextPath, String keyPath, Map templateValues) { + return translate(contextPath, keyPath, null, templateValues); } - public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { + public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry... templateValues) { Map map = createMap(templateValues); - return translate(locale, contextPath, templateKey, defaultTemplate, map); + return translate(locale, contextPath, keyPath, defaultTemplate, map); } - public String translate(String contextPath, String templateKey, String defaultTemplate, AbstractMap.Entry... templateValues) { + public String translate(String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry... templateValues) { Map map = createMap(templateValues); - return translate(contextPath, templateKey, defaultTemplate, map); + return translate(contextPath, keyPath, defaultTemplate, map); } - public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, Map templateValues) { + public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, Map templateValues) { // look for requested language String template = null; if(locale != null) - template = getTemplateFromNearestPath(locale, contextPath, templateKey); + template = getTemplateFromNearestPath(locale, contextPath, keyPath); if(template != null) return substitute(template, templateValues); - return translate(contextPath, templateKey, defaultTemplate, templateValues); + return translate(contextPath, keyPath, defaultTemplate, templateValues); } - public String translate(String contextPath, String templateKey, String defaultTemplate, Map templateValues) { + public String translate(String contextPath, String keyPath, 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); + template = getTemplateFromNearestPath(defaultLocale, contextPath, keyPath); if(template != null) break; } @@ -155,14 +155,14 @@ public class Translator { return result; } - private String getTemplateFromNearestPath(Locale locale, String contextPath, String templateKey) { + private String getTemplateFromNearestPath(Locale locale, String contextPath, String keyPath) { Map localTranslations = this.translations.get(locale); if(localTranslations == null) return null; String template = null; while(true) { - String path = ContextPaths.combinePaths(contextPath, templateKey); + String path = ContextPaths.combinePaths(contextPath, keyPath); template = localTranslations.get(path); if(template != null) break; // found template