From c21b1f07588c749d6e6e72875a9ba00746c9504c Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Sun, 7 Aug 2022 10:46:13 -0400 Subject: [PATCH] Add support for `@JsonView` Closes #1008 Signed-off-by: Michael Edgar --- .../api/constants/JacksonConstants.java | 6 +- .../runtime/io/schema/SchemaFactory.java | 19 +-- .../scanner/OpenApiDataObjectScanner.java | 6 +- .../runtime/scanner/SchemaRegistry.java | 73 ++++++--- .../dataobject/AnnotationTargetProcessor.java | 20 ++- .../scanner/dataobject/TypeProcessor.java | 8 +- .../scanner/dataobject/TypeResolver.java | 19 +++ .../scanner/spi/AnnotationScanner.java | 37 +++++ .../scanner/spi/AnnotationScannerContext.java | 6 + .../openapi/runtime/util/JandexUtil.java | 36 +++-- .../openapi/runtime/util/TypeUtil.java | 53 +++++++ .../scanner/ExpectationWithRefsTests.java | 5 +- .../scanner/JaxRsAnnotationScannerTest.java | 3 +- .../runtime/scanner/JsonViewTests.java | 97 ++++++++++++ .../runtime/scanner/KitchenSinkTest.java | 3 +- .../scanner/NestedSchemaReferenceTests.java | 3 +- .../runtime/scanner/SchemaRegistryTests.java | 15 +- .../special.jsonview-schemas-basic.json | 148 ++++++++++++++++++ 18 files changed, 492 insertions(+), 65 deletions(-) create mode 100644 extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java create mode 100644 extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-schemas-basic.json diff --git a/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java b/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java index 0cb3514b2..5198de9ff 100644 --- a/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java +++ b/core/src/main/java/io/smallrye/openapi/api/constants/JacksonConstants.java @@ -22,10 +22,12 @@ public class JacksonConstants { .createSimple("com.fasterxml.jackson.annotation.JsonPropertyOrder"); public static final DotName JSON_UNWRAPPED = DotName .createSimple("com.fasterxml.jackson.annotation.JsonUnwrapped"); - public static final DotName JSON_NAMING = DotName - .createSimple("com.fasterxml.jackson.databind.annotation.JsonNaming"); public static final DotName JSON_VALUE = DotName .createSimple("com.fasterxml.jackson.annotation.JsonValue"); + public static final DotName JSON_VIEW = DotName + .createSimple("com.fasterxml.jackson.annotation.JsonView"); + public static final DotName JSON_NAMING = DotName + .createSimple("com.fasterxml.jackson.databind.annotation.JsonNaming"); public static final String PROP_VALUE = "value"; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java index 1dd75d688..bb483b3ee 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java @@ -583,14 +583,15 @@ private static Schema introspectClassToSchema(final AnnotationScannerContext con } SchemaRegistry schemaRegistry = SchemaRegistry.currentInstance(); - if (schemaReferenceSupported && schemaRegistry.hasSchema(ctype)) { - return schemaRegistry.lookupRef(ctype); - } else if (!schemaReferenceSupported && schemaRegistry != null && schemaRegistry.hasSchema(ctype)) { + if (schemaReferenceSupported && schemaRegistry.hasSchema(ctype, context.getJsonViews())) { + return schemaRegistry.lookupRef(ctype, context.getJsonViews()); + } else if (!schemaReferenceSupported && schemaRegistry != null + && schemaRegistry.hasSchema(ctype, context.getJsonViews())) { // Clone the schema from the registry using mergeObjects - return MergeUtil.mergeObjects(new SchemaImpl(), schemaRegistry.lookupSchema(ctype)); + return MergeUtil.mergeObjects(new SchemaImpl(), schemaRegistry.lookupSchema(ctype, context.getJsonViews())); } else if (context.getScanStack().contains(ctype)) { // Protect against stack overflow when the type is in the process of being scanned. - return SchemaRegistry.registerReference(ctype, null, new SchemaImpl()); + return SchemaRegistry.registerReference(ctype, context.getJsonViews(), null, new SchemaImpl()); } else { Schema schema = OpenApiDataObjectScanner.process(context, ctype); @@ -614,9 +615,9 @@ public static Schema schemaRegistration(final AnnotationScannerContext context, SchemaRegistry schemaRegistry = SchemaRegistry.currentInstance(); if (allowRegistration(context, schemaRegistry, type, schema)) { - schema = schemaRegistry.register(type, schema); - } else if (schemaRegistry != null && schemaRegistry.hasRef(type)) { - schema = schemaRegistry.lookupRef(type); + schema = schemaRegistry.register(type, context.getJsonViews(), schema); + } else if (schemaRegistry != null && schemaRegistry.hasRef(type, context.getJsonViews())) { + schema = schemaRegistry.lookupRef(type, context.getJsonViews()); } return schema; @@ -643,7 +644,7 @@ static boolean allowRegistration(final AnnotationScannerContext context, SchemaR /* * Only register if the type is not already registered */ - return !registry.hasSchema(type); + return !registry.hasSchema(type, context.getJsonViews()); } /** diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java index 8718d9961..befe05083 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java @@ -221,7 +221,7 @@ private void depthFirstGraphSearch() { Type currentType = currentPathEntry.getClazzType(); - if (SchemaRegistry.hasSchema(currentType, null)) { + if (SchemaRegistry.hasSchema(currentType, context.getJsonViews(), null)) { // This type has already been scanned and registered, don't do it again! continue; } @@ -292,9 +292,9 @@ private void processInheritance(DataObjectDeque.PathEntry currentPathEntry) { this.rootSchema = enclosingSchema; } - if (SchemaRegistry.hasSchema(currentType, null)) { + if (SchemaRegistry.hasSchema(currentType, context.getJsonViews(), null)) { // Replace the registered schema if one is present - SchemaRegistry.currentInstance().register(currentType, enclosingSchema); + SchemaRegistry.currentInstance().register(currentType, context.getJsonViews(), enclosingSchema); } } } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java index eb775ec87..ee4af47e1 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/SchemaRegistry.java @@ -2,6 +2,7 @@ import static io.smallrye.openapi.runtime.util.TypeUtil.getSchemaAnnotation; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -106,8 +107,8 @@ public static void remove() { * @return the same schema if not eligible for registration, or a reference * to the schema registered for the given Type */ - public static Schema checkRegistration(Type type, TypeResolver resolver, Schema schema) { - return register(type, resolver, schema, (registry, key) -> registry.register(key, schema, null)); + public static Schema checkRegistration(Type type, Set views, TypeResolver resolver, Schema schema) { + return register(type, views, resolver, schema, (registry, key) -> registry.register(key, schema, null)); } /** @@ -140,11 +141,11 @@ public static Schema checkRegistration(Type type, TypeResolver resolver, Schema * @return the same schema if not eligible for registration, or a reference * to the schema registered for the given Type */ - public static Schema registerReference(Type type, TypeResolver resolver, Schema schema) { - return register(type, resolver, schema, (registry, key) -> registry.registerReference(key)); + public static Schema registerReference(Type type, Set views, TypeResolver resolver, Schema schema) { + return register(type, views, resolver, schema, (registry, key) -> registry.registerReference(key)); } - static Schema register(Type type, TypeResolver resolver, Schema schema, + static Schema register(Type type, Set views, TypeResolver resolver, Schema schema, BiFunction registrationAction) { Type resolvedType; @@ -170,7 +171,7 @@ static Schema register(Type type, TypeResolver resolver, Schema schema, return schema; } - TypeKey key = new TypeKey(resolvedType); + TypeKey key = new TypeKey(resolvedType, views); if (registry.hasRef(key)) { schema = registry.lookupRef(key); @@ -192,7 +193,7 @@ static Schema register(Type type, TypeResolver resolver, Schema schema, * @param resolver resolver for type parameter * @return true when schema references are enabled and the type is present in the registry, otherwise false */ - public static boolean hasSchema(Type type, TypeResolver resolver) { + public static boolean hasSchema(Type type, Set views, TypeResolver resolver) { SchemaRegistry registry = currentInstance(); if (registry == null) { @@ -207,7 +208,7 @@ public static boolean hasSchema(Type type, TypeResolver resolver) { resolvedType = type; } - return registry.hasSchema(resolvedType); + return registry.hasSchema(resolvedType, views); } /** @@ -267,7 +268,7 @@ private SchemaRegistry(AnnotationScannerContext context) { } Type type = Type.create(DotName.createSimple(className), Type.Kind.CLASS); - this.register(new TypeKey(type), schema, ((SchemaImpl) schema).getName()); + this.register(new TypeKey(type, Collections.emptySet()), schema, ((SchemaImpl) schema).getName()); ScannerLogging.logger.configSchemaRegistered(className); }); } @@ -279,12 +280,14 @@ private SchemaRegistry(AnnotationScannerContext context) { * * @param entityType * the type the {@link Schema} applies to + * @param views + * * @param schema * {@link Schema} to add to the registry * @return a reference to the newly registered {@link Schema} */ - public Schema register(Type entityType, Schema schema) { - TypeKey key = new TypeKey(entityType); + public Schema register(Type entityType, Set views, Schema schema) { + TypeKey key = new TypeKey(entityType, views); if (hasRef(key)) { // This is a replacement registration @@ -349,7 +352,7 @@ String deriveName(TypeKey key, String schemaName) { } String nameBase = schemaName != null ? schemaName : key.defaultName(); - String name = nameBase; + String name = nameBase + key.viewSuffix(); int idx = 1; while (this.names.contains(name)) { name = nameBase + idx++; @@ -358,20 +361,20 @@ String deriveName(TypeKey key, String schemaName) { return name; } - public Schema lookupRef(Type instanceType) { - return lookupRef(new TypeKey(instanceType)); + public Schema lookupRef(Type instanceType, Set views) { + return lookupRef(new TypeKey(instanceType, views)); } - public boolean hasRef(Type instanceType) { - return hasRef(new TypeKey(instanceType)); + public boolean hasRef(Type instanceType, Set views) { + return hasRef(new TypeKey(instanceType, views)); } - public Schema lookupSchema(Type instanceType) { - return lookupSchema(new TypeKey(instanceType)); + public Schema lookupSchema(Type instanceType, Set views) { + return lookupSchema(new TypeKey(instanceType, views)); } - public boolean hasSchema(Type instanceType) { - return hasSchema(new TypeKey(instanceType)); + public boolean hasSchema(Type instanceType, Set views) { + return hasSchema(new TypeKey(instanceType, views)); } public boolean isTypeRegistrationSupported(Type type, Schema schema) { @@ -431,12 +434,20 @@ private void remove(TypeKey key) { */ static class TypeKey { private final Type type; + private final Set views; private int hashCode = 0; - TypeKey(Type type) { + TypeKey(Type type, Set views) { this.type = type; + this.views = new LinkedHashSet<>(views); } + /* + * TypeKey(Type type) { + * this(type, Collections.emptySet()); + * } + */ + public String defaultName() { StringBuilder name = new StringBuilder(type.name().local()); @@ -454,6 +465,21 @@ public String defaultName() { return name.toString(); } + public String viewSuffix() { + if (views.isEmpty()) { + return ""; + } + + StringBuilder suffix = new StringBuilder(); + + for (Type view : views) { + suffix.append('_'); + suffix.append(view.name().local()); + } + + return suffix.toString(); + } + static void appendParameterNames(StringBuilder name, ParameterizedType type) { for (Type param : type.asParameterizedType().arguments()) { switch (param.kind()) { @@ -519,6 +545,10 @@ public boolean equals(Object o) { return false; } + if (!views.equals(other.views)) { + return false; + } + if (type instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) type; ParameterizedType otherType = (ParameterizedType) other.type; @@ -565,6 +595,7 @@ public int hashCode() { } hash = type.name().hashCode(); + hash = 31 * hash + views.hashCode(); if (type instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) type; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java index cb139e2b8..570723355 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/AnnotationTargetProcessor.java @@ -149,10 +149,17 @@ Schema processField() { if (typeSchema.getType() != SchemaType.ARRAY) { // Only register a reference to the type schema. The full schema will be added by subsequent // items on the stack (if not already present in the registry). - registeredTypeSchema = SchemaRegistry.registerReference(registrationType, typeResolver, typeSchema); + if (JandexUtil.isRef(schemaAnnotation)) { + registeredTypeSchema = null; + } else { + registeredTypeSchema = SchemaRegistry.registerReference(registrationType, context.getJsonViews(), + typeResolver, + typeSchema); + } } else { // Allow registration of arrays since we may not encounter a List again. - registeredTypeSchema = SchemaRegistry.checkRegistration(registrationType, typeResolver, typeSchema); + registeredTypeSchema = SchemaRegistry.checkRegistration(registrationType, context.getJsonViews(), typeResolver, + typeSchema); } } @@ -210,8 +217,11 @@ Schema processField() { } else { fieldSchema = registeredTypeSchema; // Reference to the type schema } - } else { - // Registration did not occur, overlay anything defined by the field on the type's schema + } else if (!JandexUtil.isRef(schemaAnnotation)) { + /* + * Registration did not occur and the user did not indicate this schema is a simple reference, + * overlay anything defined by the field on the type's schema + */ fieldSchema = MergeUtil.mergeObjects(typeSchema, fieldSchema); } @@ -311,7 +321,7 @@ private void setXmlName(Schema fieldSchema, String realName, AnnotationInstance * @return true if the schemas are not the same (i.e. registration occurred), otherwise false */ private boolean registrationSuccessful(Schema typeSchema, Schema registeredTypeSchema) { - return (typeSchema != registeredTypeSchema); + return (registeredTypeSchema != null && typeSchema != registeredTypeSchema); } private Schema readSchemaAnnotatedField(String propertyKey, AnnotationInstance annotation, Type postProcessedField) { diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java index c4c4d924f..e2b412d81 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeProcessor.java @@ -68,7 +68,7 @@ public Schema getSchema() { public Type processType() { // If it's a terminal type. if (isTerminalType(type)) { - SchemaRegistry.checkRegistration(type, typeResolver, schema); + SchemaRegistry.checkRegistration(type, context.getJsonViews(), typeResolver, schema); return type; } @@ -151,7 +151,7 @@ private Type readArrayType(ArrayType arrayType, Schema arraySchema) { pushToStack(componentType, itemSchema); } - itemSchema = SchemaRegistry.registerReference(componentType, typeResolver, itemSchema); + itemSchema = SchemaRegistry.registerReference(componentType, context.getJsonViews(), typeResolver, itemSchema); while (arrayType.dimensions() > 1) { Schema parentArrSchema = new SchemaImpl(); @@ -250,7 +250,7 @@ private Schema resolveParameterizedType(Type valueType, Schema schema, Schema pr Type resolved = resolveTypeVariable(propsSchema, valueType, true); if (index.containsClass(resolved)) { propsSchema.type(Schema.SchemaType.OBJECT); - propsSchema = SchemaRegistry.registerReference(valueType, typeResolver, propsSchema); + propsSchema = SchemaRegistry.registerReference(valueType, context.getJsonViews(), typeResolver, propsSchema); } } else if (index.containsClass(valueType)) { if (isA(valueType, ENUM_TYPE)) { @@ -262,7 +262,7 @@ private Schema resolveParameterizedType(Type valueType, Schema schema, Schema pr pushToStack(valueType, propsSchema); } - propsSchema = SchemaRegistry.registerReference(valueType, typeResolver, propsSchema); + propsSchema = SchemaRegistry.registerReference(valueType, context.getJsonViews(), typeResolver, propsSchema); } return propsSchema; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java index b724e7fa1..f203ea1e1 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolver.java @@ -493,11 +493,13 @@ public static Map getAllFields(AnnotationScannerContext co JandexUtil.fields(context, currentClass) .stream() .filter(TypeResolver::acceptField) + .filter(field -> isViewable(context, field)) .forEach(field -> scanField(context, properties, field, stack, reference, descendants)); methods(context, currentClass) .stream() .filter(TypeResolver::acceptMethod) + .filter(method -> isViewable(context, method)) .forEach(method -> scanMethod(context, properties, method, stack, reference, descendants)); JandexUtil.interfaces(index, currentClass) @@ -506,6 +508,7 @@ public static Map getAllFields(AnnotationScannerContext co .map(index::getClass) .filter(Objects::nonNull) .flatMap(clazz -> methods(context, clazz).stream()) + .filter(method -> isViewable(context, method)) .forEach(method -> scanMethod(context, properties, method, stack, reference, descendants)); descendants.add(currentClass); @@ -552,6 +555,22 @@ private static boolean isNonPublicOrAbsent(MethodInfo method) { return method == null || !Modifier.isPublic(method.flags()); } + private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) { + Set activeViews = context.getJsonViews(); + + if (activeViews.isEmpty()) { + return true; + } + + Type[] applicableViews = TypeUtil.getDeclaredAnnotationValue(propertySource, JacksonConstants.JSON_VIEW); + + if (applicableViews != null && applicableViews.length > 0) { + return Arrays.stream(applicableViews).anyMatch(activeViews::contains); + } + + return true; + } + /** * Determine if the target should be exposed in the API or ignored. Explicitly un-hiding a property * via the @Schema annotation overrides configuration to ignore the same property diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java index 5dc5ac690..b74104490 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScanner.java @@ -41,6 +41,7 @@ import org.jboss.jandex.Type.Kind; import io.smallrye.openapi.api.OpenApiConfig.OperationIdStrategy; +import io.smallrye.openapi.api.constants.JacksonConstants; import io.smallrye.openapi.api.constants.KotlinConstants; import io.smallrye.openapi.api.constants.OpenApiConstants; import io.smallrye.openapi.api.constants.SecurityConstants; @@ -66,6 +67,7 @@ import io.smallrye.openapi.runtime.io.tag.TagReader; import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension; import io.smallrye.openapi.runtime.scanner.ResourceParameters; +import io.smallrye.openapi.runtime.scanner.dataobject.AugmentedIndexView; import io.smallrye.openapi.runtime.scanner.processor.JavaSecurityProcessor; import io.smallrye.openapi.runtime.util.JandexUtil; import io.smallrye.openapi.runtime.util.ModelUtil; @@ -338,9 +340,33 @@ default Optional processOperation(final AnnotationScannerContext cont return Optional.of(operation); } + default void setJsonViewContext(AnnotationScannerContext context, Type[] views) { + clearJsonViewContext(context); + + if (views != null && views.length > 0) { + AugmentedIndexView index = context.getAugmentedIndex(); + + Arrays.stream(views) + .map(viewType -> { + if (index.containsClass(viewType)) { + return JandexUtil.inheritanceChain(index, index.getClass(viewType), viewType).values(); + } + return Collections.singleton(viewType); + }) + .flatMap(Collection::stream) + .forEach(context.getJsonViews()::add); + } + } + + default void clearJsonViewContext(AnnotationScannerContext context) { + context.getJsonViews().clear(); + } + default void processResponse(final AnnotationScannerContext context, final MethodInfo method, Operation operation, Map exceptionAnnotationMap) { + setJsonViewContext(context, TypeUtil.getDeclaredAnnotationValue(method, JacksonConstants.JSON_VIEW)); + List apiResponseAnnotations = ResponseReader.getResponseAnnotations(method); for (AnnotationInstance annotation : apiResponseAnnotations) { addApiReponseFromAnnotation(context, annotation, operation); @@ -374,6 +400,8 @@ default void processResponse(final AnnotationScannerContext context, final Metho } } } + + clearJsonViewContext(context); } /** @@ -842,6 +870,9 @@ default RequestBody processRequestBody(final AnnotationScannerContext context, // Only generate the request body schema if the @RequestBody is not a reference and no schema is yet specified if (requestBodyType != null && requestBody.getRef() == null) { + Type[] views = JandexUtil + .value(JandexUtil.getMethodParameterAnnotation(method, requestBodyType, JacksonConstants.JSON_VIEW)); + setJsonViewContext(context, views); if (!ModelUtil.requestBodyHasSchema(requestBody)) { requestBodyType = context.getResourceTypeResolver().resolve(requestBodyType); Schema schema = SchemaFactory.typeToSchema(context, requestBodyType, context.getExtensions()); @@ -876,6 +907,9 @@ && getConsumes(context) != null) { requestBodyType = context.getResourceTypeResolver().resolve(requestBodyType); if (requestBodyType != null && !isScannerInternalParameter(requestBodyType)) { + Type[] views = JandexUtil.value( + JandexUtil.getMethodParameterAnnotation(method, requestBodyType, JacksonConstants.JSON_VIEW)); + setJsonViewContext(context, views); Schema schema = null; if (isMultipartInput(requestBodyType)) { @@ -899,6 +933,9 @@ && getConsumes(context) != null) { } } } + + clearJsonViewContext(context); + return requestBody; } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java index e0c7fa392..b0fecb839 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AnnotationScannerContext.java @@ -3,7 +3,9 @@ import java.util.ArrayDeque; import java.util.Collections; import java.util.Deque; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.function.UnaryOperator; import org.eclipse.microprofile.openapi.models.OpenAPI; @@ -35,6 +37,7 @@ public class AnnotationScannerContext { private final OpenAPI openApi; private final Deque scanStack = new ArrayDeque<>(); private Deque resolverStack = new ArrayDeque<>(); + private final Set jsonViews = new LinkedHashSet<>(); public AnnotationScannerContext(FilteredIndexView index, ClassLoader classLoader, List extensions, @@ -99,4 +102,7 @@ public TypeResolver getResourceTypeResolver() { return resolverStack.peek(); } + public Set getJsonViews() { + return jsonViews; + } } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java index 5b604dc15..2be5a13bd 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/JandexUtil.java @@ -137,6 +137,10 @@ public static String refValue(AnnotationInstance annotation, RefType refType) { return ref; } + public static T value(AnnotationInstance annotation) { + return annotation != null ? value(annotation, OpenApiConstants.VALUE) : null; + } + /** * Convenience method to retrieve the named parameter from an annotation. * The value will be unwrapped from its containing {@link AnnotationValue}. @@ -340,7 +344,7 @@ public static > T enumValue(String strVal, Class clazz) { * @return Whether it's a "ref" */ public static boolean isRef(AnnotationInstance annotation) { - return annotation.value(OpenApiConstants.REF) != null; + return annotation != null && annotation.value(OpenApiConstants.REF) != null; } /** @@ -565,14 +569,28 @@ public static Type getMethodParameterType(MethodParameterInfo parameter) { */ public static AnnotationInstance getMethodParameterAnnotation(MethodInfo method, int parameterIndex, DotName annotationName) { - for (AnnotationInstance annotation : method.annotations()) { - if (annotation.target().kind() == Kind.METHOD_PARAMETER && - annotation.target().asMethodParameter().position() == parameterIndex && - annotation.name().equals(annotationName)) { - return annotation; - } - } - return null; + return method.annotations(annotationName) + .stream() + .filter(annotation -> annotation.target().kind() == Kind.METHOD_PARAMETER) + .filter(annotation -> annotation.target().asMethodParameter().position() == parameterIndex) + .findFirst() + .orElse(null); + } + + /** + * Finds an annotation (if present) with the given name, on a particular parameter of a + * method based on the identity of the parameterType. Returns null if not found. + * + * @param method the method + * @param parameterType the parameter type + * @param annotationName name of annotation we are looking for + * @return the Annotation instance + */ + public static AnnotationInstance getMethodParameterAnnotation(MethodInfo method, Type parameterType, + DotName annotationName) { + // parameterType must be the same object as in the method's parameter type array + int parameterIndex = method.parameterTypes().indexOf(parameterType); + return getMethodParameterAnnotation(method, parameterIndex, annotationName); } public static List schemaDisplayValues(AnnotationInstance annotation) { diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java index 2c41402f3..8376f93f4 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -852,6 +853,58 @@ public static Collection getAnnotations(AnnotationTarget typ return Collections.emptyList(); } + public static T getDeclaredAnnotationValue(AnnotationTarget type, DotName annotationName, String propertyName) { + AnnotationInstance annotation = getDeclaredAnnotation(type, annotationName); + T value = null; + + if (annotation != null) { + value = JandexUtil.value(annotation, propertyName); + } + + return value; + } + + public static T getDeclaredAnnotationValue(AnnotationTarget type, DotName annotationName) { + return getDeclaredAnnotationValue(type, annotationName, OpenApiConstants.VALUE); + } + + public static AnnotationInstance getDeclaredAnnotation(AnnotationTarget type, DotName annotationName) { + Function lookup; + + switch (type.kind()) { + case CLASS: + lookup = type.asClass()::classAnnotation; + break; + case FIELD: + lookup = type.asField()::annotation; + break; + case METHOD: + lookup = name -> type.asMethod().annotations(name).stream().filter(a -> type.equals(a.target())).findFirst() + .orElse(null); + break; + case METHOD_PARAMETER: + MethodParameterInfo parameter = type.asMethodParameter(); + lookup = name -> parameter + .method() + .annotations(name) + .stream() + .filter(a -> a.target().kind() == Kind.METHOD_PARAMETER) + .filter(a -> a.target().asMethodParameter().position() == parameter.position()) + .findFirst() + .orElse(null); + break; + case RECORD_COMPONENT: + lookup = type.asRecordComponent()::annotation; + break; + case TYPE: + default: + lookup = name -> null; + break; + } + + return lookup.apply(annotationName); + } + public static ClassInfo getDeclaringClass(AnnotationTarget type) { switch (type.kind()) { case FIELD: diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ExpectationWithRefsTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ExpectationWithRefsTests.java index 42126826a..d3cf700e2 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ExpectationWithRefsTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ExpectationWithRefsTests.java @@ -1,6 +1,7 @@ package io.smallrye.openapi.runtime.scanner; import java.io.IOException; +import java.util.Collections; import org.eclipse.microprofile.openapi.models.media.Schema; import org.jboss.jandex.ClassType; @@ -37,7 +38,7 @@ private void testAssertion(Class target, String expectedResourceName) throws OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, type); Schema result = scanner.process(); - registry.register(type, result); + registry.register(type, Collections.emptySet(), result); printToConsole(oai); assertJsonEquals(expectedResourceName, oai); @@ -53,7 +54,7 @@ private void testAssertion(Class containerClass, OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, parentType); Schema result = scanner.process(); - registry.register(parentType, result); + registry.register(parentType, Collections.emptySet(), result); printToConsole(oai); assertJsonEquals(expectedResourceName, oai); diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java index 415cf5f67..7edef62b4 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JaxRsAnnotationScannerTest.java @@ -3,6 +3,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; import java.util.HashMap; import java.util.UUID; import java.util.zip.GZIPInputStream; @@ -397,7 +398,7 @@ public void registerCustomSchemas(SchemaRegistry schemaRegistry) { schema.setTitle("UUID"); schema.setDescription("Universally Unique Identifier"); schema.setExample("de8681db-b4d6-4c47-a428-4b959c1c8e9a"); - schemaRegistry.register(uuidType, schema); + schemaRegistry.register(uuidType, Collections.emptySet(), schema); } } diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java new file mode 100644 index 000000000..d066c9551 --- /dev/null +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/JsonViewTests.java @@ -0,0 +1,97 @@ +package io.smallrye.openapi.runtime.scanner; + +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.jboss.jandex.Index; +import org.junit.jupiter.api.Test; + +import io.smallrye.openapi.api.OpenApiConfig; +import io.smallrye.openapi.api.OpenApiDocument; +import io.smallrye.openapi.api.constants.OpenApiConstants; + +class JsonViewTests extends IndexScannerTestBase { + + @Test + void testJsonViewSchemasPresent() throws Exception { + class Views { + class Public { + } + + class Internal extends Public { + } + + class WriteOnly extends Public { + } + } + + @Schema(name = "Inner2") + class InnerBean2 { + @Schema + String value; + } + + @Schema(name = "Inner1") + class InnerBean1 { + @Schema + String value; + @Schema(ref = "Inner2") + InnerBean2 inner2; + } + + @Schema(name = "BeanName") + class Bean { + @com.fasterxml.jackson.annotation.JsonView(Views.Internal.class) + String id; + @com.fasterxml.jackson.annotation.JsonView(Views.Public.class) + String name; + @com.fasterxml.jackson.annotation.JsonView(Views.WriteOnly.class) + String secret; + @Schema + InnerBean1 inner1; + } + + @jakarta.ws.rs.Path("/item/{id}") + class TestResource { + @jakarta.ws.rs.GET + @jakarta.ws.rs.Path("internal") + @jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + @com.fasterxml.jackson.annotation.JsonView(Views.Internal.class) + public Bean getInternal() { + return null; + } + + @jakarta.ws.rs.GET + @jakarta.ws.rs.Path("public") + @jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + @com.fasterxml.jackson.annotation.JsonView(Views.Public.class) + public java.util.concurrent.CompletionStage getPublic() { + return null; + } + + @jakarta.ws.rs.POST + @jakarta.ws.rs.Path("public") + @jakarta.ws.rs.Consumes(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + @jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + @com.fasterxml.jackson.annotation.JsonView(Views.Public.class) + public java.util.concurrent.CompletionStage updatePublic( + @com.fasterxml.jackson.annotation.JsonView(Views.WriteOnly.class) Bean modified) { + return null; + } + } + + Index index = Index.of(Views.Public.class, Views.WriteOnly.class, Views.Internal.class, Bean.class, InnerBean1.class, + InnerBean2.class, TestResource.class); + OpenApiConfig config = dynamicConfig(OpenApiConstants.SMALLRYE_REMOVE_UNUSED_SCHEMAS, Boolean.TRUE); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index); + + OpenApiDocument document = OpenApiDocument.newInstance(); + document.reset(); + document.config(config); + document.modelFromAnnotations(scanner.scan()); + document.initialize(); + + OpenAPI result = document.get(); + printToConsole(result); + assertJsonEquals("special.jsonview-schemas-basic.json", result); + } +} diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/KitchenSinkTest.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/KitchenSinkTest.java index c8b379e67..4f559e67b 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/KitchenSinkTest.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/KitchenSinkTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.io.IOException; +import java.util.Collections; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.eclipse.microprofile.openapi.models.media.Schema; @@ -66,7 +67,7 @@ void testKitchenSinkWithRefs() throws IOException, JSONException { SchemaRegistry registry = SchemaRegistry.newInstance(context); Schema result = scanner.process(); - registry.register(type, result); + registry.register(type, Collections.emptySet(), result); printToConsole(oai); assertJsonEquals("refsEnabled.kitchenSink.expected.json", oai); diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/NestedSchemaReferenceTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/NestedSchemaReferenceTests.java index d1853bd75..0296b38df 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/NestedSchemaReferenceTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/NestedSchemaReferenceTests.java @@ -1,6 +1,7 @@ package io.smallrye.openapi.runtime.scanner; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import org.eclipse.microprofile.openapi.models.OpenAPI; @@ -29,7 +30,7 @@ void testNestedSchemasAddedToRegistry() throws IOException, JSONException { OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, parentType); Schema result = scanner.process(); - registry.register(parentType, result); + registry.register(parentType, Collections.emptySet(), result); printToConsole(oai); assertJsonEquals("refsEnabled.nested.schema.family.expected.json", oai); diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/SchemaRegistryTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/SchemaRegistryTests.java index d9ba34061..6ad81336b 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/SchemaRegistryTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/SchemaRegistryTests.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; +import java.util.Collections; import org.eclipse.microprofile.openapi.models.media.Schema; import org.jboss.jandex.ClassInfo; @@ -42,9 +43,9 @@ void testParameterizedNameCollisionsUseSequence() throws IOException, JSONExcept FieldInfo n2 = cInfo.field("n2"); FieldInfo n3 = cInfo.field("n3"); - Schema s1 = registry.register(n1.type(), new SchemaImpl()); - Schema s2 = registry.register(n2.type(), new SchemaImpl()); - Schema s3 = registry.register(n3.type(), new SchemaImpl()); + Schema s1 = registry.register(n1.type(), Collections.emptySet(), new SchemaImpl()); + Schema s2 = registry.register(n2.type(), Collections.emptySet(), new SchemaImpl()); + Schema s3 = registry.register(n3.type(), Collections.emptySet(), new SchemaImpl()); assertEquals("#/components/schemas/NestableStringNestableStringString", s1.getRef()); assertEquals("#/components/schemas/NestableStringNestableStringObject", s2.getRef()); @@ -66,7 +67,7 @@ void testWildcardLowerBoundName() throws IOException, JSONException { ClassInfo cInfo = index.getClassByName(cName); FieldInfo n4 = cInfo.field("n4"); - Schema s4 = registry.register(n4.type(), new SchemaImpl()); + Schema s4 = registry.register(n4.type(), Collections.emptySet(), new SchemaImpl()); assertEquals("#/components/schemas/NestableStringSuperInteger", s4.getRef()); } @@ -85,7 +86,7 @@ void testWildcardUpperBoundName() throws IOException, JSONException { ClassInfo cInfo = index.getClassByName(cName); FieldInfo n5 = cInfo.field("n5"); - Schema s5 = registry.register(n5.type(), new SchemaImpl()); + Schema s5 = registry.register(n5.type(), Collections.emptySet(), new SchemaImpl()); assertEquals("#/components/schemas/NestableExtendsCharSequenceExtendsNumber", s5.getRef()); } @@ -105,7 +106,7 @@ void testWildcardWithGivenName() throws IOException, JSONException { ClassInfo cInfo = index.getClassByName(cName); FieldInfo n6 = cInfo.field("n6"); - Schema s6 = registry.register(n6.type(), new SchemaImpl()); + Schema s6 = registry.register(n6.type(), Collections.emptySet(), new SchemaImpl()); assertEquals("#/components/schemas/n6", s6.getRef()); } @@ -128,7 +129,7 @@ void testNestedGenericWildcard() throws IOException, JSONException { OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, n6Type); Schema result = scanner.process(); - registry.register(n6Type, result); + registry.register(n6Type, Collections.emptySet(), result); printToConsole(context.getOpenApi()); String field3SchemaName = ModelUtil.nameFromRef(result.getProperties().get("field3").getRef()); diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-schemas-basic.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-schemas-basic.json new file mode 100644 index 000000000..4571d1c54 --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-schemas-basic.json @@ -0,0 +1,148 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Generated API", + "version": "1.0" + }, + "paths": { + "/item/{id}/internal": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BeanName_Internal_Public" + } + } + } + } + } + } + }, + "/item/{id}/public": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BeanName_Public" + } + } + } + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BeanName_WriteOnly_Public" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BeanName_Public" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BeanName_Internal_Public": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "inner1": { + "$ref": "#/components/schemas/Inner1_Internal_Public" + } + } + }, + "BeanName_Public": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "inner1": { + "$ref": "#/components/schemas/Inner1_Public" + } + } + }, + "BeanName_WriteOnly_Public": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "inner1": { + "$ref": "#/components/schemas/Inner1_WriteOnly_Public" + } + } + }, + "Inner1_Internal_Public": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "inner2": { + "$ref": "#/components/schemas/Inner2" + } + } + }, + "Inner1_Public": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "inner2": { + "$ref": "#/components/schemas/Inner2" + } + } + }, + "Inner1_WriteOnly_Public": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "inner2": { + "$ref": "#/components/schemas/Inner2" + } + } + }, + "Inner2": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file