Skip to content
Open
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 @@ -77,11 +77,13 @@ private AttributeMethods(@Nullable Class<? extends Annotation> annotationType, M
if (!foundDefaultValueMethod && (method.getDefaultValue() != null)) {
foundDefaultValueMethod = true;
}
if (!foundNestedAnnotation && (type.isAnnotation() || (type.isArray() && type.componentType().isAnnotation()))) {
boolean nestedAnnotation = (type.isAnnotation() || (type.isArray() && type.componentType().isAnnotation()));
if (!foundNestedAnnotation && nestedAnnotation) {
foundNestedAnnotation = true;
}
ReflectionUtils.makeAccessible(method);
this.canThrowTypeNotPresentException[i] = (type == Class.class || type == Class[].class || type.isEnum());
this.canThrowTypeNotPresentException[i] = (type == Class.class || type == Class[].class ||
type.isEnum() || nestedAnnotation);
}
this.hasDefaultValueMethod = foundDefaultValueMethod;
this.hasNestedAnnotation = foundNestedAnnotation;
Expand All @@ -104,7 +106,10 @@ boolean canLoad(Annotation annotation, AnnotatedElement source) {
for (int i = 0; i < size(); i++) {
if (canThrowTypeNotPresentException(i)) {
try {
AnnotationUtils.invokeAnnotationMethod(get(i), annotation);
Object value = AnnotationUtils.invokeAnnotationMethod(get(i), annotation);
if (!canLoadNestedAnnotations(value, source)) {
return false;
}
}
catch (IllegalStateException ex) {
// Plain invocation failure to expose -> leave up to attribute retrieval
Expand All @@ -123,6 +128,20 @@ boolean canLoad(Annotation annotation, AnnotatedElement source) {
return true;
}

private boolean canLoadNestedAnnotations(@Nullable Object value, AnnotatedElement source) {
if (value instanceof Annotation nested) {
return forAnnotationType(nested.annotationType()).canLoad(nested, source);
}
if (value instanceof Annotation[] nestedArray) {
for (Annotation nested : nestedArray) {
if (!forAnnotationType(nested.annotationType()).canLoad(nested, source)) {
return false;
}
}
}
return true;
}

/**
* Check if values from the given annotation can be safely accessed without causing
* any {@link TypeNotPresentException TypeNotPresentExceptions}.
Expand All @@ -138,7 +157,8 @@ void validate(Annotation annotation) {
for (int i = 0; i < size(); i++) {
if (canThrowTypeNotPresentException(i)) {
try {
AnnotationUtils.invokeAnnotationMethod(get(i), annotation);
Object value = AnnotationUtils.invokeAnnotationMethod(get(i), annotation);
validateNestedAnnotations(value);
}
catch (IllegalStateException ex) {
throw ex;
Expand All @@ -152,6 +172,17 @@ void validate(Annotation annotation) {
}
}

private void validateNestedAnnotations(@Nullable Object value) {
if (value instanceof Annotation nested) {
forAnnotationType(nested.annotationType()).validate(nested);
}
else if (value instanceof Annotation[] nestedArray) {
for (Annotation nested : nestedArray) {
forAnnotationType(nested.annotationType()).validate(nested);
}
}
}

private void assertAnnotation(Annotation annotation) {
Assert.notNull(annotation, "Annotation must not be null");
if (this.annotationType != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ void canThrowTypeNotPresentExceptionWhenHasClassArrayAttributeReturnsTrue() {
assertThat(methods.canThrowTypeNotPresentException(0)).isTrue();
}

@Test
void canThrowTypeNotPresentExceptionWhenHasAnnotationAttributeReturnsTrue() {
AttributeMethods methods = AttributeMethods.forAnnotationType(NestedValue.class);
assertThat(methods.canThrowTypeNotPresentException(0)).isTrue();
}

@Test
void canThrowTypeNotPresentExceptionWhenHasAnnotationArrayAttributeReturnsTrue() {
AttributeMethods methods = AttributeMethods.forAnnotationType(NestedArrayValue.class);
assertThat(methods.canThrowTypeNotPresentException(0)).isTrue();
}

@Test
void canThrowTypeNotPresentExceptionWhenNotClassOrClassArrayAttributeReturnsFalse() {
AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class);
Expand Down Expand Up @@ -141,6 +153,117 @@ void validateWhenDoesNotHaveTypeNotPresentExceptionThrowsNothing() {
attributes.validate(annotation);
}

@Test
void isValidWhenNestedAnnotationHasEnumConstantNotPresentExceptionReturnsFalse() {
EnumValueInner inner = mockBrokenEnumValueInner();
NestedValue annotation = mockAnnotation(NestedValue.class);
given(annotation.value()).willReturn(inner);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation, getClass())).isFalse();
}

@Test
void isValidWhenNestedAnnotationDoesNotHaveEnumConstantNotPresentExceptionReturnsTrue() {
EnumValueInner inner = mockHealthyEnumValueInner();
NestedValue annotation = mockAnnotation(NestedValue.class);
given(annotation.value()).willReturn(inner);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation, getClass())).isTrue();
}

@Test
void validateWhenNestedAnnotationHasEnumConstantNotPresentExceptionThrowsException() {
EnumValueInner inner = mockBrokenEnumValueInner();
NestedValue annotation = mockAnnotation(NestedValue.class);
given(annotation.value()).willReturn(inner);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThatIllegalStateException().isThrownBy(() -> attributes.validate(annotation))
.withMessageContaining("EnumValueInner")
.withCauseInstanceOf(EnumConstantNotPresentException.class);
}

@Test
void validateWhenNestedAnnotationDoesNotHaveEnumConstantNotPresentExceptionThrowsNothing() {
EnumValueInner inner = mockHealthyEnumValueInner();
NestedValue annotation = mockAnnotation(NestedValue.class);
given(annotation.value()).willReturn(inner);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
attributes.validate(annotation);
}

@Test
void isValidWhenNestedAnnotationArrayHasEnumConstantNotPresentExceptionReturnsFalse() {
EnumValueInner inner = mockBrokenEnumValueInner();
NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class);
given(annotation.value()).willReturn(new EnumValueInner[] {inner});
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation, getClass())).isFalse();
}

@Test
void isValidWhenNestedAnnotationArrayDoesNotHaveEnumConstantNotPresentExceptionReturnsTrue() {
EnumValueInner inner = mockHealthyEnumValueInner();
NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class);
given(annotation.value()).willReturn(new EnumValueInner[] {inner});
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation, getClass())).isTrue();
}

@Test
void isValidWhenNestedAnnotationArrayIsEmptyReturnsTrue() {
NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class);
given(annotation.value()).willReturn(new EnumValueInner[0]);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation, getClass())).isTrue();
}

@Test
void validateWhenNestedAnnotationArrayHasEnumConstantNotPresentExceptionThrowsException() {
EnumValueInner inner = mockBrokenEnumValueInner();
NestedArrayValue annotation = mockAnnotation(NestedArrayValue.class);
given(annotation.value()).willReturn(new EnumValueInner[] {inner});
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThatIllegalStateException().isThrownBy(() -> attributes.validate(annotation))
.withMessageContaining("EnumValueInner")
.withCauseInstanceOf(EnumConstantNotPresentException.class);
}

@Test
void isValidWhenDeeplyNestedAnnotationHasEnumConstantNotPresentExceptionReturnsFalse() {
EnumValueInner inner = mockBrokenEnumValueInner();
NestedValue nested = mockAnnotation(NestedValue.class);
given(nested.value()).willReturn(inner);
DeepNestedValue annotation = mockAnnotation(DeepNestedValue.class);
given(annotation.value()).willReturn(nested);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThat(attributes.canLoad(annotation, getClass())).isFalse();
}

@Test
void validateWhenDeeplyNestedAnnotationHasEnumConstantNotPresentExceptionThrowsException() {
EnumValueInner inner = mockBrokenEnumValueInner();
NestedValue nested = mockAnnotation(NestedValue.class);
given(nested.value()).willReturn(inner);
DeepNestedValue annotation = mockAnnotation(DeepNestedValue.class);
given(annotation.value()).willReturn(nested);
AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType());
assertThatIllegalStateException().isThrownBy(() -> attributes.validate(annotation))
.withMessageContaining("EnumValueInner")
.withCauseInstanceOf(EnumConstantNotPresentException.class);
}

private EnumValueInner mockBrokenEnumValueInner() {
EnumValueInner inner = mockAnnotation(EnumValueInner.class);
given(inner.value()).willThrow(new EnumConstantNotPresentException(ExampleEnum.class, "MISSING"));
return inner;
}

private EnumValueInner mockHealthyEnumValueInner() {
EnumValueInner inner = mockAnnotation(EnumValueInner.class);
given(inner.value()).willReturn(ExampleEnum.ONE);
return inner;
}

private List<Method> getAll(AttributeMethods attributes) {
List<Method> result = new ArrayList<>(attributes.size());
for (int i = 0; i < attributes.size(); i++) {
Expand Down Expand Up @@ -208,4 +331,38 @@ private <A extends Annotation> A mockAnnotation(Class<A> annotationType) {

}

@Retention(RetentionPolicy.RUNTIME)
@interface EnumValueInner {

ExampleEnum value();

}

@Retention(RetentionPolicy.RUNTIME)
@interface NestedValue {

EnumValueInner value();

}

@Retention(RetentionPolicy.RUNTIME)
@interface NestedArrayValue {

EnumValueInner[] value();

}

@Retention(RetentionPolicy.RUNTIME)
@interface DeepNestedValue {

NestedValue value();

}

enum ExampleEnum {

ONE

}

}