diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java index d9becad9df..6cf6331c45 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java @@ -151,6 +151,24 @@ JsonNode toJson(Map values, VisitorContext context) { return jsonMapper.valueToTree(newValues); } + /** + * Convert the given Map to a JSON node and then to the specified type. + * @param The output class type + * @param values The values + * @param context The visitor context + * @param type The class + * @return The converted instance + */ + Optional toValue(Map values, VisitorContext context, Class type) { + JsonNode node = toJson(values, context); + try { + return Optional.of(treeToValue(node, type)); + } catch (JsonProcessingException e) { + context.warn("Error converting [" + node + "]: to " + type + ": " + e.getMessage(), null); + } + return Optional.empty(); + } + /** * Resolve the PathItem for the given {@link UriMatchTemplate}. * @@ -419,15 +437,8 @@ private void processAnnotationValue(VisitorContext context, A .filter(entry -> filters == null || ! filters.contains(entry.getKey())) .collect(toMap( e -> e.getKey().equals("requiredProperties") ? "required" : e.getKey(), Map.Entry::getValue)); - JsonNode schemaJson = toJson(values, context); - try { - T schema = treeToValue(schemaJson, type); - if (schema != null) { - schemaToValueMap(arraySchemaMap, schema); - } - } catch (JsonProcessingException e) { - context.warn("Error reading Swagger Schema: " + e.getMessage(), null); - } + Optional schema = toValue(values, context, type); + schema.ifPresent(s -> schemaToValueMap(arraySchemaMap, s)); } private Map resolveArraySchemaAnnotationValues(VisitorContext context, AnnotationValue av) { @@ -1029,11 +1040,11 @@ protected Schema readSchema(AnnotationValue e.getKey().equals("requiredProperties") ? "required" : e.getKey(), Map.Entry::getValue)); - JsonNode schemaJson = toJson(values, context); - Schema schema = treeToValue(schemaJson, Schema.class); - if (schema == null) { + Optional schemaOpt = toValue(values, context, Schema.class); + if (!schemaOpt.isPresent()) { return null; } + Schema schema = schemaOpt.get(); ComposedSchema composedSchema = null; if (schema instanceof ComposedSchema) { composedSchema = (ComposedSchema) schema; @@ -1226,13 +1237,15 @@ private boolean isContainerType(ClassElement type) { } /** - * Processes {@link io.swagger.v3.oas.annotations.security.SecurityScheme} annotations. + * Processes {@link io.swagger.v3.oas.annotations.security.SecurityScheme} + * annotations. * * @param element The element * @param context The visitor context */ protected void processSecuritySchemes(ClassElement element, VisitorContext context) { - final List> values = element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.security.SecurityScheme.class); + final List> values = element + .getAnnotationValuesByType(io.swagger.v3.oas.annotations.security.SecurityScheme.class); final OpenAPI openAPI = resolveOpenAPI(context); for (AnnotationValue securityRequirementAnnotationValue : values) { @@ -1245,25 +1258,17 @@ protected void processSecuritySchemes(ClassElement element, VisitorContext conte } else { map.remove("name"); } - normalizeEnumValues(map, CollectionUtils.mapOf( - "type", SecurityScheme.Type.class, "in", SecurityScheme.In.class - )); - final JsonNode jsonNode = toJson(map, context); - try { - final Optional securityRequirement = Optional.of(treeToValue(jsonNode, SecurityScheme.class)); - securityRequirement.ifPresent(securityScheme -> { + normalizeEnumValues(map, CollectionUtils.mapOf("type", SecurityScheme.Type.class, "in", SecurityScheme.In.class)); + Optional securityRequirement = toValue(map, context, SecurityScheme.class); + securityRequirement.ifPresent(securityScheme -> { - try { - securityScheme.setIn(Enum.valueOf(SecurityScheme.In.class, map.get("in").toString().toUpperCase(Locale.ENGLISH))); - } catch (Exception e) { - // ignore - } - resolveComponents(openAPI).addSecuritySchemes(name, securityScheme); - } - ); - } catch (JsonProcessingException e) { - context.warn("Error reading Swagger SecurityRequirement for element [" + element + "]: " + e.getMessage(), element); - } + try { + securityScheme.setIn(Enum.valueOf(SecurityScheme.In.class, map.get("in").toString().toUpperCase(Locale.ENGLISH))); + } catch (Exception e) { + // ignore + } + resolveComponents(openAPI).addSecuritySchemes(name, securityScheme); + }); }); } } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java index eb2f6ae3c5..3a618e83cf 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiApplicationVisitor.java @@ -15,8 +15,6 @@ */ package io.micronaut.openapi.visitor; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.PropertyNamingStrategy.PropertyNamingStrategyBase; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -279,22 +277,15 @@ private List processOpenApiAnnotation(ClassElement tagList = new ArrayList<>(); } for (AnnotationValue tag : annotations) { - JsonNode jsonNode; + Map values; if (tag.getAnnotationName().equals(SecurityRequirement.class.getName()) && tag.getValues().size() > 0) { Object name = tag.getValues().get("name"); Object scopes = Optional.ofNullable(tag.getValues().get("scopes")).orElse(new ArrayList()); - jsonNode = toJson(Collections.singletonMap((CharSequence) name, scopes), context); + values = Collections.singletonMap((CharSequence) name, scopes); } else { - jsonNode = toJson(tag.getValues(), context); - } - try { - T t = treeToValue(jsonNode, modelType); - if (t != null) { - tagList.add(t); - } - } catch (JsonProcessingException e) { - context.warn("Error reading OpenAPI" + annotationType + " annotation", element); + values = tag.getValues(); } + toValue(values, context, modelType).ifPresent(tagList::add); } } return tagList; @@ -302,23 +293,16 @@ private List processOpenApiAnnotation(ClassElement private OpenAPI readOpenAPI(ClassElement element, VisitorContext context) { return element.findAnnotation(OpenAPIDefinition.class).flatMap(o -> { - JsonNode jsonNode = toJson(o.getValues(), context); - - try { - Optional result = Optional.of(treeToValue(jsonNode, OpenAPI.class)); - result.ifPresent(openAPI -> { - List securityRequirements = - o.getAnnotations("security", io.swagger.v3.oas.annotations.security.SecurityRequirement.class) - .stream() - .map(this::mapToSecurityRequirement) - .collect(Collectors.toList()); - openAPI.setSecurity(securityRequirements); - }); - return result; - } catch (JsonProcessingException e) { - context.warn("Error reading Swagger OpenAPI for element [" + element + "]: " + e.getMessage(), element); - return Optional.empty(); - } + Optional result = toValue(o.getValues(), context, OpenAPI.class); + result.ifPresent(openAPI -> { + List securityRequirements = + o.getAnnotations("security", io.swagger.v3.oas.annotations.security.SecurityRequirement.class) + .stream() + .map(this::mapToSecurityRequirement) + .collect(Collectors.toList()); + openAPI.setSecurity(securityRequirements); + }); + return result; }).orElse(new OpenAPI()); } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiControllerVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiControllerVisitor.java index 658f8015b4..eefb331069 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiControllerVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/OpenApiControllerVisitor.java @@ -96,12 +96,14 @@ */ @Experimental public class OpenApiControllerVisitor extends AbstractOpenApiVisitor implements TypeElementVisitor { + private static final String CLASS_TAGS = "CLASS_TAGS"; private PropertyPlaceholderResolver propertyPlaceholderResolver; @Override public void visitClass(ClassElement element, VisitorContext context) { processSecuritySchemes(element, context); + context.put(CLASS_TAGS, readTags(element, context)); } private boolean hasNoBindingAnnotationOrType(ParameterElement parameter) { @@ -122,6 +124,7 @@ private boolean hasNoBindingAnnotationOrType(ParameterElement parameter) { !parameter.getType().isAssignable("io.micronaut.http.BasicAuth"); } + @SuppressWarnings("unchecked") @Override public void visitMethod(MethodElement element, VisitorContext context) { if (element.isAnnotationPresent(Hidden.class)) { @@ -152,19 +155,9 @@ public void visitMethod(MethodElement element, VisitorContext context) { OpenAPI openAPI = resolveOpenAPI(context); final Optional> operationAnnotation = element.findAnnotation(Operation.class); - io.swagger.v3.oas.models.Operation swaggerOperation = operationAnnotation.flatMap(o -> { - JsonNode jsonNode = toJson(o.getValues(), context); - - try { - return Optional.of(treeToValue(jsonNode, io.swagger.v3.oas.models.Operation.class)); - } catch (Exception e) { - context.warn("Error reading Swagger Operation for element [" + element + "]: " + e.getMessage(), element); - return Optional.empty(); - } - }).orElse(new io.swagger.v3.oas.models.Operation()); - - readTags(element, swaggerOperation); - + io.swagger.v3.oas.models.Operation swaggerOperation = operationAnnotation.flatMap(o -> + toValue(o.getValues(), context, io.swagger.v3.oas.models.Operation.class)).orElse(new io.swagger.v3.oas.models.Operation()); + readTags(element, swaggerOperation, (List) context.get(CLASS_TAGS, List.class, Collections.emptyList())); readSecurityRequirements(element, context, swaggerOperation); readApiResponses(element, context, swaggerOperation); @@ -596,17 +589,9 @@ private void readApiResponses(MethodElement element, VisitorContext context, io. if (CollectionUtils.isNotEmpty(responseAnnotations)) { ApiResponses apiResponses = new ApiResponses(); for (AnnotationValue r : responseAnnotations) { - - JsonNode jn = toJson(r.getValues(), context); - try { - Optional newResponse = Optional.of(treeToValue(jn, ApiResponse.class)); - newResponse.ifPresent(apiResponse -> { - String name = r.get("responseCode", String.class).orElse("default"); - apiResponses.put(name, apiResponse); - }); - } catch (Exception e) { - context.warn("Error reading Swagger ApiResponses for element [" + element + "]: " + e.getMessage(), element); - } + Optional newResponse = toValue(r.getValues(), context, ApiResponse.class); + newResponse.ifPresent(apiResponse -> + apiResponses.put(r.get("responseCode", String.class).orElse("default"), apiResponse)); } swaggerOperation.setResponses(apiResponses); } @@ -614,16 +599,7 @@ private void readApiResponses(MethodElement element, VisitorContext context, io. private void readSwaggerRequestBody(Element element, VisitorContext context, io.swagger.v3.oas.models.Operation swaggerOperation) { element.findAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class) - .flatMap(annotation -> { - JsonNode jn = toJson(annotation.getValues(), context); - try { - return Optional.of(treeToValue(jn, RequestBody.class)); - } catch (Exception e) { - context.warn("Error reading Swagger ResponseBody for element [" + element + "]: " + e.getMessage(), element); - return Optional.empty(); - } - - }) + .flatMap(annotation -> toValue(annotation.getValues(), context, RequestBody.class)) .ifPresent(swaggerOperation::setRequestBody); } @@ -644,13 +620,7 @@ private void readServers(MethodElement element, VisitorContext context, io.swagg List> serverAnnotations = element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.servers.Server.class); if (CollectionUtils.isNotEmpty(serverAnnotations)) { for (AnnotationValue r : serverAnnotations) { - JsonNode jn = toJson(r.getValues(), context); - try { - Optional newRequirement = Optional.of(treeToValue(jn, Server.class)); - newRequirement.ifPresent(swaggerOperation::addServersItem); - } catch (Exception e) { - context.warn("Error reading Swagger Server for element [" + element + "]: " + e.getMessage(), element); - } + toValue(r.getValues(), context, Server.class).ifPresent(swaggerOperation::addServersItem); } } } @@ -677,16 +647,8 @@ private void readCallbacks(MethodElement element, VisitorContext context, io.swa final PathItem pathItem = new PathItem(); for (AnnotationValue operation : operations) { final Optional operationMethod = operation.get("method", HttpMethod.class); - operationMethod.ifPresent(httpMethod -> { - JsonNode jsonNode = toJson(operation.getValues(), context); - - try { - final Optional op = Optional.of(treeToValue(jsonNode, io.swagger.v3.oas.models.Operation.class)); - op.ifPresent(operation1 -> setOperationOnPathItem(pathItem, operation1, httpMethod)); - } catch (Exception e) { - context.warn("Error reading Swagger Operation for element [" + element + "]: " + e.getMessage(), element); - } - }); + operationMethod.ifPresent(httpMethod -> + toValue(operation.getValues(), context, io.swagger.v3.oas.models.Operation.class).ifPresent(op -> setOperationOnPathItem(pathItem, op, httpMethod))); } Map callbacks = initCallbacks(swaggerOperation); final io.swagger.v3.oas.models.callbacks.Callback c = new io.swagger.v3.oas.models.callbacks.Callback(); @@ -720,13 +682,36 @@ private Map initCallbacks(i return callbacks; } - private void readTags(MethodElement element, io.swagger.v3.oas.models.Operation swaggerOperation) { + private void readTags(MethodElement element, io.swagger.v3.oas.models.Operation swaggerOperation, List classTags) { List> tagAnnotations = element.getAnnotationValuesByType(Tag.class); if (CollectionUtils.isNotEmpty(tagAnnotations)) { for (AnnotationValue r : tagAnnotations) { r.get("name", String.class).ifPresent(swaggerOperation::addTagsItem); } } + if (!classTags.isEmpty()) { + List operationTags = swaggerOperation.getTags(); + if (operationTags == null) { + operationTags = new ArrayList<>(classTags.size()); + swaggerOperation.setTags(operationTags); + } + for (io.swagger.v3.oas.models.tags.Tag tag : classTags) { + if (!operationTags.contains(tag.getName())) { + operationTags.add(tag.getName()); + } + } + } + } + + private List readTags(ClassElement element, VisitorContext context) { + List tagList = new ArrayList<>(); + List> tagAnnotations = element.getAnnotationValuesByType(Tag.class); + if (CollectionUtils.isNotEmpty(tagAnnotations)) { + for (AnnotationValue tag : tagAnnotations) { + toValue(tag.getValues(), context, io.swagger.v3.oas.models.tags.Tag.class).ifPresent(tagList::add); + } + } + return tagList; } private Content buildContent(Element definingElement, ClassElement type, String mediaType, OpenAPI openAPI, VisitorContext context) {