Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,9 @@ public static Schema readSchema(final AnnotationScannerContext context, Annotati
public static Schema readSchema(final AnnotationScannerContext context,
Schema schema,
AnnotationInstance annotation,
ClassInfo clazz) {
return readSchema(context, schema, annotation, clazz, Collections.emptyMap());
ClassInfo clazz,
boolean registerSchema) {
return readSchema(context, schema, annotation, clazz, registerSchema, Collections.emptyMap());
}

/**
Expand All @@ -113,6 +114,7 @@ static Schema readSchema(final AnnotationScannerContext context,
Schema schema,
AnnotationInstance annotation,
ClassInfo clazz,
boolean registerSchema,
Map<String, Object> defaults) {

if (isAnnotationMissingOrHidden(context, annotation, defaults)) {
Expand All @@ -129,7 +131,9 @@ static Schema readSchema(final AnnotationScannerContext context,
*
* Ignore the reference returned by register, the caller expects the full schema.
*/
schemaRegistration(context, clazzType, schema);
if (registerSchema) {
schemaRegistration(context, clazzType, schema);
}

return schema;
}
Expand Down Expand Up @@ -641,7 +645,7 @@ public static Schema enumToSchema(final AnnotationScannerContext context, Type e
Map<String, Object> defaults = new LinkedHashMap<>(TypeUtil.getTypeAttributes(enumValueType));
defaults.put(SchemaConstant.PROP_ENUMERATION, enumeration);

enumSchema = readSchema(context, enumSchema, schemaAnnotation, enumKlazz, defaults);
enumSchema = readSchema(context, enumSchema, schemaAnnotation, enumKlazz, true, defaults);
} else {
TypeUtil.applyTypeAttributes(enumValueType, enumSchema);
enumSchema.setEnumeration(enumeration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.smallrye.openapi.runtime.scanner.dataobject.AnnotationTargetProcessor;
import io.smallrye.openapi.runtime.scanner.dataobject.AugmentedIndexView;
import io.smallrye.openapi.runtime.scanner.dataobject.DataObjectDeque;
import io.smallrye.openapi.runtime.scanner.dataobject.IgnoreResolver;
import io.smallrye.openapi.runtime.scanner.dataobject.TypeResolver;
import io.smallrye.openapi.runtime.scanner.spi.AnnotationScannerContext;
import io.smallrye.openapi.runtime.util.TypeUtil;
Expand Down Expand Up @@ -246,16 +247,19 @@ private void depthFirstGraphSearch() {

ClassInfo currentClass = currentPathEntry.getClazz();
Schema currentSchema = currentPathEntry.getSchema();
AnnotationTarget currentTarget = currentPathEntry.getAnnotationTarget();
boolean allowRegistration = !IgnoreResolver.configuresVisibility(context, currentTarget);

// First, handle class annotations (re-assign since readKlass may return new schema)
currentSchema = readKlass(currentClass, currentType, currentSchema);
currentSchema = readKlass(currentClass, currentType, currentSchema, allowRegistration);

TypeUtil.mapDeprecated(context, currentClass, currentSchema::getDeprecated, currentSchema::setDeprecated);
currentPathEntry.setSchema(currentSchema);

if (!hasNonNullType(currentSchema)) {
// If not schema has yet been set, consider this an "object"
SchemaSupport.setType(currentSchema, Schema.SchemaType.OBJECT);
} else {
} else if (allowRegistration) {
maybeRegisterSchema(currentType, currentSchema, entrySchema);
}

Expand Down Expand Up @@ -364,14 +368,15 @@ private void encloseCurrentSchema(Schema currentSchema, Type currentType, DataOb

private Schema readKlass(ClassInfo currentClass,
Type currentType,
Schema currentSchema) {
Schema currentSchema,
boolean registerSchema) {

AnnotationInstance annotation = TypeUtil.getSchemaAnnotation(context, currentClass);
Schema classSchema;

if (annotation != null) {
// Because of implementation= field, *may* return a new schema rather than modify.
classSchema = SchemaFactory.readSchema(context, currentSchema, annotation, currentClass);
classSchema = SchemaFactory.readSchema(context, currentSchema, annotation, currentClass, registerSchema);
} else if (isA(currentType, ENUM_TYPE)) {
classSchema = SchemaFactory.enumToSchema(context, currentType);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,40 @@ public Visibility referenceVisibility(String propertyName,
return Visibility.UNSET;
}

/**
* Check whether the given target contains annotations that configure the
* visibility of the properties of the type annotated.
*
* This is used to 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.
*
* <p>
* 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.
*
* <pre>
* <code>
* class A {
* &#64;JsonIgnoreProperties({"field2"})
* B fieldB;
* }
*
* class B {
* int field1;
* int field2;
* }
* </code>
* </pre>
*/
public static boolean configuresVisibility(AnnotationScannerContext context, AnnotationTarget target) {
if (target != null) {
return context.getIgnoreResolver().configuresVisibility(target);
}
return false;
}

public boolean configuresVisibility(AnnotationTarget reference) {
for (IgnoreAnnotationHandler handler : ignoreHandlers) {
if (handler.configuresVisibility(reference)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,27 +155,10 @@ public Type processType() {
* refers to the type's class is not annotated with an annotation that alters
* the visibility of fields in the class.
*
* <p>
* 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.
*
* <pre>
* <code>
* class A {
* &#64;JsonIgnoreProperties({"field2"})
* B fieldB;
* }
*
* class B {
* int field1;
* int field2;
* }
* </code>
* </pre>
* @see IgnoreResolver#configuresVisibility(AnnotationScannerContext, AnnotationTarget)
*/
public boolean allowRegistration() {
return !context.getIgnoreResolver().configuresVisibility(annotationTarget);
return !IgnoreResolver.configuresVisibility(context, annotationTarget);
}

private Type readArrayType(ArrayType arrayType, Schema arraySchema) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
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;
Expand Down Expand Up @@ -581,47 +580,6 @@ private static boolean isNonPublicOrAbsent(MethodInfo method) {
return method == null || !Modifier.isPublic(method.flags());
}

private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) {
Map<Type, Boolean> activeViews = context.getJsonViews();

if (activeViews.isEmpty()) {
return true;
}

Type[] applicableViews = getJsonViews(context, propertySource);

if (applicableViews != null && applicableViews.length > 0) {
return Arrays.stream(applicableViews).anyMatch(activeViews::containsKey);
}

return true;
}

/**
* Find {@literal @}JsonView annotations on the field/method, or on the
* declaring class when the field/method itself it not directly targeted by the
* annotation.
*/
private static Type[] getJsonViews(AnnotationScannerContext context, AnnotationTarget propertySource) {
Annotations annotations = context.annotations();

Type[] directViews = annotations.getAnnotationValue(propertySource, JacksonConstants.JSON_VIEW);

if (directViews != null) {
return directViews;
}

ClassInfo declaringClass;

if (propertySource.kind() == Kind.FIELD) {
declaringClass = propertySource.asField().declaringClass();
} else {
declaringClass = propertySource.asMethod().declaringClass();
}

return annotations.getAnnotationValue(declaringClass, JacksonConstants.JSON_VIEW);
}

/**
* Determine if the target should be exposed in the API or ignored. Explicitly un-hiding a property
* via the <code>@Schema</code> annotation overrides configuration to ignore the same property
Expand All @@ -638,12 +596,6 @@ private void processVisibility(AnnotationScannerContext context, AnnotationTarge
return;
}

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, propertyName)) {
case EXPOSED:
this.exposed = true;
Expand Down Expand Up @@ -706,13 +658,26 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context
List<ClassInfo> descendants,
String propertyName) {

IgnoreResolver.Visibility visibility;

if (!isViewable(context, target)) {
return IgnoreResolver.Visibility.IGNORED;
}

IgnoreResolver ignoreResolver = context.getIgnoreResolver();
visibility = ignoreResolver.referenceVisibility(propertyName, target, reference);

if (visibility != IgnoreResolver.Visibility.UNSET) {
return visibility;
}

if (isUnhidden(target)) {
// @Schema with hidden = false and not already ignored by a member lower in the class hierarchy
return IgnoreResolver.Visibility.EXPOSED;
}

// First check if a descendant class has hidden/exposed the property
IgnoreResolver.Visibility visibility = ignoreResolver.getDescendantVisibility(propertyName, descendants);
visibility = ignoreResolver.getDescendantVisibility(propertyName, descendants);

if (visibility == IgnoreResolver.Visibility.UNSET) {
visibility = ignoreResolver.isIgnore(propertyName, target, reference);
Expand All @@ -721,23 +686,64 @@ private IgnoreResolver.Visibility getVisibility(AnnotationScannerContext context
return visibility;
}

private static boolean isViewable(AnnotationScannerContext context, AnnotationTarget propertySource) {
Map<Type, Boolean> activeViews = context.getJsonViews();

if (activeViews.isEmpty()) {
return true;
}

Type[] applicableViews = getJsonViews(context, propertySource);

if (applicableViews != null && applicableViews.length > 0) {
return Arrays.stream(applicableViews).anyMatch(activeViews::containsKey);
}

return true;
}

/**
* Find {@literal @}JsonView annotations on the field/method, or on the
* declaring class when the field/method itself it not directly targeted by the
* annotation.
*/
private static Type[] getJsonViews(AnnotationScannerContext context, AnnotationTarget propertySource) {
Annotations annotations = context.annotations();

AnnotationInstance jsonView = annotations.getAnnotation(propertySource, JacksonConstants.JSON_VIEW);

if (jsonView != null) {
return annotations.value(jsonView);
}

ClassInfo declaringClass;

if (propertySource.kind() == Kind.FIELD) {
declaringClass = propertySource.asField().declaringClass();
} else {
declaringClass = propertySource.asMethod().declaringClass();
}

return annotations.getAnnotationValue(declaringClass, JacksonConstants.JSON_VIEW);
}

/**
* Determines whether the target is set to hidden = false via the <code>@Schema</code>
* 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, AnnotationTarget reference) {
boolean isUnhidden(AnnotationTarget target) {
if (target != null) {
AnnotationInstance schemaAnnotation = TypeUtil.getSchemaAnnotation(context, target);
Annotations annotations = context.annotations();
AnnotationInstance schema = annotations.getAnnotation(target, SchemaConstant.DOTNAME_SCHEMA);

if (schemaAnnotation != null) {
Boolean hidden = context.annotations().value(schemaAnnotation, SchemaConstant.PROP_HIDDEN);
if (schema != null) {
Boolean hidden = annotations.value(schema, SchemaConstant.PROP_HIDDEN);

if (hidden == null || !hidden.booleanValue()) {
return context.getIgnoreResolver().referenceVisibility(propertyName, target,
reference) != Visibility.IGNORED;
return true;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,6 @@ class Role {
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;
Expand All @@ -152,13 +148,10 @@ class Group {
@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<Role> roles;
}
Expand Down Expand Up @@ -199,8 +192,41 @@ public Response post(@RequestBody @JsonView(Views.Ingest.class) User group) {
}
}

@Path("/role")
class RoleResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@JsonView(Views.Full.class)
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Role.class)))
public Response getRole() {
return null;
}

@POST
public Response post(@RequestBody @JsonView(Views.Ingest.class) Role role) {
return null;
}
}

@Path("/group")
class GroupResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@JsonView(Views.Full.class)
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = Group.class)))
public Response get() {
return null;
}

@POST
public Response post(@RequestBody @JsonView(Views.Ingest.class) Group 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);
User.class, Group.class, Role.class, UserResource.class, LocalDateTime.class, RoleResource.class,
GroupResource.class);
OpenApiConfig config = dynamicConfig(SmallRyeOASConfig.SMALLRYE_REMOVE_UNUSED_SCHEMAS, Boolean.TRUE);
OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, index);

Expand Down
Loading