Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for com.fasterxml.jackson.annotation.JacksonAnnotationsInside annotation #487

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
@@ -0,0 +1,134 @@
/*
* Copyright 2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.victools.jsonschema.generator;

import com.fasterxml.classmate.members.ResolvedMember;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
* Helper class providing with standard mechanism to resolve annotations on annotated entities.
*
* @since 4.37.0
*/
public final class AnnotationHelper {

private AnnotationHelper() {
super();
}

/**
* Resolves the specified annotation on the given resolved member and resolve nested annotations.
*
* @param <A> the generic type of the annotation
* @param member where to look for the specified annotation
* @param annotationClass the class of the annotation to look for
* @param metaAnnotationPredicate the predicate indicating nested annotations
* @return an empty entry if not found
*/
public static <A extends Annotation> Optional<A> resolveAnnotation(
ResolvedMember<?> member,
Class<A> annotationClass,
Predicate<Annotation> metaAnnotationPredicate
) {
final A annotation = member.getAnnotations().get(annotationClass);
if (annotation != null) {
return Optional.of(annotation);
}
return resolveNestedAnnotations(StreamSupport.stream(member.getAnnotations().spliterator(), false), annotationClass, metaAnnotationPredicate);
}

/**
* Select the instance of the specified annotation type from the given list.
*
* <p>Also considering meta annotations (i.e., annotations on annotations) if a meta annotation is
* deemed eligible according to the given {@code Predicate}.</p>
*
* @param <A> the generic type of the annotation
* @param annotationList a list of annotations to look into
* @param annotationClass the class of the annotation to look for
* @param metaAnnotationPredicate the predicate indicating nested annotations
* @return an empty entry if not found
*/
public static <A extends Annotation> Optional<A> resolveAnnotation(
List<Annotation> annotationList,
Class<A> annotationClass,
Predicate<Annotation> metaAnnotationPredicate
) {
final Optional<Annotation> annotation = annotationList.stream().filter(annotationClass::isInstance).findFirst();
if (annotation.isPresent()) {
return annotation.map(annotationClass::cast);
}
return resolveNestedAnnotations(annotationList.stream(), annotationClass, metaAnnotationPredicate);
}

/**
* Select the instance of the specified annotation type from the given annotatedElement's annotations.
*
* <p>Also considering meta annotations (i.e., annotations on annotations) if a meta annotation is
* deemed eligible according to the given <code>metaAnnotationPredicate</code>.</p>
*
* @param <A> the generic type of the annotation
* @param annotatedElement where to look for the specified annotation
* @param annotationClass the class of the annotation to look for
* @param metaAnnotationPredicate the predicate indicating meta annotations
* @return an empty entry if not found
*/
public static <A extends Annotation> Optional<A> resolveAnnotation(
AnnotatedElement annotatedElement,
Class<A> annotationClass,
Predicate<Annotation> metaAnnotationPredicate
) {
final A annotation = annotatedElement.getAnnotation(annotationClass);
if (annotation != null) {
return Optional.of(annotation);
}
return resolveNestedAnnotations(Arrays.stream(annotatedElement.getAnnotations()), annotationClass, metaAnnotationPredicate);
}

private static <A extends Annotation> Optional<A> resolveNestedAnnotations(
Stream<Annotation> initialAnnotations,
Class<A> annotationClass,
Predicate<Annotation> metaAnnotationPredicate
) {
List<Annotation> annotations = extractAnnotationsFromMetaAnnotations(initialAnnotations, metaAnnotationPredicate);
while (!annotations.isEmpty()) {
final Optional<Annotation> directAnnotation = annotations.stream().filter(annotationClass::isInstance).findFirst();
if (directAnnotation.isPresent()) {
return directAnnotation.map(annotationClass::cast);
}
annotations = extractAnnotationsFromMetaAnnotations(annotations.stream(), metaAnnotationPredicate);
}
return Optional.empty();
}

private static List<Annotation> extractAnnotationsFromMetaAnnotations(
Stream<Annotation> annotations,
Predicate<Annotation> metaAnnotationPredicate
) {
return annotations.filter(metaAnnotationPredicate)
.flatMap(a -> Arrays.stream(a.annotationType().getAnnotations()))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -276,20 +276,7 @@ public ResolvedType getContainerItemType(ResolvedType containerType) {
*/
public <A extends Annotation> A getAnnotationFromList(Class<A> annotationClass, List<Annotation> annotationList,
Predicate<Annotation> considerOtherAnnotation) {
List<Annotation> annotations = annotationList;
while (!annotations.isEmpty()) {
Optional<Annotation> nestedAnnotation = annotations.stream()
.filter(annotationClass::isInstance)
.findFirst();
if (nestedAnnotation.isPresent()) {
return nestedAnnotation.map(annotationClass::cast).get();
}
annotations = annotations.stream()
.filter(considerOtherAnnotation)
.flatMap(otherAnnotation -> Stream.of(otherAnnotation.annotationType().getAnnotations()))
.collect(Collectors.toList());
}
return null;
return AnnotationHelper.resolveAnnotation(annotationList, annotationClass, considerOtherAnnotation).orElse(null);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.github.victools.jsonschema.generator;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
* Unit test class dedicated to the validation of {@link AnnotationHelper}.
*/
class AnnotationHelperTest {
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetAnnotation {
String value() default "";
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MetaAnnotation {}

@TargetAnnotation
private static class DirectlyAnnotatedClass {
}

private static class NonAnnotatedClass {
}

@UselessFirstComboAnnotation
@UselessSecondComboAnnotation
private static class AnnotatedClassWithUselessAnnotations {

}

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@MetaAnnotation
private @interface UselessFirstComboAnnotation {
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@MetaAnnotation
private @interface UselessSecondComboAnnotation {
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@MetaAnnotation
@TargetAnnotation("first combo annotation value")
private @interface FirstComboAnnotation {
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@MetaAnnotation
@TargetAnnotation("second combo annotation value")
private @interface SecondComboAnnotation {
}

@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@MetaAnnotation
@SecondComboAnnotation
private @interface ThirdComboAnnotation {
}

@FirstComboAnnotation
@SecondComboAnnotation
private static class IndirectlyAnnotatedClass {
}

@TargetAnnotation("direct value")
@FirstComboAnnotation
@SecondComboAnnotation
private static class BothDirectAndIndirectlyAnnotatedClass {
}

@ThirdComboAnnotation
@FirstComboAnnotation
private static class BreadthFirstAnnotatedClass {}

static Stream<Arguments> annotationLookupScenarios() {
return Stream.of(
Arguments.of(NonAnnotatedClass.class, Optional.empty()),
Arguments.of(AnnotatedClassWithUselessAnnotations.class, Optional.empty()),
Arguments.of(DirectlyAnnotatedClass.class, Optional.of("")),
Arguments.of(BothDirectAndIndirectlyAnnotatedClass.class, Optional.of("direct value")),
Arguments.of(IndirectlyAnnotatedClass.class, Optional.of("first combo annotation value")),
Arguments.of(BreadthFirstAnnotatedClass.class, Optional.of("first combo annotation value"))
);
}

@ParameterizedTest
@MethodSource("annotationLookupScenarios")
void resolveAnnotation_AnnotatedElement_respects_annotationLookupScenarios(Class<?> annotatedClass, Optional<String> expectedAnnotationValue) {
Optional<String> value = AnnotationHelper.resolveAnnotation(annotatedClass, TargetAnnotation.class, metaAnnotationPredicate()).map(TargetAnnotation::value);
Assertions.assertEquals(expectedAnnotationValue, value);
}

@ParameterizedTest
@MethodSource("annotationLookupScenarios")
void resolveAnnotation_List_respects_annotationLookupScenarios(Class<?> annotatedClass, Optional<String> expectedAnnotationValue) {
Optional<String> value = AnnotationHelper.resolveAnnotation(Arrays.asList(annotatedClass.getAnnotations()), TargetAnnotation.class, metaAnnotationPredicate()).map(TargetAnnotation::value);
Assertions.assertEquals(expectedAnnotationValue, value);
}

private static Predicate<Annotation> metaAnnotationPredicate() {
return (annotation) -> annotation.annotationType().isAnnotationPresent(MetaAnnotation.class);
}

}
1 change: 1 addition & 0 deletions jsonschema-module-jackson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Module for the [jsonschema-generator](../jsonschema-generator) – deriving JSON
13. Optionally: ignore all methods but those with a `@JsonProperty` annotation, if the `JacksonOption.INCLUDE_ONLY_JSONPROPERTY_ANNOTATED_METHODS` was provided (i.e. this is an "opt-in").
14. Optionally: respect `@JsonIdentityReference(alwaysAsId=true)` annotation if there is a corresponding `@JsonIdentityInfo` annotation on the type and the `JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID` as provided (i.e., this is an "opt-in")
15. Elevate nested properties to the parent type where members are annotated with `@JsonUnwrapped`.
16. Support `com.fasterxml.jackson.annotation.JacksonAnnotationsInside` annotated combo annotations

Schema attributes derived from validation annotations on getter methods are also applied to their associated fields.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.victools.jsonschema.module.jackson;

import com.fasterxml.classmate.members.ResolvedMember;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

final class AnnotationHelper {

static final Predicate<Annotation> JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER = (annotation) ->
annotation.annotationType().isAnnotationPresent(JacksonAnnotationsInside.class);

private AnnotationHelper() {
super();
}

/**
* Resolves the specified annotation on the given resolved member and resolve indirect jackson annotations.
*
* @param <A> the generic type of the annotation
* @param member where to look for the specified annotation
* @param annotationClass the class of the annotation to look for
* @return an empty entry if not found
*/
static <A extends Annotation> Optional<A> resolveAnnotation(ResolvedMember<?> member, Class<A> annotationClass) {
return com.github.victools.jsonschema.generator.AnnotationHelper.resolveAnnotation(
member,
annotationClass,
JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
}

/**
* Resolves the specified annotation on the given type and resolve indirect jackson annotations.
*
* @param <A> the generic type of the annotation
* @param declaringType where to look for the specified annotation
* @param annotationClass the class of the annotation to look for
* @return an empty entry if not found
*/
static <A extends Annotation> Optional<A> resolveAnnotation(AnnotatedElement declaringType, Class<A> annotationClass) {
return com.github.victools.jsonschema.generator.AnnotationHelper.resolveAnnotation(
declaringType,
annotationClass,
JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 VicTools.
* Copyright 2020-2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -121,7 +121,7 @@ protected ResolvedMethod getJsonValueAnnotatedMethod(ResolvedType javaType, Sche
ResolvedMethod[] memberMethods = context.getTypeContext().resolveWithMembers(javaType).getMemberMethods();
Set<ResolvedMethod> jsonValueAnnotatedMethods = Stream.of(memberMethods)
.filter(method -> method.getArgumentCount() == 0)
.filter(method -> Optional.ofNullable(method.getAnnotations().get(JsonValue.class)).map(JsonValue::value).orElse(false))
.filter(method -> AnnotationHelper.resolveAnnotation(method, JsonValue.class).map(JsonValue::value).orElse(false))
.collect(Collectors.toSet());
if (jsonValueAnnotatedMethods.size() == 1) {
return jsonValueAnnotatedMethods.iterator().next();
Expand All @@ -141,14 +141,16 @@ protected List<String> getSerializedValuesFromJsonProperty(ResolvedType javaType
List<String> serializedJsonValues = new ArrayList<>(enumConstants.length);
for (Object enumConstant : enumConstants) {
String enumValueName = ((Enum<?>) enumConstant).name();
JsonProperty annotation = javaType.getErasedType()
.getDeclaredField(enumValueName)
.getAnnotation(JsonProperty.class);
if (annotation == null) {
Optional<JsonProperty> annotation = AnnotationHelper.resolveAnnotation(
javaType.getErasedType().getDeclaredField(enumValueName),
JsonProperty.class
);
if (!annotation.isPresent()) {
// enum constant without @JsonProperty annotation
return null;
}
serializedJsonValues.add(JsonProperty.USE_DEFAULT_NAME.equals(annotation.value()) ? enumValueName : annotation.value());
final String annotationValue = annotation.get().value();
serializedJsonValues.add(JsonProperty.USE_DEFAULT_NAME.equals(annotationValue) ? enumValueName : annotationValue);
}
return serializedJsonValues;
} catch (NoSuchFieldException | SecurityException ex) {
Expand Down
Loading
Loading