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
This commit is contained in:
Kc 2018-10-20 01:29:20 +02:00
parent d2aab4b446
commit 6590863201
7 changed files with 183 additions and 169 deletions

View File

@ -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<String, PathItem> 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<String, ApiResponse> 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 <T> void TranslateProperty(List<TranslatableProperty<T>> translatableProperties, ContextInformation context, T item) {
private void addApiErrorResponses(Operation operation) {
List<ApiError> 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 <T> void TranslateProperties(List<TranslatableProperty<T>> translatableProperties, ContextInformation context, T item) {
if(context.keys != null) {
Map<String, String> keys = context.keys;
for(TranslatableProperty<T> prop : translatableProperties) {
@ -80,6 +119,48 @@ public class AnnotationPostProcessor implements ReaderListener {
}
}
private List<ApiError> getApiErrors(Map<String, Object> extensions) {
if(extensions == null)
return null;
List<String> 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<ApiError> 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<String, Object> extensions) {
return getContextInformation(extensions, null);
}
@ -104,11 +185,21 @@ public class AnnotationPostProcessor implements ReaderListener {
return null;
}
private void removeApiErrorsAnnotations(Map<String, Object> extensions) {
String extensionName = Constants.API_ERRORS_EXTENSION_NAME;
removeExtension(extensions, extensionName);
}
private void removeTranslationAnnotations(Map<String, Object> extensions) {
String extensionName = Constants.TRANSLATION_EXTENSION_NAME;
removeExtension(extensions, extensionName);
}
private void removeExtension(Map<String, Object> extensions, String extensionName) {
if(extensions == null)
return;
extensions.remove("x-" + Constants.TRANSLATION_EXTENSION_NAME);
extensions.remove("x-" + extensionName);
}
private Map<String, String> getTranslationKeys(Map<String, Object> translationDefinitions) {

View File

@ -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;
}

View File

@ -147,13 +147,17 @@ public class ApiErrorFactory {
}
private ErrorMessageEntry createErrorMessageEntry(ApiError errorCode, String defaultTemplate, AbstractMap.SimpleEntry<String, Object>... 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;
}

View File

@ -48,10 +48,15 @@ public class BlocksResource {
@Path("/{signature}")
@Operation(
description = "returns the block that matches the given signature",
extensions = @Extension(name = "translation", properties = {
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 = {
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 = {
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 = {
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 = {
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")
})
}
)
}
)

View File

@ -8,6 +8,8 @@ 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,9 +19,9 @@ 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<TranslatableProperty<Info>> TRANSLATABLE_INFO_PROPERTIES = asList(
new TranslatableProperty<Info>() {
@Override public String keyName() { return TRANSLATION_ANNOTATION_DESCRIPTION_KEY; }

View File

@ -53,7 +53,7 @@ public class TranslationXmlStreamReader {
State state = new State(Locale.forLanguageTag("default"), "/");
List<TranslationEntry> result = new ArrayList<TranslationEntry>();
List<TranslationEntry> result = new ArrayList<>();
if (eventReader.hasNext())
{
XMLEvent event = eventReader.nextTag();

View File

@ -89,52 +89,52 @@ public class Translator {
return map;
}
public String translate(Locale locale, String contextPath, String templateKey, AbstractMap.Entry<String, Object>... templateValues) {
public String translate(Locale locale, String contextPath, String keyPath, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> map = createMap(templateValues);
return translate(locale, contextPath, templateKey, map);
return translate(locale, contextPath, keyPath, map);
}
public String translate(String contextPath, String templateKey, AbstractMap.Entry<String, Object>... templateValues) {
public String translate(String contextPath, String keyPath, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> map = createMap(templateValues);
return translate(contextPath, templateKey, map);
return translate(contextPath, keyPath, map);
}
public String translate(Locale locale, String contextPath, String templateKey, Map<String, Object> templateValues) {
return translate(locale, contextPath, templateKey, null, templateValues);
public String translate(Locale locale, String contextPath, String keyPath, Map<String, Object> templateValues) {
return translate(locale, contextPath, keyPath, null, templateValues);
}
public String translate(String contextPath, String templateKey, Map<String, Object> templateValues) {
return translate(contextPath, templateKey, null, templateValues);
public String translate(String contextPath, String keyPath, Map<String, Object> templateValues) {
return translate(contextPath, keyPath, null, templateValues);
}
public String translate(Locale locale, String contextPath, String templateKey, String defaultTemplate, AbstractMap.Entry<String, Object>... templateValues) {
public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> 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<String, Object>... templateValues) {
public String translate(String contextPath, String keyPath, String defaultTemplate, AbstractMap.Entry<String, Object>... templateValues) {
Map<String, Object> 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<String, Object> templateValues) {
public String translate(Locale locale, String contextPath, String keyPath, String defaultTemplate, Map<String, Object> 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<String, Object> templateValues) {
public String translate(String contextPath, String keyPath, String defaultTemplate, Map<String, Object> 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<String, String> 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