Skip to content

Commit

Permalink
unify annotation parameter analysis
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
codecholeric authored and Alen Kosanović committed Jan 9, 2020
1 parent edbff5a commit bf0ac80
Show file tree
Hide file tree
Showing 5 changed files with 695 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
* <ul>
* <li>Class&lt;?&gt; --&gt; TypeDetails{clazz}</li>
* <li>Class&lt;?&gt;[] --&gt; [TypeDetails{clazz},...]</li>
* <li>Class&lt;?&gt; --&gt; JavaClass{clazz}</li>
* <li>Class&lt;?&gt;[] --&gt; [JavaClass{clazz},...]</li>
* <li>Enum --&gt; JavaEnumConstant</li>
* <li>Enum[] --&gt; [JavaEnumConstant,...]</li>
* <li>Annotation --&gt; JavaAnnotation</li>
* <li>Annotation[] --&gt; [JavaAnnotation,...]</li>
* <li>anyOtherType --&gt; anyOtherType</li>
* </ul>
*/
Expand All @@ -194,6 +197,64 @@ public Map<String, Object> getProperties() {
return values;
}

/**
* Simple implementation of the Visitor pattern (compare e.g.
* <a href="https://en.wikipedia.org/wiki/Visitor_pattern">https://en.wikipedia.org/wiki/Visitor_pattern</a>).<br><br>
* 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()}).<br><br>
* {@link #accept(ParameterVisitor)} offers an alternative by taking away traversal and casting logic when analysing
* potentially unknown {@link JavaAnnotation JavaAnnotations} of different parameter structures.<br>
* 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<br><br>
* <pre><code>
* String value = (String) knownJavaAnnotation.get("value").get()
* </code></pre>
*
* might be completely sufficient. However an analysis like "all class parameters of all annotations in package
* <code>foo.bar</code> 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:<br><br>
*
* <pre><code>
* unknownJavaAnnotation.accept(new DefaultParameterVisitor() {
* {@literal @}Override
* public void visitClass(String propertyName, JavaClass javaClass) {
* // do whatever check on the class parameter javaClass
* }
* });</code></pre>
*
* 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:<br><br>
*
* <pre><code>
* unknownJavaAnnotation.accept(new DefaultParameterVisitor() {
* // parameter handling logic
*
* {@literal @}Override
* public void visitAnnotation(String propertyName, JavaAnnotation<?> nestedAnnotation) {
* nestedAnnotation.accept(this);
* }
* });</code></pre>
*
* @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 <code>as(MyAnnotation.class)</code> will be of type <code>MyAnnotation</code> and allow property access
* in a compile safe manner. For this to work the respective <code>{@link Annotation}</code> 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
* <code>annotationType</code> or a {@link RuntimeException} will likely occur.
*
* @param annotationType Any type implementing {@link Annotation}
* @param <A> 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 extends Annotation> A as(Class<A> annotationType) {
return AnnotationProxy.of(annotationType, this);
Expand All @@ -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.<br><br>
* The list of declared methods is exhaustive, thus any legal parameter type of an {@link Annotation}
* is represented by the respective <code>visit</code>-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.<br>
* <code>visit</code>-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) {
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> properties, JavaAnnotation.ParameterVisitor parameterVisitor) {
for (Map.Entry<String, Object> 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;
}
}
}
}
Loading

0 comments on commit bf0ac80

Please sign in to comment.