From bf0ac800b2a6f5e84436bd8fc9d34befa6d92f8c Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Thu, 9 Jan 2020 00:26:13 +0100 Subject: [PATCH] unify annotation parameter analysis Since the code to recursively analyze annotation parameters is complicated, error prone and duplicated, it makes sense to introduce a more sophisticated parameter handling within `JavaAnnotation` itself. This way we can a) separate traversal from actual handling (say what we want to do, not how), b) unify and test the traversal in isolation and c) offer a nicer end user API for handling annotation parameters for `JavaAnnotations` that are not used via Reflection in tests (so far there was pretty much no choice but to use instanceof's and casts requiring some knowledge about the imported annotation type). Parameter handling is implemented as a Visitor pattern, which is well known and established for complex tree structures (with various types of nodes), where conditional traversal happens in various places. Signed-off-by: Peter Gafert --- .../archunit/core/domain/JavaAnnotation.java | 155 +++++++- ...avaAnnotationParameterVisitorAcceptor.java | 151 ++++++++ .../core/domain/JavaClassDependencies.java | 48 +-- .../core/importer/ClassGraphCreator.java | 44 +-- .../core/domain/JavaAnnotationTest.java | 358 ++++++++++++++++++ 5 files changed, 695 insertions(+), 61 deletions(-) create mode 100644 archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java index f644670742..8256b9de79 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java @@ -28,6 +28,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; /** * Represents an imported annotation on an annotated object like a class or a method. To be @@ -174,10 +175,12 @@ public CanBeAnnotated getAnnotatedElement() { * @param property The name of the annotation property, i.e. the declared method name * @return the value of the given property, where the result type is more precisely * */ @@ -194,6 +197,64 @@ public Map getProperties() { return values; } + /** + * Simple implementation of the Visitor pattern (compare e.g. + * https://en.wikipedia.org/wiki/Visitor_pattern).

+ * While it is fairly convenient to analyse a {@link JavaAnnotation} that is on the classpath (e.g. by using {@link #as(Class)}), + * it is quite tedious to do so for a {@link JavaAnnotation} not on the classpath (i.e. via {@link #getProperties()}).

+ * {@link #accept(ParameterVisitor)} offers an alternative by taking away traversal and casting logic when analysing + * potentially unknown {@link JavaAnnotation JavaAnnotations} of different parameter structures.
+ * Whether using this method or performing casting operations on {@link #getProperties()} might depend on the use case. + * For a known annotation where only a single known parameter is relevant, a solution like

+ *

+     * String value = (String) knownJavaAnnotation.get("value").get()
+     * 
+ * + * might be completely sufficient. However an analysis like "all class parameters of all annotations in package + * foo.bar should implement a certain interface" (or potentially nested annotations), this might not be an easily + * readable approach. {@link #accept(ParameterVisitor)} makes this use case fairly trivial:

+ * + *

+     * unknownJavaAnnotation.accept(new DefaultParameterVisitor() {
+     *    {@literal @}Override
+     *     public void visitClass(String propertyName, JavaClass javaClass) {
+     *         // do whatever check on the class parameter javaClass
+     *     }
+     * });
+ * + * Furthermore {@link ParameterVisitor} does exactly specify which cases can occur for {@link JavaAnnotation} parameters + * without the need to introspect and cast the values. In case traversal into nested {@link JavaAnnotation JavaAnnotations} is necessary, + * this also becomes quite simple:

+ * + *

+     * unknownJavaAnnotation.accept(new DefaultParameterVisitor() {
+     *     // parameter handling logic
+     *
+     *    {@literal @}Override
+     *     public void visitAnnotation(String propertyName, JavaAnnotation nestedAnnotation) {
+     *         nestedAnnotation.accept(this);
+     *     }
+     * });
+ * + * @param parameterVisitor A visitor which allows to implement behavior for different types of annotation parameters + */ + @PublicAPI(usage = ACCESS) + public void accept(ParameterVisitor parameterVisitor) { + JavaAnnotationParameterVisitorAcceptor.accept(getProperties(), parameterVisitor); + } + + /** + * Returns a compile safe proxied version of the respective {@link JavaAnnotation}. In other words, the result + * of as(MyAnnotation.class) will be of type MyAnnotation and allow property access + * in a compile safe manner. For this to work the respective {@link Annotation} including all + * referred parameter types must be on the classpath or an {@link Exception} will be thrown. + * Furthermore the respective {@link JavaAnnotation} must actually be an import of the passed parameter + * annotationType or a {@link RuntimeException} will likely occur. + * + * @param annotationType Any type implementing {@link Annotation} + * @param The type of the imported {@link Annotation} backing this {@link JavaAnnotation} + * @return A compile safe proxy of type {@link A} + */ @PublicAPI(usage = ACCESS) public A as(Class annotationType) { return AnnotationProxy.of(annotationType, this); @@ -208,4 +269,94 @@ public String getDescription() { public String toString() { return getClass().getSimpleName() + '{' + type.getName() + '}'; } + + /** + * A Visitor (compare {@link #accept(ParameterVisitor)}) offering possibilities to specify + * behavior when various types of {@link JavaAnnotation#getProperties()} are encountered.

+ * The list of declared methods is exhaustive, thus any legal parameter type of an {@link Annotation} + * is represented by the respective visit-method. + */ + @PublicAPI(usage = INHERITANCE) + public interface ParameterVisitor { + void visitBoolean(String propertyName, boolean propertyValue); + + void visitByte(String propertyName, byte propertyValue); + + void visitCharacter(String propertyName, Character propertyValue); + + void visitDouble(String propertyName, Double propertyValue); + + void visitFloat(String propertyName, Float propertyValue); + + void visitInteger(String propertyName, int propertyValue); + + void visitLong(String propertyName, Long propertyValue); + + void visitShort(String propertyName, Short propertyValue); + + void visitString(String propertyName, String propertyValue); + + void visitClass(String propertyName, JavaClass propertyValue); + + void visitEnumConstant(String propertyName, JavaEnumConstant propertyValue); + + void visitAnnotation(String propertyName, JavaAnnotation propertyValue); + } + + /** + * Default implementation of {@link ParameterVisitor} implementing a no-op + * behavior, i.e. this Visitor will do nothing on any type encountered.
+ * visit-methods for relevant types can be selectively overridden + * (compare {@link #accept(ParameterVisitor)}). + */ + @PublicAPI(usage = INHERITANCE) + public static class DefaultParameterVisitor implements ParameterVisitor { + @Override + public void visitBoolean(String propertyName, boolean propertyValue) { + } + + @Override + public void visitByte(String propertyName, byte propertyValue) { + } + + @Override + public void visitCharacter(String propertyName, Character propertyValue) { + } + + @Override + public void visitDouble(String propertyName, Double propertyValue) { + } + + @Override + public void visitFloat(String propertyName, Float propertyValue) { + } + + @Override + public void visitInteger(String propertyName, int propertyValue) { + } + + @Override + public void visitLong(String propertyName, Long propertyValue) { + } + + @Override + public void visitShort(String propertyName, Short propertyValue) { + } + + @Override + public void visitString(String propertyName, String propertyValue) { + } + + @Override + public void visitClass(String propertyName, JavaClass propertyValue) { + } + + @Override + public void visitEnumConstant(String propertyName, JavaEnumConstant propertyValue) { + } + + @Override + public void visitAnnotation(String propertyName, JavaAnnotation propertyValue) { + } + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java new file mode 100644 index 0000000000..8c21a89544 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java @@ -0,0 +1,151 @@ +/* + * Copyright 2014-2020 TNG Technology Consulting GmbH + * + * 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.tngtech.archunit.core.domain; + +import java.util.Map; + +/** + * Sole purpose is to keep all the ugly boring fluff out of {@link JavaAnnotation}. + * The order of the conditionals is based on an educated guess about the frequency of annotation parameter types. + */ +class JavaAnnotationParameterVisitorAcceptor { + static void accept(Map properties, JavaAnnotation.ParameterVisitor parameterVisitor) { + for (Map.Entry property : properties.entrySet()) { + if (property.getValue() instanceof Boolean) { + parameterVisitor.visitBoolean(property.getKey(), (Boolean) property.getValue()); + continue; + } + if (property.getValue() instanceof Integer) { + parameterVisitor.visitInteger(property.getKey(), (Integer) property.getValue()); + continue; + } + if (property.getValue() instanceof String) { + parameterVisitor.visitString(property.getKey(), (String) property.getValue()); + continue; + } + if (property.getValue() instanceof JavaClass) { + parameterVisitor.visitClass(property.getKey(), (JavaClass) property.getValue()); + continue; + } + if (property.getValue() instanceof JavaEnumConstant) { + parameterVisitor.visitEnumConstant(property.getKey(), (JavaEnumConstant) property.getValue()); + continue; + } + if (property.getValue() instanceof JavaAnnotation) { + parameterVisitor.visitAnnotation(property.getKey(), (JavaAnnotation) property.getValue()); + continue; + } + if (property.getValue() instanceof String[]) { + for (String value : ((String[]) property.getValue())) { + parameterVisitor.visitString(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof JavaClass[]) { + for (JavaClass value : ((JavaClass[]) property.getValue())) { + parameterVisitor.visitClass(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof JavaEnumConstant[]) { + for (JavaEnumConstant value : ((JavaEnumConstant[]) property.getValue())) { + parameterVisitor.visitEnumConstant(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof JavaAnnotation[]) { + for (JavaAnnotation value : ((JavaAnnotation[]) property.getValue())) { + parameterVisitor.visitAnnotation(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof Long) { + parameterVisitor.visitLong(property.getKey(), (Long) property.getValue()); + continue; + } + if (property.getValue() instanceof int[]) { + for (int value : ((int[]) property.getValue())) { + parameterVisitor.visitInteger(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof boolean[]) { + for (boolean value : ((boolean[]) property.getValue())) { + parameterVisitor.visitBoolean(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof long[]) { + for (long value : ((long[]) property.getValue())) { + parameterVisitor.visitLong(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof Byte) { + parameterVisitor.visitByte(property.getKey(), (Byte) property.getValue()); + continue; + } + if (property.getValue() instanceof byte[]) { + for (byte value : ((byte[]) property.getValue())) { + parameterVisitor.visitByte(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof Character) { + parameterVisitor.visitCharacter(property.getKey(), (Character) property.getValue()); + continue; + } + if (property.getValue() instanceof char[]) { + for (char value : ((char[]) property.getValue())) { + parameterVisitor.visitCharacter(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof Double) { + parameterVisitor.visitDouble(property.getKey(), (Double) property.getValue()); + continue; + } + if (property.getValue() instanceof double[]) { + for (double value : ((double[]) property.getValue())) { + parameterVisitor.visitDouble(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof Float) { + parameterVisitor.visitFloat(property.getKey(), (Float) property.getValue()); + continue; + } + if (property.getValue() instanceof float[]) { + for (float value : ((float[]) property.getValue())) { + parameterVisitor.visitFloat(property.getKey(), value); + } + continue; + } + if (property.getValue() instanceof Short) { + parameterVisitor.visitShort(property.getKey(), (Short) property.getValue()); + continue; + } + if (property.getValue() instanceof short[]) { + for (short value : ((short[]) property.getValue())) { + parameterVisitor.visitShort(property.getKey(), value); + } + // The danger of missing the continue when resorting is bigger than the "nuisance" of the obsolete continue + //noinspection UnnecessaryContinue + continue; + } + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java index a5a2edb339..695041c3d1 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java @@ -22,6 +22,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.domain.JavaAnnotation.DefaultParameterVisitor; import com.tngtech.archunit.core.domain.properties.HasAnnotations; import static com.google.common.base.Suppliers.memoize; @@ -210,41 +211,26 @@ private > Set annotatio } private > Set annotationDependencies(T annotated) { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (JavaAnnotation annotation : annotated.getAnnotations()) { + final ImmutableSet.Builder result = ImmutableSet.builder(); + for (final JavaAnnotation annotation : annotated.getAnnotations()) { result.addAll(Dependency.tryCreateFromAnnotation(annotation).asSet()); - result.addAll(annotationParametersDependencies(annotation)); - } - return result.build(); - } + annotation.accept(new DefaultParameterVisitor() { + @Override + public void visitClass(String propertyName, JavaClass javaClass) { + result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, javaClass).asSet()); + } - private Set annotationParametersDependencies(JavaAnnotation annotation) { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (Object value : annotation.getProperties().values()) { - if (value instanceof Object[]) { - Object[] values = (Object[]) value; - for (Object o : values) { - result.addAll(annotationParameterDependencies(annotation, o)); + @Override + public void visitEnumConstant(String propertyName, JavaEnumConstant enumConstant) { + result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, enumConstant.getDeclaringClass()).asSet()); } - } else { - result.addAll(annotationParameterDependencies(annotation, value)); - } - } - return result.build(); - } - private Set annotationParameterDependencies(JavaAnnotation origin, Object value) { - ImmutableSet.Builder result = ImmutableSet.builder(); - if (value instanceof JavaClass) { - JavaClass annotationMember = (JavaClass) value; - result.addAll(Dependency.tryCreateFromAnnotationMember(origin, annotationMember).asSet()); - } else if (value instanceof JavaEnumConstant) { - JavaEnumConstant enumConstant = (JavaEnumConstant) value; - result.addAll(Dependency.tryCreateFromAnnotationMember(origin, enumConstant.getDeclaringClass()).asSet()); - } else if (value instanceof JavaAnnotation) { - JavaAnnotation nestedAnnotation = (JavaAnnotation) value; - result.addAll(Dependency.tryCreateFromAnnotationMember(origin, nestedAnnotation.getRawType()).asSet()); - result.addAll(annotationParametersDependencies(nestedAnnotation)); + @Override + public void visitAnnotation(String propertyName, JavaAnnotation memberAnnotation) { + result.addAll(Dependency.tryCreateFromAnnotationMember(annotation, memberAnnotation.getRawType()).asSet()); + memberAnnotation.accept(this); + } + }); } return result.build(); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java index 2a09f02bda..dd78c5da88 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java @@ -31,6 +31,7 @@ import com.tngtech.archunit.core.domain.DomainObjectCreationContext; import com.tngtech.archunit.core.domain.ImportContext; import com.tngtech.archunit.core.domain.JavaAnnotation; +import com.tngtech.archunit.core.domain.JavaAnnotation.DefaultParameterVisitor; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaCodeUnit; @@ -346,38 +347,25 @@ void registerConstructors(Set constructors) { } void registerAnnotations(Collection> annotations) { - for (JavaAnnotation annotation : annotations) { + for (final JavaAnnotation annotation : annotations) { annotationTypeDependencies.put(annotation.getRawType(), annotation); - registerAnnotationParameters(annotation); - } - } + annotation.accept(new DefaultParameterVisitor() { + @Override + public void visitClass(String propertyName, JavaClass javaClass) { + annotationParameterTypeDependencies.put(javaClass, annotation); + } - void registerAnnotationParameters(JavaAnnotation annotation) { - for (Map.Entry entry : annotation.getProperties().entrySet()) { - Object value = entry.getValue(); - if (value.getClass().isArray()) { - if (!value.getClass().getComponentType().isPrimitive()) { - Object[] values = (Object[]) value; - for (Object o : values) { - registerAnnotationParameter(annotation, o); - } + @Override + public void visitEnumConstant(String propertyName, JavaEnumConstant enumConstant) { + annotationParameterTypeDependencies.put(enumConstant.getDeclaringClass(), annotation); } - } else { - registerAnnotationParameter(annotation, value); - } - } - } - private void registerAnnotationParameter(JavaAnnotation annotation, Object value) { - if (value instanceof JavaClass) { - annotationParameterTypeDependencies.put((JavaClass) value, annotation); - } else if (value instanceof JavaEnumConstant) { - JavaEnumConstant enumConstant = (JavaEnumConstant) value; - annotationParameterTypeDependencies.put(enumConstant.getDeclaringClass(), annotation); - } else if (value instanceof JavaAnnotation) { - JavaAnnotation memberAnnotation = (JavaAnnotation) value; - annotationParameterTypeDependencies.put(memberAnnotation.getRawType(), annotation); - registerAnnotationParameters(memberAnnotation); + @Override + public void visitAnnotation(String propertyName, JavaAnnotation memberAnnotation) { + annotationParameterTypeDependencies.put(memberAnnotation.getRawType(), annotation); + memberAnnotation.accept(this); + } + }); } } diff --git a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaAnnotationTest.java b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaAnnotationTest.java index f0f3c5e7d4..ec8c196bd8 100644 --- a/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaAnnotationTest.java +++ b/archunit/src/test/java/com/tngtech/archunit/core/domain/JavaAnnotationTest.java @@ -1,9 +1,17 @@ package com.tngtech.archunit.core.domain; +import java.util.Map; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import org.assertj.core.data.MapEntry; import org.junit.Test; import static com.tngtech.archunit.core.domain.TestUtils.importClasses; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.assertj.guava.api.Assertions.assertThat; public class JavaAnnotationTest { @Test @@ -51,6 +59,103 @@ public void description_of_method_annotation_parameter() { + "> on method <" + SomeClass.class.getName() + ".method()>"); } + @Test + public void visits_first_level_parameters() { + JavaClasses classes = importClasses( + AnnotationWithFirstLevelParameters.class, ClassWithAnnotationWithFirstLevelParameters.class, + Class1.class, Class2.class, SomeEnum.class); + JavaAnnotation annotation = classes.get(ClassWithAnnotationWithFirstLevelParameters.class) + .getAnnotationOfType(AnnotationWithFirstLevelParameters.class.getName()); + Map, JavaEnumConstant> enumConstants = getEnumConstants(classes.get(SomeEnum.class)); + ParametersStoringVisitor visitor = new ParametersStoringVisitor(); + + annotation.accept(visitor); + + MapEntry[] expectedParameters = expectedParametersWithOffset1("", classes, enumConstants); + assertThat(visitor.getVisitedParameters()) + .contains(expectedParameters) + .hasSize(expectedParameters.length); + } + + @Test + public void visits_second_level_parameters() { + JavaClasses classes = importClasses( + AnnotationWithFirstLevelParameters.class, AnnotationWithSecondLevelParameters.class, + ClassWithAnnotationWithSecondLevelParameters.class, + Class1.class, Class2.class, Class3.class, Class4.class, SomeEnum.class); + JavaAnnotation annotation = classes.get(ClassWithAnnotationWithSecondLevelParameters.class) + .getAnnotationOfType(AnnotationWithSecondLevelParameters.class.getName()); + Map, JavaEnumConstant> enumConstants = getEnumConstants(classes.get(SomeEnum.class)); + ParametersStoringVisitor visitor = new ParametersStoringVisitor(); + + annotation.accept(visitor); + + JavaAnnotation[] annotations = (JavaAnnotation[]) annotation.get("annotations").get(); + MapEntry[] expectedTopLevelParameters = createEntries( + entry("annotation", annotation.get("annotation").get()), + entry("annotations", (Object) annotations[0]), entry("annotations", (Object) annotations[1])); + MapEntry[] expectedParametersOfSingleAnnotation = expectedParametersWithOffset1("annotation.", classes, enumConstants); + MapEntry[] expectedParametersOfFirstAnnotationOfArray = expectedParametersWithOffset1("annotations.", classes, enumConstants); + MapEntry[] expectedParametersOfSecondAnnotationOfArray = expectedParametersWithOffset3("annotations.", classes, enumConstants); + + assertThat(visitor.getVisitedParameters()) + .contains(expectedTopLevelParameters) + .contains(expectedParametersOfSingleAnnotation) + .contains(expectedParametersOfFirstAnnotationOfArray) + .contains(expectedParametersOfSecondAnnotationOfArray) + .hasSize(expectedTopLevelParameters.length + expectedParametersOfSingleAnnotation.length + + expectedParametersOfFirstAnnotationOfArray.length + expectedParametersOfSecondAnnotationOfArray.length); + } + + private MapEntry[] expectedParametersWithOffset1(String prefix, JavaClasses classes, Map, JavaEnumConstant> enumConstants) { + return createEntries( + entry(prefix + "aBoolean", (Object) false), entry(prefix + "booleans", (Object) true), + entry(prefix + "aByte", (Object) (byte) 1), entry(prefix + "bytes", (Object) (byte) 1), entry(prefix + "bytes", (Object) (byte) 2), + entry(prefix + "aChar", (Object) 'a'), entry(prefix + "chars", (Object) 'a'), entry(prefix + "chars", (Object) 'b'), + entry(prefix + "aDouble", (Object) 1.0), entry(prefix + "doubles", (Object) 1.0), entry(prefix + "doubles", (Object) 2.0), + entry(prefix + "aFloat", (Object) 1.0F), entry(prefix + "floats", (Object) 1.0F), entry(prefix + "floats", (Object) 2.0F), + entry(prefix + "anInt", (Object) 1), entry(prefix + "ints", (Object) 1), entry(prefix + "ints", (Object) 2), + entry(prefix + "aLong", (Object) 1L), entry(prefix + "longs", (Object) 1L), entry(prefix + "longs", (Object) 2L), + entry(prefix + "aShort", (Object) (short) 1), entry(prefix + "shorts", (Object) (short) 1), entry(prefix + "shorts", (Object) (short) 2), + entry(prefix + "aString", (Object) "string1"), entry(prefix + "strings", (Object) "string1"), entry(prefix + "strings", (Object) "string2"), + entry(prefix + "aClass", (Object) classes.get(Class1.class)), + entry(prefix + "classes", (Object) classes.get(Class1.class)), entry(prefix + "classes", (Object) classes.get(Class2.class)), + entry(prefix + "anEnum", (Object) enumConstants.get(SomeEnum.FIRST)), + entry(prefix + "enums", (Object) enumConstants.get(SomeEnum.FIRST)), entry(prefix + "enums", (Object) enumConstants.get(SomeEnum.SECOND))); + } + + private MapEntry[] expectedParametersWithOffset3(String prefix, JavaClasses classes, Map, JavaEnumConstant> enumConstants) { + return createEntries( + entry(prefix + "aBoolean", (Object) true), entry(prefix + "booleans", (Object) false), + entry(prefix + "aByte", (Object) (byte) 3), entry(prefix + "bytes", (Object) (byte) 3), entry(prefix + "bytes", (Object) (byte) 4), + entry(prefix + "aChar", (Object) 'c'), entry(prefix + "chars", (Object) 'c'), entry(prefix + "chars", (Object) 'd'), + entry(prefix + "aDouble", (Object) 3.0), entry(prefix + "doubles", (Object) 3.0), entry(prefix + "doubles", (Object) 4.0), + entry(prefix + "aFloat", (Object) 3.0F), entry(prefix + "floats", (Object) 3.0F), entry(prefix + "floats", (Object) 4.0F), + entry(prefix + "anInt", (Object) 3), entry(prefix + "ints", (Object) 3), entry(prefix + "ints", (Object) 4), + entry(prefix + "aLong", (Object) 3L), entry(prefix + "longs", (Object) 3L), entry(prefix + "longs", (Object) 4L), + entry(prefix + "aShort", (Object) (short) 3), entry(prefix + "shorts", (Object) (short) 3), entry(prefix + "shorts", (Object) (short) 4), + entry(prefix + "aString", (Object) "string3"), entry(prefix + "strings", (Object) "string3"), entry(prefix + "strings", (Object) "string4"), + entry(prefix + "aClass", (Object) classes.get(Class3.class)), + entry(prefix + "classes", (Object) classes.get(Class3.class)), entry(prefix + "classes", (Object) classes.get(Class4.class)), + entry(prefix + "anEnum", (Object) enumConstants.get(SomeEnum.THIRD)), + entry(prefix + "enums", (Object) enumConstants.get(SomeEnum.THIRD)), entry(prefix + "enums", (Object) enumConstants.get(SomeEnum.FOURTH))); + } + + @SafeVarargs + @SuppressWarnings("unchecked") + private final MapEntry[] createEntries(MapEntry... entries) { + return (MapEntry[]) entries; + } + + private Map, JavaEnumConstant> getEnumConstants(JavaClass enumType) { + ImmutableMap.Builder, JavaEnumConstant> result = ImmutableMap.builder(); + for (Object enumConstant : enumType.reflect().getEnumConstants()) { + Enum reflectedConstant = (Enum) enumConstant; + result.put(reflectedConstant, enumType.getEnumConstant(reflectedConstant.name())); + } + return result.build(); + } + private @interface SomeAnnotation { SubAnnotation[] sub() default {}; } @@ -58,10 +163,263 @@ public void description_of_method_annotation_parameter() { private @interface SubAnnotation { } + @SuppressWarnings("unused") @SomeAnnotation(sub = @SubAnnotation) private static class SomeClass { @SomeAnnotation(sub = @SubAnnotation) void method() { } } + + private enum SomeEnum { + FIRST, SECOND, THIRD, FOURTH + } + + private @interface AnnotationWithFirstLevelParameters { + boolean aBoolean(); + + boolean[] booleans(); + + byte aByte(); + + byte[] bytes(); + + char aChar(); + + char[] chars(); + + double aDouble(); + + double[] doubles(); + + float aFloat(); + + float[] floats(); + + int anInt(); + + int[] ints(); + + long aLong(); + + long[] longs(); + + short aShort(); + + short[] shorts(); + + String aString(); + + String[] strings(); + + Class aClass(); + + Class[] classes(); + + SomeEnum anEnum(); + + SomeEnum[] enums(); + } + + @AnnotationWithFirstLevelParameters( + aBoolean = false, + booleans = {true}, + aByte = 1, + bytes = {1, 2}, + aChar = 'a', + chars = {'a', 'b'}, + aDouble = 1.0, + doubles = {1.0, 2.0}, + aFloat = 1.0F, + floats = {1.0F, 2.0F}, + anInt = 1, + ints = {1, 2}, + aLong = 1, + longs = {1, 2}, + aShort = 1, + shorts = {1, 2}, + aString = "string1", + strings = {"string1", "string2"}, + aClass = Class1.class, + classes = {Class1.class, Class2.class}, + anEnum = SomeEnum.FIRST, + enums = {SomeEnum.FIRST, SomeEnum.SECOND} + ) + private static class ClassWithAnnotationWithFirstLevelParameters { + } + + private @interface AnnotationWithSecondLevelParameters { + AnnotationWithFirstLevelParameters annotation(); + + AnnotationWithFirstLevelParameters[] annotations(); + } + + @AnnotationWithSecondLevelParameters( + annotation = @AnnotationWithFirstLevelParameters( + aBoolean = false, + booleans = {true}, + aByte = 1, + bytes = {1, 2}, + aChar = 'a', + chars = {'a', 'b'}, + aDouble = 1.0, + doubles = {1.0, 2.0}, + aFloat = 1.0F, + floats = {1.0F, 2.0F}, + anInt = 1, + ints = {1, 2}, + aLong = 1, + longs = {1, 2}, + aShort = 1, + shorts = {1, 2}, + aString = "string1", + strings = {"string1", "string2"}, + aClass = Class1.class, + classes = {Class1.class, Class2.class}, + anEnum = SomeEnum.FIRST, + enums = {SomeEnum.FIRST, SomeEnum.SECOND} + ), + annotations = { + @AnnotationWithFirstLevelParameters( + aBoolean = false, + booleans = {true}, + aByte = 1, + bytes = {1, 2}, + aChar = 'a', + chars = {'a', 'b'}, + aDouble = 1.0, + doubles = {1.0, 2.0}, + aFloat = 1.0F, + floats = {1.0F, 2.0F}, + anInt = 1, + ints = {1, 2}, + aLong = 1, + longs = {1, 2}, + aShort = 1, + shorts = {1, 2}, + aString = "string1", + strings = {"string1", "string2"}, + aClass = Class1.class, + classes = {Class1.class, Class2.class}, + anEnum = SomeEnum.FIRST, + enums = {SomeEnum.FIRST, SomeEnum.SECOND} + ), + @AnnotationWithFirstLevelParameters( + aBoolean = true, + booleans = {false}, + aByte = 3, + bytes = {3, 4}, + aChar = 'c', + chars = {'c', 'd'}, + aDouble = 3.0, + doubles = {3.0, 4.0}, + aFloat = 3.0F, + floats = {3.0F, 4.0F}, + anInt = 3, + ints = {3, 4}, + aLong = 3, + longs = {3, 4}, + aShort = 3, + shorts = {3, 4}, + aString = "string3", + strings = {"string3", "string4"}, + aClass = Class3.class, + classes = {Class3.class, Class4.class}, + anEnum = SomeEnum.THIRD, + enums = {SomeEnum.THIRD, SomeEnum.FOURTH} + ) + }) + private static class ClassWithAnnotationWithSecondLevelParameters { + } + + private static class Class1 { + } + + private static class Class2 { + } + + private static class Class3 { + } + + private static class Class4 { + } + + private static class ParametersStoringVisitor implements JavaAnnotation.ParameterVisitor { + private final String prefix; + private final Multimap visitedParameters; + + ParametersStoringVisitor() { + this("", HashMultimap.create()); + } + + private ParametersStoringVisitor(String prefix, Multimap visitedParameters) { + this.prefix = prefix; + this.visitedParameters = visitedParameters; + } + + public Multimap getVisitedParameters() { + return visitedParameters; + } + + @Override + public void visitBoolean(String propertyName, boolean propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitByte(String propertyName, byte propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitCharacter(String propertyName, Character propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitDouble(String propertyName, Double propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitFloat(String propertyName, Float propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitInteger(String propertyName, int propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitLong(String propertyName, Long propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitShort(String propertyName, Short propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitString(String propertyName, String propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitClass(String propertyName, JavaClass propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitEnumConstant(String propertyName, JavaEnumConstant propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + } + + @Override + public void visitAnnotation(String propertyName, JavaAnnotation propertyValue) { + visitedParameters.put(prefix + propertyName, propertyValue); + propertyValue.accept(new ParametersStoringVisitor(prefix + propertyName + ".", visitedParameters)); + } + } }