AnnotationUtilsTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* https://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 org.apache.commons.lang3;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
*/
class AnnotationUtilsTest extends AbstractLangTest {
@FunctionAnnotation
@RunnableAnnotation
@SupplierAnnotation
@ThrowingSupplierAnnotation
private static final class AnnotatedClass {
}
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationArray {
FunctionAnnotation[] value();
}
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationArrayContainer {
AnnotationArray[] value();
}
@AnnotationArray({ @FunctionAnnotation("first"), @FunctionAnnotation("second") })
@AnnotationArrayContainer(@AnnotationArray({ @FunctionAnnotation("first"), @FunctionAnnotation("second") }))
private static final class ArrayAnnotatedClass {
}
@Retention(RetentionPolicy.RUNTIME)
public @interface FunctionAnnotation {
Function<String, String> FUNCTION = value -> value;
String value() default "member";
}
@Retention(RetentionPolicy.RUNTIME)
public @interface NestAnnotation {
boolean booleanValue();
boolean[] booleanValues();
byte byteValue();
byte[] byteValues();
char charValue();
char[] charValues();
double doubleValue();
double[] doubleValues();
float floatValue();
float[] floatValues();
int intValue();
int[] intValues();
long longValue();
long[] longValues();
short shortValue();
short[] shortValues();
Stooge stooge();
Stooge[] stooges();
String string();
String[] strings();
Class<?> type();
Class<?>[] types();
}
@Retention(RetentionPolicy.RUNTIME)
public @interface RunnableAnnotation {
Runnable RUNNABLE = () -> {
// Creates a void synthetic method for the regression test.
};
String value() default "member";
}
public enum Stooge {
MOE, LARRY, CURLY, JOE, SHEMP
}
@Retention(RetentionPolicy.RUNTIME)
public @interface SupplierAnnotation {
Supplier<String> SUPPLIER = () -> "constant";
String value() default "member";
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
boolean booleanValue();
boolean[] booleanValues();
byte byteValue();
byte[] byteValues();
char charValue();
char[] charValues();
double doubleValue();
double[] doubleValues();
float floatValue();
float[] floatValues();
int intValue();
int[] intValues();
long longValue();
long[] longValues();
NestAnnotation nest();
NestAnnotation[] nests();
short shortValue();
short[] shortValues();
Stooge stooge();
Stooge[] stooges();
String string();
String[] strings();
Class<?> type();
Class<?>[] types();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TestMethodAnnotation {
final class None extends Throwable {
private static final long serialVersionUID = 1L;
}
Class<? extends Throwable> expected() default None.class;
long timeout() default 0L;
}
@Retention(RetentionPolicy.RUNTIME)
public @interface ThrowingSupplierAnnotation {
Supplier<String> SUPPLIER = () -> {
throw new AssertionError("Annotation member access must not invoke a lambda");
};
String value() default "member";
}
@TestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
nest = @NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object.class,
types = { Object.class }
),
nests = {
@NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object[].class,
types = { Object[].class }
)
},
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.SHEMP,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.CURLY },
string = "",
strings = { "" },
type = Object.class,
types = { Object.class }
)
public Object dummy1;
@TestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
nest = @NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object.class,
types = { Object.class }
),
nests = {
@NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object[].class,
types = { Object[].class }
)
},
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.SHEMP,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.CURLY },
string = "",
strings = { "" },
type = Object.class,
types = { Object.class }
)
public Object dummy2;
@TestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
nest = @NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object.class,
types = { Object.class }
),
nests = {
@NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object[].class,
types = { Object[].class }
),
//add a second NestAnnotation to break equality:
@NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object[].class,
types = { Object[].class }
)
},
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.SHEMP,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.CURLY },
string = "",
strings = { "" },
type = Object.class,
types = { Object.class }
)
public Object dummy3;
@NestAnnotation(
booleanValue = false,
booleanValues = { false },
byteValue = 0,
byteValues = { 0 },
charValue = 0,
charValues = { 0 },
doubleValue = 0,
doubleValues = { 0 },
floatValue = 0,
floatValues = { 0 },
intValue = 0,
intValues = { 0 },
longValue = 0,
longValues = { 0 },
shortValue = 0,
shortValues = { 0 },
stooge = Stooge.CURLY,
stooges = { Stooge.MOE, Stooge.LARRY, Stooge.SHEMP },
string = "",
strings = { "" },
type = Object[].class,
types = { Object[].class }
)
public Object dummy4;
private Field field1;
private Field field2;
private Field field3;
private Field field4;
@BeforeEach
void setup() throws Exception {
field1 = getClass().getDeclaredField("dummy1");
field2 = getClass().getDeclaredField("dummy2");
field3 = getClass().getDeclaredField("dummy3");
field4 = getClass().getDeclaredField("dummy4");
}
@Test
void testAnnotationsOfDifferingTypes() {
assertFalse(AnnotationUtils.equals(field1.getAnnotation(TestAnnotation.class), field4.getAnnotation(NestAnnotation.class)));
assertFalse(AnnotationUtils.equals(field4.getAnnotation(NestAnnotation.class), field1.getAnnotation(TestAnnotation.class)));
}
@Test
void testBothArgsNull() {
assertTrue(AnnotationUtils.equals(null, null));
}
@Test
void testEquivalence() {
assertTrue(AnnotationUtils.equals(field1.getAnnotation(TestAnnotation.class), field2.getAnnotation(TestAnnotation.class)));
assertTrue(AnnotationUtils.equals(field2.getAnnotation(TestAnnotation.class), field1.getAnnotation(TestAnnotation.class)));
}
@Test
void testEquivalenceWithoutInvokingSupplier() {
final ThrowingSupplierAnnotation first =
AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
final ThrowingSupplierAnnotation second =
(ThrowingSupplierAnnotation) Proxy.newProxyInstance(
ThrowingSupplierAnnotation.class.getClassLoader(),
new Class<?>[] { ThrowingSupplierAnnotation.class },
(proxy, method, args) -> method.invoke(first, args));
assertNotSame(first, second);
assertTrue(AnnotationUtils.equals(first, second));
}
@Test
void testGeneratedAnnotationEquivalentToRealAnnotation() {
assertTimeoutPreemptively(Duration.ofSeconds(666L), () -> {
final Test real = getClass().getDeclaredMethod(
"testGeneratedAnnotationEquivalentToRealAnnotation").getAnnotation(Test.class);
final InvocationHandler generatedTestInvocationHandler = (proxy, method, args) -> {
if ("equals".equals(method.getName()) && method.getParameterTypes().length == 1) {
return Boolean.valueOf(proxy == args[0]);
}
if ("hashCode".equals(method.getName()) && method.getParameterTypes().length == 0) {
return Integer.valueOf(System.identityHashCode(proxy));
}
if ("toString".equals(method.getName()) && method.getParameterTypes().length == 0) {
return "Test proxy";
}
return method.invoke(real, args);
};
final Test generated = (Test) Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[]{Test.class},
generatedTestInvocationHandler);
assertEquals(real, generated);
assertNotEquals(generated, real);
assertTrue(AnnotationUtils.equals(generated, real));
assertTrue(AnnotationUtils.equals(real, generated));
final Test generated2 = (Test) Proxy.newProxyInstance(Thread.currentThread()
.getContextClassLoader(), new Class[]{Test.class},
generatedTestInvocationHandler);
assertNotEquals(generated, generated2);
assertNotEquals(generated2, generated);
assertTrue(AnnotationUtils.equals(generated, generated2));
assertTrue(AnnotationUtils.equals(generated2, generated));
});
}
@Test
void testHashCode() {
assertTimeoutPreemptively(Duration.ofSeconds(666L), () -> {
final Test test = getClass().getDeclaredMethod("testHashCode").getAnnotation(Test.class);
assertEquals(test.hashCode(), AnnotationUtils.hashCode(test));
final TestAnnotation testAnnotation1 = field1.getAnnotation(TestAnnotation.class);
assertEquals(testAnnotation1.hashCode(), AnnotationUtils.hashCode(testAnnotation1));
final TestAnnotation testAnnotation3 = field3.getAnnotation(TestAnnotation.class);
assertEquals(testAnnotation3.hashCode(), AnnotationUtils.hashCode(testAnnotation3));
});
}
@Test
void testHashCodeOfAnnotationArrayEdgeCases() {
final AnnotationArray real = ArrayAnnotatedClass.class.getAnnotation(AnnotationArray.class);
final FunctionAnnotation first = real.value()[0];
final FunctionAnnotation second = real.value()[1];
final FunctionAnnotation[][] values = {
{}, { first }, { first, second }, { second, first }, { null }, { first, null, second }
};
for (final FunctionAnnotation[] value : values) {
final AnnotationArray generated = (AnnotationArray) Proxy.newProxyInstance(
AnnotationArray.class.getClassLoader(), new Class<?>[] { AnnotationArray.class },
(proxy, method, args) -> "value".equals(method.getName()) ? value : method.invoke(real, args));
final int expected = 127 * "value".hashCode() ^ Arrays.hashCode(value);
assertEquals(expected, AnnotationUtils.hashCode(generated));
}
}
@Test
void testHashCodeOfDeeplyNestedAnnotationArray() {
final AnnotationArrayContainer real = ArrayAnnotatedClass.class.getAnnotation(AnnotationArrayContainer.class);
final AnnotationArray inner = real.value()[0];
final FunctionAnnotation[] values = inner.value();
final FunctionAnnotation[] generatedValues = new FunctionAnnotation[values.length];
for (int i = 0; i < values.length; i++) {
final FunctionAnnotation value = values[i];
generatedValues[i] = (FunctionAnnotation) Proxy.newProxyInstance(
FunctionAnnotation.class.getClassLoader(), new Class<?>[] { FunctionAnnotation.class },
(proxy, method, args) -> "hashCode".equals(method.getName())
? Integer.valueOf(value.hashCode() + 1) : method.invoke(value, args));
}
final AnnotationArray generatedInner = (AnnotationArray) Proxy.newProxyInstance(
AnnotationArray.class.getClassLoader(), new Class<?>[] { AnnotationArray.class },
(proxy, method, args) -> "value".equals(method.getName()) ? generatedValues : method.invoke(inner, args));
final AnnotationArrayContainer generated = (AnnotationArrayContainer) Proxy.newProxyInstance(
AnnotationArrayContainer.class.getClassLoader(), new Class<?>[] { AnnotationArrayContainer.class },
(proxy, method, args) -> "value".equals(method.getName())
? new AnnotationArray[] { generatedInner } : method.invoke(real, args));
assertTrue(AnnotationUtils.equals(real, generated));
assertTrue(AnnotationUtils.equals(generated, real));
assertEquals(real.hashCode(), AnnotationUtils.hashCode(generated));
}
@Test
void testHashCodeOfNestedAnnotationArray() {
assertTimeoutPreemptively(Duration.ofSeconds(666L), () -> {
final TestAnnotation real = field1.getAnnotation(TestAnnotation.class);
// A nested annotation whose own hashCode() does not follow Annotation#hashCode().
final InvocationHandler nestHandler = (proxy, method, args) -> {
if ("hashCode".equals(method.getName()) && method.getParameterTypes().length == 0) {
return Integer.valueOf(real.nests()[0].hashCode() + 1);
}
return method.invoke(real.nests()[0], args);
};
final NestAnnotation[] nests = { (NestAnnotation) Proxy.newProxyInstance(
NestAnnotation.class.getClassLoader(), new Class[] { NestAnnotation.class }, nestHandler) };
final InvocationHandler testHandler = (proxy, method, args) -> {
if ("nests".equals(method.getName()) && method.getParameterTypes().length == 0) {
return nests;
}
return method.invoke(real, args);
};
final TestAnnotation generated = (TestAnnotation) Proxy.newProxyInstance(
TestAnnotation.class.getClassLoader(), new Class[] { TestAnnotation.class }, testHandler);
assertTrue(AnnotationUtils.equals(real, generated));
assertTrue(AnnotationUtils.equals(generated, real));
assertEquals(AnnotationUtils.hashCode(real), AnnotationUtils.hashCode(generated));
});
}
@Test
void testHashCodeWithFunction() {
final FunctionAnnotation annotation = AnnotatedClass.class.getAnnotation(FunctionAnnotation.class);
assertEquals(annotation.hashCode(), AnnotationUtils.hashCode(annotation));
}
@Test
void testHashCodeWithoutInvokingSupplier() {
final ThrowingSupplierAnnotation annotation =
AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
final int expected = 127 * "value".hashCode() ^ "member".hashCode();
assertEquals(expected, AnnotationUtils.hashCode(annotation));
}
@Test
void testHashCodeWithRunnable() {
final RunnableAnnotation annotation = AnnotatedClass.class.getAnnotation(RunnableAnnotation.class);
assertEquals(annotation.hashCode(), AnnotationUtils.hashCode(annotation));
}
@Test
void testHashCodeWithSupplier() {
final SupplierAnnotation annotation = AnnotatedClass.class.getAnnotation(SupplierAnnotation.class);
assertEquals(annotation.hashCode(), AnnotationUtils.hashCode(annotation));
}
@Test
void testIsValidAnnotationMemberType() {
for (final Class<?> type : new Class[] { byte.class, short.class, int.class, char.class,
long.class, float.class, double.class, boolean.class, String.class, Class.class,
NestAnnotation.class, TestAnnotation.class, Stooge.class, ElementType.class }) {
assertTrue(AnnotationUtils.isValidAnnotationMemberType(type));
assertTrue(AnnotationUtils.isValidAnnotationMemberType(Array.newInstance(type, 0)
.getClass()));
}
for (final Class<?> type : new Class[] { Object.class, Map.class, Collection.class }) {
assertFalse(AnnotationUtils.isValidAnnotationMemberType(type));
assertFalse(AnnotationUtils.isValidAnnotationMemberType(Array.newInstance(type, 0)
.getClass()));
}
}
@Test
void testNonEquivalentAnnotationsOfSameType() {
assertFalse(AnnotationUtils.equals(field1.getAnnotation(TestAnnotation.class), field3.getAnnotation(TestAnnotation.class)));
assertFalse(AnnotationUtils.equals(field3.getAnnotation(TestAnnotation.class), field1.getAnnotation(TestAnnotation.class)));
}
@Test
void testOneArgNull() {
assertFalse(AnnotationUtils.equals(field1.getAnnotation(TestAnnotation.class), null));
assertFalse(AnnotationUtils.equals(null, field1.getAnnotation(TestAnnotation.class)));
}
@Test
void testSameInstance() {
assertTrue(AnnotationUtils.equals(field1.getAnnotation(TestAnnotation.class), field1.getAnnotation(TestAnnotation.class)));
}
@Test
@TestMethodAnnotation(timeout = 666000)
void testToString() {
assertTimeoutPreemptively(Duration.ofSeconds(666L), () -> {
final TestMethodAnnotation testAnnotation =
getClass().getDeclaredMethod("testToString").getAnnotation(TestMethodAnnotation.class);
final String annotationString = AnnotationUtils.toString(testAnnotation);
assertTrue(annotationString.startsWith("@org.apache.commons.lang3.AnnotationUtilsTest$TestMethodAnnotation("));
assertTrue(annotationString.endsWith(")"));
assertTrue(annotationString.contains("expected=class org.apache.commons.lang3.AnnotationUtilsTest$TestMethodAnnotation$None"));
assertTrue(annotationString.contains("timeout=666000"));
assertTrue(annotationString.contains(", "));
});
}
@Test
void testToStringWithRunnable() {
final RunnableAnnotation annotation = AnnotatedClass.class.getAnnotation(RunnableAnnotation.class);
assertEquals("@" + RunnableAnnotation.class.getName() + "(value=member)", AnnotationUtils.toString(annotation));
}
@Test
void testToStringWithSupplier() {
final SupplierAnnotation annotation = AnnotatedClass.class.getAnnotation(SupplierAnnotation.class);
assertEquals("@" + SupplierAnnotation.class.getName() + "(value=member)", AnnotationUtils.toString(annotation));
}
@Test
void testToStringWithThrowingSupplier() {
final ThrowingSupplierAnnotation annotation = AnnotatedClass.class.getAnnotation(ThrowingSupplierAnnotation.class);
assertEquals("@" + ThrowingSupplierAnnotation.class.getName() + "(value=member)", AnnotationUtils.toString(annotation));
}
}