diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/MapModelIO.java b/core/src/main/java/io/smallrye/openapi/runtime/io/MapModelIO.java index 6dffb28b1..2b1b47bf2 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/MapModelIO.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/MapModelIO.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; @@ -65,6 +66,9 @@ public Map readMap(Collection annotations, protected Map readMap(Collection annotations, Function> nameFn, BiFunction reader) { + if (annotations.isEmpty()) { + return new LinkedHashMap<>(0); + } IoLogging.logger.annotationsMap('@' + annotationName.local()); return annotations.stream() .map(annotation -> nameFn.apply(annotation).map(name -> entry(name, reader.apply(name, annotation)))) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/media/DiscriminatorIO.java b/core/src/main/java/io/smallrye/openapi/runtime/io/media/DiscriminatorIO.java index 1d53f9fd6..086f91b2f 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/media/DiscriminatorIO.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/media/DiscriminatorIO.java @@ -39,7 +39,6 @@ public DiscriminatorIO(IOContext context) { */ @Override public Discriminator read(AnnotationInstance annotation) { - IoLogging.logger.singleAnnotationAs("@Schema", "Discriminator"); String propertyName = value(annotation, SchemaConstant.PROP_DISCRIMINATOR_PROPERTY); AnnotationInstance[] mapping = value(annotation, SchemaConstant.PROP_DISCRIMINATOR_MAPPING); @@ -47,6 +46,7 @@ public Discriminator read(AnnotationInstance annotation) { return null; } + IoLogging.logger.singleAnnotationAs("@Schema", "Discriminator"); Discriminator discriminator = OASFactory.createDiscriminator(); /* diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/servers/ServerIO.java b/core/src/main/java/io/smallrye/openapi/runtime/io/servers/ServerIO.java index a13372346..2a758eadc 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/servers/ServerIO.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/servers/ServerIO.java @@ -44,6 +44,9 @@ public List readList(AnnotationInstance[] annotations) { } public List readList(Collection annotations) { + if (annotations.isEmpty()) { + return new ArrayList<>(0); + } IoLogging.logger.annotationsArray("@Server"); return annotations.stream() .map(this::read) 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 3b4099baa..8df1fd43d 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 @@ -65,6 +65,7 @@ public class SchemaRegistry { * * @param type * the {@link Type} the {@link Schema} applies to + * @param views types applied to the currently-active JsonView (Jackson annotation) * @param resolver * a {@link TypeResolver} that will be used to resolve * parameterized and wildcard types @@ -73,7 +74,7 @@ public class SchemaRegistry { * @return the same schema if not eligible for registration, or a reference * to the schema registered for the given Type */ - public Schema checkRegistration(Type type, Set views, TypeResolver resolver, Schema schema) { + public Schema checkRegistration(Type type, Map views, TypeResolver resolver, Schema schema) { return register(type, views, resolver, schema, (reg, key) -> reg.register(key, schema, null)); } @@ -99,6 +100,7 @@ public Schema checkRegistration(Type type, Set views, TypeResolver resolve * * @param type * the {@link Type} the {@link Schema} applies to + * @param views types applied to the currently-active JsonView (Jackson annotation) * @param resolver * a {@link TypeResolver} that will be used to resolve * parameterized and wildcard types @@ -107,11 +109,11 @@ public Schema checkRegistration(Type type, Set views, TypeResolver resolve * @return the same schema if not eligible for registration, or a reference * to the schema registered for the given Type */ - public Schema registerReference(Type type, Set views, TypeResolver resolver, Schema schema) { + public Schema registerReference(Type type, Map views, TypeResolver resolver, Schema schema) { return register(type, views, resolver, schema, SchemaRegistry::registerReference); } - public Schema register(Type type, Set views, TypeResolver resolver, Schema schema, + public Schema register(Type type, Map views, TypeResolver resolver, Schema schema, BiFunction registrationAction) { final Type resolvedType = TypeResolver.resolve(type, resolver); @@ -130,7 +132,7 @@ public Schema register(Type type, Set views, TypeResolver resolver, Schema return schema; } - TypeKey key = new TypeKey(resolvedType, views); + TypeKey key = keyFor(resolvedType, views); if (hasRef(key)) { schema = lookupRef(key); @@ -153,7 +155,7 @@ public Schema register(Type type, Set views, TypeResolver resolver, 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 boolean hasSchema(Type type, Set views, TypeResolver resolver) { + public boolean hasSchema(Type type, Map views, TypeResolver resolver) { if (disabled) { return false; } @@ -235,11 +237,20 @@ public SchemaRegistry(AnnotationScannerContext context) { return; } - this.register(new TypeKey(type, Collections.emptySet()), schema, Extensions.getName(schema)); + this.register(keyFor(type, Collections.emptyMap()), schema, Extensions.getName(schema)); ScannerLogging.logger.configSchemaRegistered(typeSignature); }); } + private static TypeKey keyFor(Type type, Map views) { + if (TypeUtil.knownJavaType(type.name())) { + // do not apply views for JDK types + return new TypeKey(type, Collections.emptyMap()); + } + + return new TypeKey(type, views); + } + /** * Register the provided {@link Schema} for the provided {@link Type}. If an * existing schema has already been registered for the type, it will be @@ -253,8 +264,8 @@ public SchemaRegistry(AnnotationScannerContext context) { * {@link Schema} to add to the registry * @return a reference to the newly registered {@link Schema} */ - public Schema register(Type entityType, Set views, Schema schema) { - TypeKey key = new TypeKey(entityType, views); + public Schema register(Type entityType, Map views, Schema schema) { + TypeKey key = keyFor(entityType, views); if (hasRef(key)) { // This is a replacement registration @@ -324,20 +335,20 @@ String deriveName(TypeKey key, String schemaName) { return name; } - public Schema lookupRef(Type instanceType, Set views) { - return lookupRef(new TypeKey(instanceType, views)); + public Schema lookupRef(Type instanceType, Map views) { + return lookupRef(keyFor(instanceType, views)); } - public boolean hasRef(Type instanceType, Set views) { - return hasRef(new TypeKey(instanceType, views)); + public boolean hasRef(Type instanceType, Map views) { + return hasRef(keyFor(instanceType, views)); } - public Schema lookupSchema(Type instanceType, Set views) { - return lookupSchema(new TypeKey(instanceType, views)); + public Schema lookupSchema(Type instanceType, Map views) { + return lookupSchema(keyFor(instanceType, views)); } - public boolean hasSchema(Type instanceType, Set views) { - return hasSchema(new TypeKey(instanceType, views)); + public boolean hasSchema(Type instanceType, Map views) { + return hasSchema(keyFor(instanceType, views)); } public boolean isTypeRegistrationSupported(Type type, Schema schema) { @@ -398,20 +409,14 @@ private void remove(TypeKey key) { */ public static final class TypeKey { private final Type type; - private final Set views; + private final Map views; private int hashCode = 0; - TypeKey(Type type, Set views) { + TypeKey(Type type, Map views) { this.type = type; - this.views = new LinkedHashSet<>(views); + this.views = new LinkedHashMap<>(views); } - /* - * TypeKey(Type type) { - * this(type, Collections.emptySet()); - * } - */ - public String defaultName() { StringBuilder name = new StringBuilder(type.name().local()); @@ -436,9 +441,12 @@ public String viewSuffix() { StringBuilder suffix = new StringBuilder(); - for (Type view : views) { - suffix.append('_'); - suffix.append(view.name().local()); + for (Map.Entry view : views.entrySet()) { + if (Boolean.TRUE.equals(view.getValue())) { + // Only views that are directly specified contribute to the schema's name + suffix.append('_'); + suffix.append(view.getKey().name().local()); + } } return suffix.toString(); 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 53e840f2e..3d8c7a593 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 @@ -159,6 +159,7 @@ Schema processField() { // The registeredTypeSchema will be a reference to typeSchema if registration occurs registrationType = TypeUtil.isWrappedType(entityType) ? fieldType : entityType; registrationCandidate = !JandexUtil.isRef(schemaAnnotation) && + typeProcessor.allowRegistration() && schemaRegistry.register(registrationType, context.getJsonViews(), typeResolver, initTypeSchema, (reg, key) -> null) != initTypeSchema; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java index c38521e24..bb799f878 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/dataobject/IgnoreResolver.java @@ -2,16 +2,15 @@ import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; -import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -55,10 +54,10 @@ public enum Visibility { UNSET } - public Visibility isIgnore(Map properties, + public Visibility isIgnore(String propertyName, AnnotationTarget annotationTarget, AnnotationTarget reference) { for (IgnoreAnnotationHandler handler : ignoreHandlers) { - Visibility v = handler.shouldIgnore(properties, annotationTarget, reference); + Visibility v = handler.visibility(propertyName, annotationTarget, reference); if (v != Visibility.UNSET) { return v; @@ -67,6 +66,27 @@ public Visibility isIgnore(Map properties, return Visibility.UNSET; } + public Visibility referenceVisibility(String propertyName, + AnnotationTarget annotationTarget, AnnotationTarget reference) { + for (IgnoreAnnotationHandler handler : ignoreHandlers) { + Visibility v = handler.referenceVisibility(propertyName, annotationTarget, reference); + + if (v != Visibility.UNSET) { + return v; + } + } + return Visibility.UNSET; + } + + public boolean configuresVisibility(AnnotationTarget reference) { + for (IgnoreAnnotationHandler handler : ignoreHandlers) { + if (handler.configuresVisibility(reference)) { + return true; + } + } + return false; + } + public Visibility getDescendantVisibility(String propertyName, List descendants) { for (IgnoreAnnotationHandler handler : ignoreHandlers) { Visibility v = handler.getDescendantVisibility(propertyName, descendants); @@ -84,8 +104,7 @@ public Visibility getDescendantVisibility(String propertyName, List d */ private final class SchemaHiddenHandler implements IgnoreAnnotationHandler { @Override - public Visibility shouldIgnore(Map properties, AnnotationTarget target, - AnnotationTarget reference) { + public Visibility visibility(AnnotationTarget target, AnnotationTarget reference) { AnnotationInstance annotationInstance = context.annotations().getAnnotation(target, getNames()); if (annotationInstance != null) { Boolean hidden = context.annotations().value(annotationInstance, SchemaConstant.PROP_HIDDEN); @@ -108,8 +127,7 @@ public List getNames() { */ private final class JsonbTransientHandler implements IgnoreAnnotationHandler { @Override - public Visibility shouldIgnore(Map properties, AnnotationTarget target, - AnnotationTarget reference) { + public Visibility visibility(AnnotationTarget target, AnnotationTarget reference) { return context.annotations().hasAnnotation(target, getNames()) ? Visibility.IGNORED : Visibility.UNSET; } @@ -125,15 +143,24 @@ public List getNames() { private final class JsonIgnorePropertiesHandler implements IgnoreAnnotationHandler { @Override - public Visibility shouldIgnore(Map properties, AnnotationTarget target, - AnnotationTarget reference) { - Visibility visibility = declaringClassIgnore(properties, target); + public Visibility visibility(String propertyName, AnnotationTarget target, AnnotationTarget reference) { + Visibility visibility = declaringClassIgnore(propertyName, target); if (visibility != Visibility.UNSET) { return visibility; } - return nestingPropertyIgnore(reference, propertyName(properties, target)); + return nestingPropertyIgnore(reference, propertyName); + } + + @Override + public Visibility referenceVisibility(String propertyName, AnnotationTarget target, AnnotationTarget reference) { + return nestingPropertyIgnore(reference, propertyName); + } + + @Override + public boolean configuresVisibility(AnnotationTarget reference) { + return context.annotations().hasAnnotation(reference, getNames()); } /** @@ -151,10 +178,10 @@ public Visibility shouldIgnore(Map properties, AnnotationT * @param target * @return */ - private Visibility declaringClassIgnore(Map properties, AnnotationTarget target) { + private Visibility declaringClassIgnore(String propertyName, AnnotationTarget target) { AnnotationInstance declaringClassJIP = context.annotations().getAnnotation(TypeUtil.getDeclaringClass(target), getNames()); - return shouldIgnoreTarget(declaringClassJIP, propertyName(properties, target)); + return shouldIgnoreTarget(declaringClassJIP, propertyName); } /** @@ -186,14 +213,6 @@ private Visibility nestingPropertyIgnore(AnnotationTarget nesting, String proper return shouldIgnoreTarget(nestedTypeJIP, propertyName); } - private String propertyName(Map properties, AnnotationTarget target) { - if (target.kind() == Kind.FIELD) { - return target.asField().name(); - } - // Assuming this is a getter or setter - return TypeResolver.propertyName(properties, target.asMethod()); - } - private Visibility shouldIgnoreTarget(AnnotationInstance jipAnnotation, String targetName) { if (jipAnnotation == null || jipAnnotation.value() == null) { return Visibility.UNSET; @@ -230,10 +249,8 @@ public Visibility getDescendantVisibility(String propertyName, List d * Handler for Jackson's @{@link com.fasterxml.jackson.annotation.JsonIgnore JsonIgnore} */ private final class JsonIgnoreHandler implements IgnoreAnnotationHandler { - @Override - public Visibility shouldIgnore(Map properties, AnnotationTarget target, - AnnotationTarget reference) { + public Visibility visibility(AnnotationTarget target, AnnotationTarget reference) { AnnotationInstance annotationInstance = context.annotations().getAnnotation(target, getNames()); if (annotationInstance != null && valueAsBooleanOrTrue(annotationInstance)) { return Visibility.IGNORED; @@ -254,8 +271,7 @@ private final class JsonIgnoreTypeHandler implements IgnoreAnnotationHandler { private final Set ignoredTypes = new LinkedHashSet<>(); @Override - public Visibility shouldIgnore(Map properties, AnnotationTarget target, - AnnotationTarget reference) { + public Visibility visibility(AnnotationTarget target, AnnotationTarget reference) { Type classType; switch (target.kind()) { @@ -316,8 +332,7 @@ public List getNames() { private final class TransientIgnoreHandler implements IgnoreAnnotationHandler { @Override - public Visibility shouldIgnore(Map properties, AnnotationTarget target, - AnnotationTarget reference) { + public Visibility visibility(AnnotationTarget target, AnnotationTarget reference) { if (target.kind() == AnnotationTarget.Kind.FIELD) { FieldInfo field = target.asField(); // If field has transient modifier, e.g. `transient String foo;`, then hide it. @@ -344,8 +359,7 @@ public List getNames() { private final class JaxbAccessibilityHandler implements IgnoreAnnotationHandler { @Override - public Visibility shouldIgnore(Map properties, AnnotationTarget target, - AnnotationTarget reference) { + public Visibility visibility(AnnotationTarget target, AnnotationTarget reference) { if (hasXmlTransient(target)) { return Visibility.IGNORED; } @@ -406,11 +420,6 @@ Visibility getXmlVisibility(ClassInfo declaringClass, String accessTypeRequired, return Visibility.IGNORED; } - - @Override - public List getNames() { - return null; - } } private boolean valueAsBooleanOrTrue(AnnotationInstance annotation) { @@ -420,11 +429,29 @@ private boolean valueAsBooleanOrTrue(AnnotationInstance annotation) { } private interface IgnoreAnnotationHandler { - Visibility shouldIgnore(Map properties, - AnnotationTarget target, - AnnotationTarget reference); + default Visibility visibility(String propertyName, AnnotationTarget target, AnnotationTarget reference) { + return visibility(target, reference); + } + + default Visibility visibility(AnnotationTarget target, AnnotationTarget reference) { + return Visibility.UNSET; + } + + default Visibility referenceVisibility(String propertyName, AnnotationTarget target, AnnotationTarget reference) { + return Visibility.UNSET; + } - List getNames(); + /** + * Checks whether the given reference (method/field) specifies visible/ignored + * properties on the type that is referenced. + */ + default boolean configuresVisibility(AnnotationTarget reference) { + return false; + } + + default List getNames() { + return Collections.emptyList(); + } default Visibility getDescendantVisibility(String propertyName, List descendants) { return Visibility.UNSET; 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 1c8b08ece..bd75f5021 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 @@ -81,7 +81,9 @@ public Schema getSchema() { public Type processType() { // If it's a terminal type. if (isTerminalType(type)) { - context.getSchemaRegistry().checkRegistration(type, context.getJsonViews(), typeResolver, schema); + if (allowRegistration()) { + context.getSchemaRegistry().checkRegistration(type, context.getJsonViews(), typeResolver, schema); + } return type; } @@ -148,6 +150,34 @@ public Type processType() { return type; } + /** + * Only allow registration of a type if the annotation target (field or method) that + * refers to the type's class is not annotated with an annotation that alters + * the visibility of fields in the class. + * + *

+ * For example, in this scenario when we process `fieldB`, registration of class B's + * schema will not occur because it's definition is altered by and specific to its + * use in class A. + * + *

+     * 
+     * class A {
+     *   @JsonIgnoreProperties({"field2"})
+     *   B fieldB;
+     * }
+     *
+     * class B {
+     *   int field1;
+     *   int field2;
+     * }
+     * 
+     * 
+ */ + public boolean allowRegistration() { + return !context.getIgnoreResolver().configuresVisibility(annotationTarget); + } + private Type readArrayType(ArrayType arrayType, Schema arraySchema) { DataObjectLogging.logger.processingArray(arrayType); @@ -168,9 +198,12 @@ private Type readArrayType(ArrayType arrayType, Schema arraySchema) { if (!isTerminalType(componentType) && index.containsClass(componentType)) { // If it's not a terminal type, then push for later inspection. pushToStack(componentType, itemSchema); - itemSchema = context.getSchemaRegistry().registerReference(componentType, context.getJsonViews(), typeResolver, - itemSchema); - } else { + + if (allowRegistration()) { + itemSchema = context.getSchemaRegistry().registerReference(componentType, context.getJsonViews(), typeResolver, + itemSchema); + } + } else if (allowRegistration()) { // Otherwise, allow registration since we may not encounter the array's element type again. itemSchema = context.getSchemaRegistry().checkRegistration(componentType, context.getJsonViews(), typeResolver, itemSchema); @@ -306,11 +339,15 @@ private Schema resolveParameterizedType(Type valueType, Schema propsSchema) { SchemaRegistry registry = context.getSchemaRegistry(); if (registry.hasSchema(valueType, context.getJsonViews(), typeResolver)) { - propsSchema = registry.lookupRef(valueType, context.getJsonViews()); + if (allowRegistration()) { + propsSchema = registry.lookupRef(valueType, context.getJsonViews()); + } } else { pushToStack(valueType, propsSchema); - propsSchema = registry.registerReference(valueType, context.getJsonViews(), typeResolver, - propsSchema); + if (allowRegistration()) { + propsSchema = registry.registerReference(valueType, context.getJsonViews(), typeResolver, + 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 28ebc2701..8388d4611 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 @@ -41,6 +41,7 @@ import io.smallrye.openapi.api.constants.JaxbConstants; import io.smallrye.openapi.api.constants.JsonbConstants; import io.smallrye.openapi.runtime.io.schema.SchemaConstant; +import io.smallrye.openapi.runtime.scanner.dataobject.IgnoreResolver.Visibility; import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext; import io.smallrye.openapi.runtime.util.Annotations; import io.smallrye.openapi.runtime.util.JandexUtil; @@ -581,7 +582,7 @@ private static boolean isNonPublicOrAbsent(MethodInfo method) { } private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) { - Set activeViews = context.getJsonViews(); + Map activeViews = context.getJsonViews(); if (activeViews.isEmpty()) { return true; @@ -590,7 +591,7 @@ private static boolean isViewable(AnnotationScannerContext context, AnnotationTa Type[] applicableViews = getJsonViews(context, propertySource); if (applicableViews != null && applicableViews.length > 0) { - return Arrays.stream(applicableViews).anyMatch(activeViews::contains); + return Arrays.stream(applicableViews).anyMatch(activeViews::containsKey); } return true; @@ -629,23 +630,21 @@ private static Type[] getJsonViews(AnnotationScannerContext context, AnnotationT * @param target the field or method to be checked for ignoring or exposure in the API * @param reference an annotated member (field or method) that referenced the type of target's declaring class * @param descendants list of classes that descend from the class containing target - * @param properties map of other known properties that are peers of the target */ private void processVisibility(AnnotationScannerContext context, AnnotationTarget target, AnnotationTarget reference, - List descendants, - Map properties) { + List descendants) { if (this.exposed || this.ignored) { // @Schema with hidden = false OR ignored somehow by a member lower in the class hierarchy return; } - if (this.isUnhidden(target)) { + if (this.isUnhidden(target, reference)) { // @Schema with hidden = false and not already ignored by a member lower in the class hierarchy this.exposed = true; return; } - switch (getVisibility(context, target, reference, descendants, properties)) { + switch (getVisibility(context, target, reference, descendants, propertyName)) { case EXPOSED: this.exposed = true; break; @@ -705,7 +704,7 @@ private void processAccess(AnnotationTarget target) { private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context, AnnotationTarget target, AnnotationTarget reference, List descendants, - Map properties) { + String propertyName) { if (!isViewable(context, target)) { return IgnoreResolver.Visibility.IGNORED; @@ -716,7 +715,7 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context IgnoreResolver.Visibility visibility = ignoreResolver.getDescendantVisibility(propertyName, descendants); if (visibility == IgnoreResolver.Visibility.UNSET) { - visibility = ignoreResolver.isIgnore(properties, target, reference); + visibility = ignoreResolver.isIgnore(propertyName, target, reference); } return visibility; @@ -724,11 +723,12 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context /** * Determines whether the target is set to hidden = false via the @Schema - * annotation (explicit or default value). + * annotation (explicit or default value). A field is only considered un-hidden if its + * visibility is not set to ignored by the referencing annotation target. * * @return true if the field is un-hidden, false otherwise */ - boolean isUnhidden(AnnotationTarget target) { + boolean isUnhidden(AnnotationTarget target, AnnotationTarget reference) { if (target != null) { AnnotationInstance schemaAnnotation = TypeUtil.getSchemaAnnotation(context, target); @@ -736,7 +736,8 @@ boolean isUnhidden(AnnotationTarget target) { Boolean hidden = context.annotations().value(schemaAnnotation, SchemaConstant.PROP_HIDDEN); if (hidden == null || !hidden.booleanValue()) { - return true; + return context.getIgnoreResolver().referenceVisibility(propertyName, target, + reference) != Visibility.IGNORED; } } } @@ -800,7 +801,7 @@ private static void scanField(AnnotationScannerContext context, Map { + .flatMap(viewType -> { + /* + * Find all view classes that apply, based on the type array passed to the method. + * The views are placed in a map on the context with a boolean value indicating + * whether the view was directly specified on the method, or whether it was discovered + * as an inherited view from the view class hierarchy. + */ if (index.containsClass(viewType)) { - return index.inheritanceChain(index.getClass(viewType), viewType).values(); + return index.inheritanceChain(index.getClass(viewType), viewType) + .values() + .stream() + .map(v -> Map.entry(v, viewType.equals(v))); } - return Collections.singleton(viewType); + return Stream.of(Map.entry(viewType, Boolean.TRUE)); }) - .flatMap(Collection::stream) - .forEach(context.getJsonViews()::add); + .forEach(view -> context.getJsonViews().put(view.getKey(), view.getValue())); } } 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 8871bf145..d6a2e6ab8 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 @@ -5,12 +5,11 @@ import java.util.Collections; import java.util.Deque; import java.util.HashMap; -import java.util.LinkedHashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.function.UnaryOperator; import org.eclipse.microprofile.openapi.OASFactory; @@ -53,7 +52,7 @@ public class AnnotationScannerContext { private final Deque scanStack = new ArrayDeque<>(); private Deque resolverStack = new ArrayDeque<>(); private final Optional beanValidationScanner; - private final Set jsonViews = new LinkedHashSet<>(); + private final Map jsonViews = new LinkedHashMap<>(); private String[] currentConsumes; private String[] currentProduces; private String[] defaultConsumes; @@ -163,7 +162,7 @@ public Optional getBeanValidationScanner() { return beanValidationScanner; } - public Set getJsonViews() { + public Map getJsonViews() { return jsonViews; } diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolverTests.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolverTests.java index 5e66abe18..e50037e08 100644 --- a/core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolverTests.java +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/dataobject/TypeResolverTests.java @@ -863,22 +863,22 @@ public void setField3(String field3) { AnnotationScannerContext context = buildContext(emptyConfig(), Bean.class, Views.View0.class, Views.View1.class, Views.View2.class, Views.View3.class); - context.getJsonViews().add(Type.create(DotName.createSimple(Views.View0.class), Type.Kind.CLASS)); + context.getJsonViews().put(Type.create(DotName.createSimple(Views.View0.class), Type.Kind.CLASS), true); Map p0 = getProperties(context, Bean.class); assertFalse(p0.get("field0").isIgnored()); Stream.of("field1", "field2", "field3").forEach(f -> assertTrue(p0.get(f).isIgnored())); - context.getJsonViews().add(Type.create(DotName.createSimple(Views.View1.class), Type.Kind.CLASS)); + context.getJsonViews().put(Type.create(DotName.createSimple(Views.View1.class), Type.Kind.CLASS), true); Map p1 = getProperties(context, Bean.class); Stream.of("field0", "field1").forEach(f -> assertFalse(p1.get(f).isIgnored())); Stream.of("field2", "field3").forEach(f -> assertTrue(p1.get(f).isIgnored())); - context.getJsonViews().add(Type.create(DotName.createSimple(Views.View2.class), Type.Kind.CLASS)); + context.getJsonViews().put(Type.create(DotName.createSimple(Views.View2.class), Type.Kind.CLASS), true); Map p2 = getProperties(context, Bean.class); Stream.of("field0", "field1", "field2").forEach(f -> assertFalse(p2.get(f).isIgnored())); Stream.of("field3").forEach(f -> assertTrue(p2.get(f).isIgnored())); - context.getJsonViews().add(Type.create(DotName.createSimple(Views.View3.class), Type.Kind.CLASS)); + context.getJsonViews().put(Type.create(DotName.createSimple(Views.View3.class), Type.Kind.CLASS), true); Map p3 = getProperties(context, Bean.class); Stream.of("field0", "field1", "field2", "field3").forEach(f -> assertFalse(p3.get(f).isIgnored())); } 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 a9503e53b..dbcc79993 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 @@ -38,7 +38,7 @@ private void testAssertion(Class target, String expectedResourceName) throws OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, type); Schema result = scanner.process(); - registry.register(type, Collections.emptySet(), result); + registry.register(type, Collections.emptyMap(), result); printToConsole(oai); assertJsonEquals(expectedResourceName, oai); @@ -54,7 +54,7 @@ private void testAssertion(Class containerClass, OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, parentType); Schema result = scanner.process(); - registry.register(parentType, Collections.emptySet(), result); + registry.register(parentType, Collections.emptyMap(), 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 ddd36b373..ee2b1bbc4 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 @@ -410,7 +410,7 @@ public void registerCustomSchemas(SchemaRegistry schemaRegistry) { schema.setTitle("UUID"); schema.setDescription("Universally Unique Identifier"); schema.setExample("de8681db-b4d6-4c47-a428-4b959c1c8e9a"); - schemaRegistry.register(uuidType, Collections.emptySet(), schema); + schemaRegistry.register(uuidType, Collections.emptyMap(), 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 index b99236a52..7a7c01e01 100644 --- 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 @@ -1,10 +1,28 @@ package io.smallrye.openapi.runtime.scanner; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.jboss.jandex.Index; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonView; + import io.smallrye.openapi.api.OpenApiConfig; import io.smallrye.openapi.api.OpenApiDocument; import io.smallrye.openapi.api.SmallRyeOASConfig; @@ -95,4 +113,105 @@ public java.util.concurrent.CompletionStage updatePublic( printToConsole(result); assertJsonEquals("special.jsonview-schemas-basic.json", result); } + + @Test + void testJsonViewsWithIgnoredProperties() throws Exception { + class Views { + class Max extends Full { + } + + class Full extends Ingest { + } + + class Ingest extends Abridged { + } + + class Abridged { + } + } + + @Schema(name = "Role") + class Role { + @JsonView(Views.Full.class) + private UUID id; + @JsonView(Views.Ingest.class) + private String name; + @JsonView(Views.Full.class) + private LocalDateTime createdAt; + // Adding @Schema() here does fix the problem were @JsonIgnoreProperties in Group was removing description gobally. + // but now the @JsonView is being ignored and description field is in all JsonViews. + @Schema(title = "Title of description") + @JsonView(Views.Full.class) + private String description; + } + + @Schema(name = "Group") + class Group { + @JsonView(Views.Full.class) + private UUID id; + @JsonView(Views.Abridged.class) + private String name; + @JsonView(Views.Full.class) + private LocalDateTime createdAt; + @JsonView(Views.Full.class) + private String description; + @JsonView(Views.Ingest.class) + private String roleId; + @JsonView(Views.Abridged.class) + // @JsonIgnoreProperties should only apply to this the Group entity use case of Role. Currently, it's global. + @JsonIgnoreProperties("description") + private List roles; + } + + @Schema(name = "User") + class User { + @JsonView(Views.Full.class) + private UUID id; + + @JsonView(Views.Ingest.class) + private String name; + + @JsonView(Views.Ingest.class) + private String groupId; + + @JsonView(Views.Abridged.class) + @Schema(type = SchemaType.STRING, format = "date-time", description = "test date-time field") + private LocalDateTime birthday; + + @JsonView(Views.Full.class) + @Schema + private Group group; + } + + @Path("/user") + class UserResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + @JsonView(Views.Full.class) + @APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = User.class))) + public Response get() { + return null; + } + + @POST + public Response post(@RequestBody @JsonView(Views.Ingest.class) User group) { + return null; + } + } + + Index index = Index.of(Views.class, Views.Max.class, Views.Full.class, Views.Ingest.class, Views.Abridged.class, + User.class, Group.class, Role.class, UserResource.class, LocalDateTime.class); + OpenApiConfig config = dynamicConfig(SmallRyeOASConfig.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-with-ignored.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 607b2113d..42617f0cd 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 @@ -67,7 +67,7 @@ void testKitchenSinkWithRefs() throws IOException, JSONException { SchemaRegistry registry = context.getSchemaRegistry(); Schema result = scanner.process(); - registry.register(type, Collections.emptySet(), result); + registry.register(type, Collections.emptyMap(), 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 ce2794966..0f6d06762 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 @@ -30,7 +30,7 @@ void testNestedSchemasAddedToRegistry() throws IOException, JSONException { OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, parentType); Schema result = scanner.process(); - registry.register(parentType, Collections.emptySet(), result); + registry.register(parentType, Collections.emptyMap(), 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 0f101e896..968bf9df0 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 @@ -43,9 +43,9 @@ void testParameterizedNameCollisionsUseSequence() throws IOException, JSONExcept FieldInfo n2 = cInfo.field("n2"); FieldInfo n3 = cInfo.field("n3"); - Schema s1 = registry.register(n1.type(), Collections.emptySet(), OASFactory.createSchema()); - Schema s2 = registry.register(n2.type(), Collections.emptySet(), OASFactory.createSchema()); - Schema s3 = registry.register(n3.type(), Collections.emptySet(), OASFactory.createSchema()); + Schema s1 = registry.register(n1.type(), Collections.emptyMap(), OASFactory.createSchema()); + Schema s2 = registry.register(n2.type(), Collections.emptyMap(), OASFactory.createSchema()); + Schema s3 = registry.register(n3.type(), Collections.emptyMap(), OASFactory.createSchema()); assertEquals("#/components/schemas/NestableStringNestableStringString", s1.getRef()); assertEquals("#/components/schemas/NestableStringNestableStringObject", s2.getRef()); @@ -67,7 +67,7 @@ void testWildcardLowerBoundName() throws IOException, JSONException { ClassInfo cInfo = index.getClassByName(cName); FieldInfo n4 = cInfo.field("n4"); - Schema s4 = registry.register(n4.type(), Collections.emptySet(), OASFactory.createSchema()); + Schema s4 = registry.register(n4.type(), Collections.emptyMap(), OASFactory.createSchema()); assertEquals("#/components/schemas/NestableStringSuperInteger", s4.getRef()); } @@ -86,7 +86,7 @@ void testWildcardUpperBoundName() throws IOException, JSONException { ClassInfo cInfo = index.getClassByName(cName); FieldInfo n5 = cInfo.field("n5"); - Schema s5 = registry.register(n5.type(), Collections.emptySet(), OASFactory.createSchema()); + Schema s5 = registry.register(n5.type(), Collections.emptyMap(), OASFactory.createSchema()); assertEquals("#/components/schemas/NestableExtendsCharSequenceExtendsNumber", s5.getRef()); } @@ -106,7 +106,7 @@ void testWildcardWithGivenName() throws IOException, JSONException { ClassInfo cInfo = index.getClassByName(cName); FieldInfo n6 = cInfo.field("n6"); - Schema s6 = registry.register(n6.type(), Collections.emptySet(), OASFactory.createSchema()); + Schema s6 = registry.register(n6.type(), Collections.emptyMap(), OASFactory.createSchema()); assertEquals("#/components/schemas/n6", s6.getRef()); } @@ -129,7 +129,7 @@ void testNestedGenericWildcard() throws IOException, JSONException { OpenApiDataObjectScanner scanner = new OpenApiDataObjectScanner(context, n6Type); Schema result = scanner.process(); - registry.register(n6Type, Collections.emptySet(), result); + registry.register(n6Type, Collections.emptyMap(), 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 index bce3f1a63..864507c0d 100644 --- 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 @@ -13,7 +13,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BeanName_Internal_Public" + "$ref": "#/components/schemas/BeanName_Internal" } } } @@ -42,7 +42,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BeanName_WriteOnly_Public" + "$ref": "#/components/schemas/BeanName_WriteOnly" } } } @@ -64,7 +64,7 @@ }, "components": { "schemas": { - "BeanName_Internal_Public": { + "BeanName_Internal": { "type": "object", "properties": { "id": { @@ -74,7 +74,7 @@ "type": "string" }, "inner1": { - "$ref": "#/components/schemas/Inner1_Internal_Public" + "$ref": "#/components/schemas/Inner1_Internal" } } }, @@ -89,7 +89,7 @@ } } }, - "BeanName_WriteOnly_Public": { + "BeanName_WriteOnly": { "type": "object", "properties": { "name": { @@ -99,11 +99,11 @@ "type": "string" }, "inner1": { - "$ref": "#/components/schemas/Inner1_WriteOnly_Public" + "$ref": "#/components/schemas/Inner1_WriteOnly" } } }, - "Inner1_Internal_Public": { + "Inner1_Internal": { "type": "object", "properties": { "value": { @@ -125,7 +125,7 @@ } } }, - "Inner1_WriteOnly_Public": { + "Inner1_WriteOnly": { "type": "object", "properties": { "value": { diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-with-ignored.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-with-ignored.json new file mode 100644 index 000000000..2814be78c --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/special.jsonview-with-ignored.json @@ -0,0 +1,157 @@ +{ + "openapi" : "3.1.0", + "components" : { + "schemas" : { + "Group_Full" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "string", + "format" : "uuid", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "name" : { + "type" : "string" + }, + "createdAt" : { + "$ref" : "#/components/schemas/LocalDateTime" + }, + "description" : { + "type" : "string" + }, + "roleId" : { + "type" : "string" + }, + "roles" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "string", + "format" : "uuid", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "name" : { + "type" : "string" + }, + "createdAt" : { + "$ref" : "#/components/schemas/LocalDateTime" + } + } + } + } + } + }, + "Group_Ingest" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "roleId" : { + "type" : "string" + }, + "roles" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + } + } + } + } + } + }, + "LocalDateTime" : { + "type" : "string", + "format" : "date-time", + "examples" : [ "2022-03-10T12:15:50" ] + }, + "User_Full" : { + "type" : "object", + "properties" : { + "id" : { + "type" : "string", + "format" : "uuid", + "pattern" : "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}" + }, + "name" : { + "type" : "string" + }, + "groupId" : { + "type" : "string" + }, + "birthday" : { + "description" : "test date-time field", + "type" : "string", + "$ref" : "#/components/schemas/LocalDateTime" + }, + "group" : { + "$ref" : "#/components/schemas/Group_Full" + } + } + }, + "User_Ingest" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string" + }, + "groupId" : { + "type" : "string" + }, + "birthday" : { + "description" : "test date-time field", + "type" : "string", + "$ref" : "#/components/schemas/LocalDateTime" + }, + "group" : { + "$ref" : "#/components/schemas/Group_Ingest" + } + } + } + } + }, + "paths" : { + "/user" : { + "post" : { + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/User_Ingest" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "description" : "OK" + } + } + }, + "get" : { + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/User_Full" + } + } + } + } + } + } + } + }, + "info" : { + "title" : "Generated API", + "version" : "1.0" + } +}