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 { public String path; public Map keys; } private final Translator translator; private final ApiErrorFactory apiErrorFactory; public AnnotationPostProcessor() { this(Translator.getInstance(), ApiErrorFactory.getInstance()); } public AnnotationPostProcessor(Translator translator, ApiErrorFactory apiErrorFactory) { this.translator = translator; this.apiErrorFactory = apiErrorFactory; } @Override public void beforeScan(Reader reader, OpenAPI openAPI) {} @Override public void afterScan(Reader reader, OpenAPI openAPI) { // use context path and keys from "x-translation" extension annotations // to translate supported annotations and finally remove "x-translation" extensions Info resourceInfo = openAPI.getInfo(); ContextInformation resourceContext = getContextInformation(openAPI.getExtensions()); removeTranslationAnnotations(openAPI.getExtensions()); 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()); TranslateProperties(Constants.TRANSLATABLE_PATH_ITEM_PROPERTIES, pathContext, pathItem); for (Operation operation : pathItem.readOperations()) { ContextInformation operationContext = getContextInformation(operation.getExtensions(), pathContext); removeTranslationAnnotations(operation.getExtensions()); 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()); TranslateProperties(Constants.TRANSLATABLE_API_RESPONSE_PROPERTIES, responseContext, response); } } } } 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) { String key = keys.get(prop.keyName()); if(key != null) { String originalValue = prop.getValue(item); // XXX: use browser locale instead default? String translation = translator.translate(context.path, key, originalValue); prop.setValue(item, translation); } } } } 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); } private ContextInformation getContextInformation(Map extensions, ContextInformation base) { if(extensions != null) { Map translationDefinitions = (Map)extensions.get("x-" + Constants.TRANSLATION_EXTENSION_NAME); if(translationDefinitions != null) { ContextInformation result = new ContextInformation(); result.path = combinePaths(base, (String)translationDefinitions.get(Constants.TRANSLATION_PATH_EXTENSION_NAME)); result.keys = getTranslationKeys(translationDefinitions); return result; } } if(base != null) { ContextInformation result = new ContextInformation(); result.path = base.path; return result; } 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-" + extensionName); } private Map getTranslationKeys(Map translationDefinitions) { Map result = new HashMap<>(); for(TranslatableProperty prop : Constants.TRANSLATABLE_INFO_PROPERTIES) { String key = (String)translationDefinitions.get(prop.keyName()); if(key != null) result.put(prop.keyName(), key); } return result; } private String combinePaths(ContextInformation base, String path) { String basePath = (base != null) ? base.path : null; return ContextPaths.combinePaths(basePath, path); } }