From 6886c9866f14adac7cf788ff2adfecb3d87b58df Mon Sep 17 00:00:00 2001 From: altro3 Date: Sun, 4 Feb 2024 19:23:29 +0700 Subject: [PATCH] Add support constructor annotations --- .../visitor/AbstractOpenApiVisitor.java | 115 +++++++------ .../openapi/visitor/ElementUtils.java | 160 +++++++++++++++++- .../io/micronaut/openapi/visitor/Utils.java | 9 + .../OpenApiPojoControllerKotlinSpec.groovy | 116 +++++++++++++ 4 files changed, 338 insertions(+), 62 deletions(-) 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 5a8b32b23c..2bdd927412 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java @@ -138,9 +138,14 @@ import static io.micronaut.openapi.visitor.ConvertUtils.parseJsonString; import static io.micronaut.openapi.visitor.ConvertUtils.setDefaultValueObject; import static io.micronaut.openapi.visitor.ConvertUtils.toTupleSubMap; +import static io.micronaut.openapi.visitor.ElementUtils.findAnnotation; +import static io.micronaut.openapi.visitor.ElementUtils.getAnnotation; +import static io.micronaut.openapi.visitor.ElementUtils.getAnnotationMetadata; +import static io.micronaut.openapi.visitor.ElementUtils.isAnnotationPresent; import static io.micronaut.openapi.visitor.ElementUtils.isElementNotNullable; import static io.micronaut.openapi.visitor.ElementUtils.isFileUpload; import static io.micronaut.openapi.visitor.ElementUtils.isNullable; +import static io.micronaut.openapi.visitor.ElementUtils.stringValue; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.expandProperties; import static io.micronaut.openapi.visitor.OpenApiApplicationVisitor.resolvePlaceholders; import static io.micronaut.openapi.visitor.OpenApiConfigProperty.MICRONAUT_OPENAPI_FIELD_VISIBILITY_LEVEL; @@ -765,10 +770,10 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingEle AnnotationValue schemaAnnotationValue = null; if (definingElement != null) { - schemaAnnotationValue = definingElement.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + schemaAnnotationValue = getAnnotation(definingElement, io.swagger.v3.oas.annotations.media.Schema.class); } if (type != null && schemaAnnotationValue == null) { - schemaAnnotationValue = type.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + schemaAnnotationValue = getAnnotation(type, io.swagger.v3.oas.annotations.media.Schema.class); } boolean isSubstitudedType = false; if (schemaAnnotationValue != null) { @@ -1033,7 +1038,7 @@ private Schema processGenericAnnotations(Schema schema, ClassElement compo private void handleUnwrapped(VisitorContext context, Element element, ClassElement elementType, Schema parentSchema, AnnotationValue uw) { Map schemas = SchemaUtils.resolveSchemas(Utils.resolveOpenApi(context)); ClassElement customElementType = getCustomSchema(elementType.getName(), elementType.getTypeArguments(), context); - String schemaName = element.stringValue(io.swagger.v3.oas.annotations.media.Schema.class, "name") + String schemaName = stringValue(element, io.swagger.v3.oas.annotations.media.Schema.class, "name") .orElse(computeDefaultSchemaName(null, customElementType != null ? customElementType : elementType, elementType.getTypeArguments(), context, null)); Schema wrappedPropertySchema = schemas.get(schemaName); Map properties = wrappedPropertySchema.getProperties(); @@ -1074,12 +1079,12 @@ protected void processSchemaProperty(VisitorContext context, TypedElement elemen if (propertySchema == null) { return; } - AnnotationValue uw = element.getAnnotation(JsonUnwrapped.class); + AnnotationValue uw = getAnnotation(element, JsonUnwrapped.class); if (uw != null && uw.booleanValue("enabled").orElse(Boolean.TRUE)) { handleUnwrapped(context, element, elementType, parentSchema, uw); } else { // check schema required flag - AnnotationValue schemaAnnotationValue = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + AnnotationValue schemaAnnotationValue = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); Optional elementSchemaRequired = Optional.empty(); boolean isAutoRequiredMode = true; boolean isRequiredDefaultValueSet = false; @@ -1160,14 +1165,14 @@ private void addProperty(Schema parentSchema, String name, Schema property private String resolvePropertyName(Element element, Element classElement, Schema propertySchema) { String name = Optional.ofNullable(propertySchema.getName()).orElse(element.getName()); - if (element.hasAnnotation(io.swagger.v3.oas.annotations.media.Schema.class)) { - Optional nameFromSchema = element.stringValue(io.swagger.v3.oas.annotations.media.Schema.class, "name"); + if (isAnnotationPresent(element, io.swagger.v3.oas.annotations.media.Schema.class)) { + Optional nameFromSchema = stringValue(element, io.swagger.v3.oas.annotations.media.Schema.class, "name"); if (nameFromSchema.isPresent()) { return nameFromSchema.get(); } } - if (element.hasAnnotation(JsonProperty.class)) { - return element.stringValue(JsonProperty.class, "value").orElse(name); + if (isAnnotationPresent(element, JsonProperty.class)) { + return stringValue(element, JsonProperty.class, "value").orElse(name); } if (classElement != null && classElement.hasAnnotation(JsonNaming.class)) { // INVESTIGATE: "classValue" doesn't work in this case @@ -1202,7 +1207,7 @@ private String resolvePropertyName(Element element, Element classElement, Schema */ protected Schema bindSchemaForElement(VisitorContext context, TypedElement element, ClassElement elementType, Schema schemaToBind, @Nullable ClassElement jsonViewClass) { - AnnotationValue schemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + AnnotationValue schemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); Schema originalSchema = schemaToBind; if (originalSchema.get$ref() != null) { @@ -1228,7 +1233,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement el } } } - AnnotationValue arraySchemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class); + AnnotationValue arraySchemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.ArraySchema.class); if (arraySchemaAnn != null) { schemaToBind = bindArraySchemaAnnotationValue(context, element, schemaToBind, arraySchemaAnn, jsonViewClass); Optional schemaName = arraySchemaAnn.stringValue("name"); @@ -1255,11 +1260,11 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement el if (StringUtils.isNotEmpty(topLevelSchema.getDescription())) { notOnlyRef = true; } - if (element.isAnnotationPresent(Deprecated.class)) { + if (isAnnotationPresent(element, Deprecated.class)) { topLevelSchema.setDeprecated(true); notOnlyRef = true; } - final String defaultValue = element.stringValue(Bindable.class, "defaultValue").orElse(null); + final String defaultValue = stringValue(element, Bindable.class, "defaultValue").orElse(null); if (defaultValue != null && schemaToBind.getDefault() == null) { setDefaultValueObject(schemaToBind, defaultValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), true, context); notOnlyRef = true; @@ -1271,7 +1276,7 @@ protected Schema bindSchemaForElement(VisitorContext context, TypedElement el topLevelSchema.setNullable(true); notOnlyRef = true; } - final String defaultJacksonValue = element.stringValue(JsonProperty.class, "defaultValue").orElse(null); + final String defaultJacksonValue = stringValue(element, JsonProperty.class, "defaultValue").orElse(null); if (defaultJacksonValue != null && schemaToBind.getDefault() == null) { setDefaultValueObject(topLevelSchema, defaultJacksonValue, elementType, schemaToBind.getType(), schemaToBind.getFormat(), false, context); notOnlyRef = true; @@ -1321,46 +1326,46 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e final boolean isIterableOrMap = elementType.isIterable() || elementType.isAssignable(Map.class); if (isIterableOrMap) { - if (element.isAnnotationPresent("javax.validation.constraints.NotEmpty$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotEmpty$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.NotEmpty$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List")) { schemaToBind.setMinItems(1); } - element.findAnnotation("javax.validation.constraints.Size$List") + findAnnotation(element, "javax.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("min") .ifPresent(schemaToBind::setMinItems))); - element.findAnnotation("jakarta.validation.constraints.Size$List") + findAnnotation(element, "jakarta.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("min") .ifPresent(schemaToBind::setMinItems))); - element.findAnnotation("javax.validation.constraints.Size$List") + findAnnotation(element, "javax.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("max") .ifPresent(schemaToBind::setMaxItems))); - element.findAnnotation("jakarta.validation.constraints.Size$List") + findAnnotation(element, "jakarta.validation.constraints.Size$List") .ifPresent(listAnn -> listAnn.getValue(AnnotationValue.class) .ifPresent(ann -> ann.intValue("max") .ifPresent(schemaToBind::setMaxItems))); } else { if (PrimitiveType.STRING.getCommonName().equals(schemaToBind.getType())) { - if (element.isAnnotationPresent("javax.validation.constraints.NotEmpty$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotEmpty$List") - || element.isAnnotationPresent("javax.validation.constraints.NotBlank$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NotBlank$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.NotEmpty$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NotEmpty$List") + || isAnnotationPresent(element, "javax.validation.constraints.NotBlank$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NotBlank$List")) { schemaToBind.setMinLength(1); } - element.findAnnotation("javax.validation.constraints.Size$List") + findAnnotation(element, "javax.validation.constraints.Size$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.intValue("min").ifPresent(schemaToBind::setMinLength); ann.intValue("max").ifPresent(schemaToBind::setMaxLength); } }); - element.findAnnotation("jakarta.validation.constraints.Size$List") + findAnnotation(element, "jakarta.validation.constraints.Size$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.intValue("min").ifPresent(schemaToBind::setMinLength); @@ -1369,33 +1374,33 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e }); } - if (element.isAnnotationPresent("javax.validation.constraints.Negative$List") - || element.isAnnotationPresent("jakarta.validation.constraints.Negative$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.Negative$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.Negative$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); schemaToBind.exclusiveMaximum(true); } - if (element.isAnnotationPresent("javax.validation.constraints.NegativeOrZero$List") - || element.isAnnotationPresent("jakarta.validation.constraints.NegativeOrZero$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.NegativeOrZero$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.NegativeOrZero$List")) { schemaToBind.setMaximum(BigDecimal.ZERO); } - if (element.isAnnotationPresent("javax.validation.constraints.Positive$List") - || element.isAnnotationPresent("jakarta.validation.constraints.Positive$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.Positive$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.Positive$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); schemaToBind.exclusiveMinimum(true); } - if (element.isAnnotationPresent("javax.validation.constraints.PositiveOrZero$List") - || element.isAnnotationPresent("jakarta.validation.constraints.PositiveOrZero$List")) { + if (isAnnotationPresent(element, "javax.validation.constraints.PositiveOrZero$List") + || isAnnotationPresent(element, "jakarta.validation.constraints.PositiveOrZero$List")) { schemaToBind.setMinimum(BigDecimal.ZERO); } - element.findAnnotation("javax.validation.constraints.Min$List") + findAnnotation(element, "javax.validation.constraints.Min$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMinimum); } }); - element.findAnnotation("jakarta.validation.constraints.Min$List") + findAnnotation(element, "jakarta.validation.constraints.Min$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1403,14 +1408,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.Max$List") + findAnnotation(element, "javax.validation.constraints.Max$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMaximum); } }); - element.findAnnotation("jakarta.validation.constraints.Max$List") + findAnnotation(element, "jakarta.validation.constraints.Max$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1418,14 +1423,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.DecimalMin$List") + findAnnotation(element, "javax.validation.constraints.DecimalMin$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMinimum); } }); - element.findAnnotation("jakarta.validation.constraints.DecimalMin$List") + findAnnotation(element, "jakarta.validation.constraints.DecimalMin$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1433,14 +1438,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.DecimalMax$List") + findAnnotation(element, "javax.validation.constraints.DecimalMax$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) .ifPresent(schemaToBind::setMaximum); } }); - element.findAnnotation("jakarta.validation.constraints.DecimalMax$List") + findAnnotation(element, "jakarta.validation.constraints.DecimalMax$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.getValue(BigDecimal.class) @@ -1448,7 +1453,7 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.Email$List") + findAnnotation(element, "javax.validation.constraints.Email$List") .ifPresent(listAnn -> { schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); for (AnnotationValue ann : listAnn.getAnnotations("value")) { @@ -1456,7 +1461,7 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e .ifPresent(schemaToBind::setPattern); } }); - element.findAnnotation("jakarta.validation.constraints.Email$List") + findAnnotation(element, "jakarta.validation.constraints.Email$List") .ifPresent(listAnn -> { schemaToBind.setFormat(PrimitiveType.EMAIL.getCommonName()); for (AnnotationValue ann : listAnn.getAnnotations("value")) { @@ -1465,14 +1470,14 @@ protected void processJavaxValidationAnnotations(Element element, ClassElement e } }); - element.findAnnotation("javax.validation.constraints.Pattern$List") + findAnnotation(element, "javax.validation.constraints.Pattern$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.stringValue("regexp") .ifPresent(schemaToBind::setPattern); } }); - element.findAnnotation("jakarta.validation.constraints.Pattern$List") + findAnnotation(element, "jakarta.validation.constraints.Pattern$List") .ifPresent(listAnn -> { for (AnnotationValue ann : listAnn.getAnnotations("value")) { ann.stringValue("regexp") @@ -2263,15 +2268,15 @@ private List getEnumValues(EnumElement type, String schemaType, String s List enumValues = new ArrayList<>(); for (EnumConstantElement element : type.elements()) { - AnnotationValue schemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + AnnotationValue schemaAnn = getAnnotation(element, io.swagger.v3.oas.annotations.media.Schema.class); boolean isHidden = schemaAnn != null && schemaAnn.booleanValue("hidden").orElse(false); if (isHidden - || element.isAnnotationPresent(Hidden.class) - || element.isAnnotationPresent(JsonIgnore.class)) { + || isAnnotationPresent(element, Hidden.class) + || isAnnotationPresent(element, JsonIgnore.class)) { continue; } - AnnotationValue jsonProperty = element.getAnnotation(JsonProperty.class); + AnnotationValue jsonProperty = getAnnotation(element, JsonProperty.class); String jacksonValue = jsonProperty != null ? jsonProperty.stringValue("value").orElse(null) : null; if (StringUtils.hasText(jacksonValue)) { try { @@ -2520,10 +2525,10 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El } for (TypedElement publicField : publicFields) { - boolean isHidden = publicField.getAnnotationMetadata().booleanValue(io.swagger.v3.oas.annotations.media.Schema.class, "hidden").orElse(false); - AnnotationValue jsonAnySetterAnn = publicField.getAnnotation(JsonAnySetter.class); - if (publicField.isAnnotationPresent(JsonIgnore.class) - || publicField.isAnnotationPresent(Hidden.class) + boolean isHidden = getAnnotationMetadata(publicField).booleanValue(io.swagger.v3.oas.annotations.media.Schema.class, "hidden").orElse(false); + AnnotationValue jsonAnySetterAnn = getAnnotation(publicField, JsonAnySetter.class); + if (isAnnotationPresent(publicField, JsonIgnore.class) + || isAnnotationPresent(publicField, Hidden.class) || (jsonAnySetterAnn != null && jsonAnySetterAnn.booleanValue("enabled").orElse(true)) || isHidden) { continue; @@ -2576,7 +2581,7 @@ private void processPropertyElements(OpenAPI openAPI, VisitorContext context, El } private boolean allowedByJsonView(TypedElement publicField, String[] classLvlJsonViewClasses, ClassElement jsonViewClassEl, VisitorContext context) { - String[] fieldJsonViewClasses = publicField.getAnnotationMetadata().stringValues(JsonView.class); + String[] fieldJsonViewClasses = getAnnotationMetadata(publicField).stringValues(JsonView.class); if (ArrayUtils.isEmpty(fieldJsonViewClasses)) { fieldJsonViewClasses = classLvlJsonViewClasses; } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java index b67c0f6ddb..a0fbf6a577 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java @@ -17,6 +17,7 @@ import java.io.File; import java.io.InputStream; +import java.lang.annotation.Annotation; import java.nio.ByteBuffer; import java.security.Principal; import java.util.List; @@ -25,19 +26,24 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.Future; +import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.http.multipart.FileUpload; +import io.micronaut.inject.annotation.AnnotationMetadataHierarchy; import io.micronaut.inject.ast.ClassElement; import io.micronaut.inject.ast.Element; +import io.micronaut.inject.ast.MemberElement; +import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.ast.TypedElement; import io.micronaut.inject.visitor.VisitorContext; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -109,10 +115,6 @@ public static boolean isNullable(TypedElement element) { || element.getType().isOptional(); } - public static boolean isAnnotationPresent(Element element, String className) { - return element.findAnnotation(className).isPresent(); - } - /** * Checking if the type is file upload type. * @@ -219,12 +221,16 @@ public static boolean isIgnoredParameter(TypedElement parameter) { || parameter.isAnnotationPresent(Hidden.class) || parameter.isAnnotationPresent(JsonIgnore.class) || parameter.booleanValue(Parameter.class, "hidden").orElse(false) - || isAnnotationPresent(parameter, "io.micronaut.session.annotation.SessionValue") - || isAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttribute") - || isAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttributes") + || isParamAnnotationPresent(parameter, "io.micronaut.session.annotation.SessionValue") + || isParamAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttribute") + || isParamAnnotationPresent(parameter, "org.springframework.web.bind.annotation.SessionAttributes") || isIgnoredParameterType(parameter.getType()); } + private static boolean isParamAnnotationPresent(Element element, String className) { + return element.findAnnotation(className).isPresent(); + } + public static boolean isIgnoredParameterType(ClassElement parameterType) { return parameterType == null || parameterType.isAssignable(Principal.class) @@ -252,4 +258,144 @@ public static boolean isIgnoredParameterType(ClassElement parameterType) { || parameterType.isAssignable("org.springframework.validation.Errors") ; } + + public static AnnotationMetadata getAnnotationMetadata(Element el) { + if (el == null) { + return AnnotationMetadata.EMPTY_METADATA; + } + if (el instanceof MemberElement memberEl) { + var propMetadata = memberEl.getAnnotationMetadata(); + AnnotationMetadata constructorMetadata = null; + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + constructorMetadata = constructorParam.getAnnotationMetadata(); + break; + } + } + } + if (constructorMetadata == null || constructorMetadata.isEmpty()) { + return propMetadata; + } + return new AnnotationMetadataHierarchy(true, new AnnotationMetadata[] {propMetadata, constructorMetadata}); + } + return el.getAnnotationMetadata(); + } + + public static Optional> findAnnotation(Element el, String annName) { + if (el == null) { + return Optional.empty(); + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.findAnnotation(annName); + if (result.isPresent()) { + return result; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.findAnnotation(annName); + } + } + } + return Optional.empty(); + } + return el.findAnnotation(annName); + } + + public static boolean isAnnotationPresent(Element el, Class annClass) { + return isAnnotationPresent(el, annClass.getName()); + } + + public static boolean isAnnotationPresent(Element el, String annName) { + if (el == null) { + return false; + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.isAnnotationPresent(annName); + if (result) { + return true; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.isAnnotationPresent(annName); + } + } + } + return false; + } + return el.isAnnotationPresent(annName); + } + + public static Optional stringValue(Element el, Class annClass, String member) { + if (el == null) { + return Optional.empty(); + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.stringValue(annClass, member); + if (result.isPresent()) { + return result; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.stringValue(annClass, member); + } + } + } + return result; + } + return el.stringValue(annClass, member); + } + + public static AnnotationValue getAnnotation(Element el, Class annClass) { + return getAnnotation(el, annClass.getName()); + } + + public static AnnotationValue getAnnotation(Element el, String annName) { + if (el == null) { + return null; + } + if (el instanceof MemberElement memberEl) { + var result = memberEl.getAnnotation(annName); + if (result != null) { + return result; + } + var constructor = getCreatorConstructor(memberEl.getOwningType()); + if (constructor != null) { + for (var constructorParam : constructor.getParameters()) { + if (constructorParam.getName().equals(memberEl.getName())) { + return constructorParam.getAnnotation(annName); + } + } + } + return result; + } + return el.getAnnotation(annName); + } + + private static MethodElement getCreatorConstructor(ClassElement classEl) { + + var cachedConstructor = Utils.getCreatorConstructorsCache().get(classEl.getName()); + if (cachedConstructor != null) { + return cachedConstructor; + } + + var creatorConstructor = classEl.getPrimaryConstructor().orElse(null); + var constructors = classEl.getAccessibleConstructors(); + if (constructors.size() > 1) { + for (var constructor : constructors) { + if (constructor.isDeclaredAnnotationPresent(JsonCreator.class)) { + creatorConstructor = constructor; + } + } + } + Utils.getCreatorConstructorsCache().put(classEl.getName(), creatorConstructor); + return creatorConstructor; + } } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java index bed6b4115e..05291ed6d4 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/Utils.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -36,6 +37,7 @@ import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.value.PropertyResolver; import io.micronaut.http.MediaType; +import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.openapi.javadoc.JavadocParser; import io.micronaut.openapi.visitor.group.EndpointInfo; @@ -60,6 +62,8 @@ public final class Utils { public static final List DEFAULT_MEDIA_TYPES = Collections.singletonList(MediaType.APPLICATION_JSON_TYPE); + private static Map creatorConstructorsCache = new HashMap<>(); + private static Set allKnownVersions; private static Set allKnownGroups; private static Map> endpointInfos; @@ -330,6 +334,10 @@ public static void setIncludedClassesGroupsExcluded(Map> in Utils.includedClassesGroupsExcluded = includedClassesGroupsExcluded; } + public static Map getCreatorConstructorsCache() { + return creatorConstructorsCache; + } + public static void clean() { openApis = null; endpointInfos = null; @@ -344,5 +352,6 @@ public static void clean() { testFileName = null; testYamlReference = null; testJsonReference = null; + creatorConstructorsCache = new HashMap<>(); } } diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy index 2effb05112..c7a4386177 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerKotlinSpec.groovy @@ -157,4 +157,120 @@ class MyBean schemas.Animal schemas.ColorEnum } + + void "test kotlin constructor annotations"() { + + when: + buildBeanDefinition('test.MyBean', ''' +package test + +import com.fasterxml.jackson.annotation.* +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.annotation.JsonValue +import io.micronaut.core.annotation.Nullable +import io.micronaut.http.annotation.Body +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Put +import io.micronaut.serde.annotation.Serdeable +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.Valid +import jakarta.validation.constraints.* +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import reactor.core.publisher.Mono + +@Controller +class HelloController { + + @Put("/sendModelWithDiscriminator") + fun sendModelWithDiscriminator( + @Body @NotNull @Valid animal: Animal + ): Mono = Mono.empty() +} + +/** + * Animal + * + * @param color + * @param propertyClass + */ +@Serdeable +@JsonPropertyOrder( + Animal.JSON_PROPERTY_PROPERTY_CLASS, + Animal.JSON_PROPERTY_COLOR +) +open class Animal ( + @Nullable + @Schema(name = "color", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @JsonProperty(JSON_PROPERTY_COLOR) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var color: ColorEnum? = null, + @Size(max = 50) + @Nullable + @Schema(name = "class", requiredMode = Schema.RequiredMode.NOT_REQUIRED) +// @JsonProperty(JSON_PROPERTY_PROPERTY_CLASS) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var propertyClass: String? = null, +) { + + companion object { + + const val JSON_PROPERTY_PROPERTY_CLASS = "class" + const val JSON_PROPERTY_COLOR = "color" + } +} + +@Serdeable +enum class ColorEnum ( + @get:JsonValue val value: String +) { + + @JsonProperty("red") + RED("red"), + @JsonProperty("blue") + BLUE("blue"), + @JsonProperty("green") + GREEN("green"), + @JsonProperty("light-blue") + LIGHT_BLUE("light-blue"), + @JsonProperty("dark-green") + DARK_GREEN("dark-green"); + + override fun toString(): String { + return value + } + + companion object { + + @JvmField + val VALUE_MAPPING = entries.associateBy { it.value } + + @JsonCreator + @JvmStatic + fun fromValue(value: String): ColorEnum { + require(VALUE_MAPPING.containsKey(value)) { "Unexpected value '$value'" } + return VALUE_MAPPING[value]!! + } + } +} + +@jakarta.inject.Singleton +class MyBean {} +''') + then: "the state is correct" + Utils.testReference != null + + when: "The OpenAPI is retrieved" + OpenAPI openAPI = Utils.testReference + Schema schema = openAPI.components.schemas.Animal + + then: "the components are valid" + schema + schema.properties.size() == 2 + schema.properties.class + schema.properties.class.maxLength == 50 + } }