/* * Copyright 2017 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.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.MayResolveTypesViaReflection; import static com.google.common.base.Preconditions.checkArgument; @MayResolveTypesViaReflection(reason = "We depend on the classpath, if we proxy an annotation type") class AnnotationProxy { public static <A extends Annotation> A of(Class<A> annotationType, JavaAnnotation toProxy) { checkArgument(annotationType.getName().equals(toProxy.getType().getName()), "Requested annotation type %s is incompatible with %s of type %s", annotationType.getSimpleName(), JavaAnnotation.class.getSimpleName(), toProxy.getType().getSimpleName()); return newProxy(annotationType, toProxy); } @SuppressWarnings("unchecked") // annotationType A will be implemented private static <A extends Annotation> A newProxy(Class<A> annotationType, JavaAnnotation toProxy) { return (A) Proxy.newProxyInstance( annotationType.getClassLoader(), new Class[]{annotationType}, new AnnotationMethodInvocationHandler(annotationType, toProxy)); } private static class AnnotationMethodInvocationHandler implements InvocationHandler { private final JavaAnnotation toProxy; private final Conversions conversions; private final Map<MethodKey, SpecificHandler> handlersByMethod; private AnnotationMethodInvocationHandler(Class<?> annotationType, JavaAnnotation toProxy) { this.toProxy = toProxy; conversions = initConversions(annotationType); handlersByMethod = initHandlersByMethod(annotationType, toProxy, conversions); } private Conversions initConversions(Class<?> annotationType) { JavaClassConversion javaClassConversion = new JavaClassConversion(annotationType.getClassLoader()); JavaEnumConstantConversion enumConversion = new JavaEnumConstantConversion(); JavaAnnotationConversion annotationConversion = new JavaAnnotationConversion(annotationType.getClassLoader()); return new Conversions( javaClassConversion, new JavaClassArrayConversion(javaClassConversion), enumConversion, new JavaEnumConstantArrayConversion(enumConversion), annotationConversion, new JavaAnnotationArrayConversion(annotationConversion)); } private ImmutableMap<MethodKey, SpecificHandler> initHandlersByMethod( Class<?> annotationType, JavaAnnotation toProxy, Conversions conversions) { return ImmutableMap.of( new MethodKey("annotationType"), new ConstantReturnValueHandler(annotationType), new MethodKey("equals", Object.class.getName()), new EqualsHandler(), new MethodKey("hashCode"), new HashCodeHandler(), new MethodKey("toString"), new ToStringHandler(annotationType, toProxy, conversions) ); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodKey key = MethodKey.of(method); if (handlersByMethod.containsKey(key)) { return handlersByMethod.get(key).handle(proxy, method, args); } Object result = toProxy.get(method.getName()).or(method.getDefaultValue()); return conversions.convertIfNecessary(result, method.getReturnType()); } } private interface Conversion<F> { Object convert(F input, Class<?> returnType); boolean canHandle(Class<?> returnType); } private static class JavaClassConversion implements Conversion<JavaClass> { private final ClassLoader classLoader; private JavaClassConversion(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public Class<?> convert(JavaClass input, Class<?> returnType) { return JavaType.From.javaClass(input).resolveClass(classLoader); } @Override public boolean canHandle(Class<?> returnType) { return Class.class.isAssignableFrom(returnType); } } private static class JavaClassArrayConversion implements Conversion<Object[]> { private final JavaClassConversion javaClassConversion; private JavaClassArrayConversion(JavaClassConversion javaClassConversion) { this.javaClassConversion = javaClassConversion; } @Override public Object convert(Object[] input, Class<?> returnType) { return convertArray(input, javaClassConversion, returnType.getComponentType()); } @Override public boolean canHandle(Class<?> returnType) { return Class[].class.isAssignableFrom(returnType); } } private static class JavaEnumConstantConversion implements Conversion<JavaEnumConstant> { @Override public Enum<?> convert(JavaEnumConstant input, Class<?> returnType) { for (Object constant : JavaType.From.javaClass(input.getDeclaringClass()).resolveClass().getEnumConstants()) { Enum<?> anEnum = (Enum<?>) constant; if (anEnum.name().equals(input.name())) { return anEnum; } } throw new IllegalStateException(String.format( "Couldn't find Enum Constant %s.%s", input.getDeclaringClass().getSimpleName(), input.name())); } @Override public boolean canHandle(Class<?> returnType) { return returnType.isEnum(); } } private static class JavaEnumConstantArrayConversion implements Conversion<Object[]> { private final JavaEnumConstantConversion enumConversion; private JavaEnumConstantArrayConversion(JavaEnumConstantConversion enumConversion) { this.enumConversion = enumConversion; } @Override public Object convert(Object[] input, Class<?> returnType) { return convertArray(input, enumConversion, returnType.getComponentType()); } @Override public boolean canHandle(Class<?> returnType) { return returnType.getComponentType() != null && returnType.getComponentType().isEnum(); } } private static class JavaAnnotationConversion implements Conversion<JavaAnnotation> { private final ClassLoader classLoader; private JavaAnnotationConversion(ClassLoader classLoader) { this.classLoader = classLoader; } @Override public Annotation convert(JavaAnnotation input, Class<?> returnType) { // JavaAnnotation#getType() will return the type name of a Class<? extends Annotation> @SuppressWarnings("unchecked") Class<? extends Annotation> type = (Class<? extends Annotation>) JavaType.From.javaClass(input.getType()).resolveClass(classLoader); return AnnotationProxy.of(type, input); } @Override public boolean canHandle(Class<?> returnType) { return returnType.isAnnotation(); } } private static class JavaAnnotationArrayConversion implements Conversion<Object[]> { private final JavaAnnotationConversion annotationConversion; private JavaAnnotationArrayConversion(JavaAnnotationConversion annotationConversion) { this.annotationConversion = annotationConversion; } @Override public Object convert(Object[] input, Class<?> returnType) { return convertArray(input, annotationConversion, returnType.getComponentType()); } @Override public boolean canHandle(Class<?> returnType) { return returnType.getComponentType() != null && returnType.getComponentType().isAnnotation(); } } @SuppressWarnings("unchecked") // canHandle must ensure this private static <F> Object[] convertArray(Object[] input, Conversion<F> elementConversion, Class<?> targetType) { Object[] result = (Object[]) Array.newInstance(targetType, input.length); for (int i = 0; i < input.length; i++) { result[i] = elementConversion.convert((F) input[i], targetType); } return result; } private interface SpecificHandler { Object handle(Object proxy, Method method, Object[] args); } private static class ConstantReturnValueHandler implements SpecificHandler { private final Object value; private ConstantReturnValueHandler(Object value) { this.value = value; } @Override public Object handle(Object proxy, Method method, Object[] args) { return value; } } private static class EqualsHandler implements SpecificHandler { @Override public Object handle(Object proxy, Method method, Object[] args) { return proxy == args[0]; } } private static class HashCodeHandler implements SpecificHandler { @Override public Object handle(Object proxy, Method method, Object[] args) { return System.identityHashCode(proxy); } } private static class ToStringHandler implements SpecificHandler { private final Class<?> annotationType; private final JavaAnnotation toProxy; private final Conversions conversions; private ToStringHandler(Class<?> annotationType, JavaAnnotation toProxy, Conversions conversions) { this.annotationType = annotationType; this.toProxy = toProxy; this.conversions = conversions; } @Override public Object handle(Object proxy, Method method, Object[] args) { return String.format("@%s(%s)", toProxy.getType().getName(), propertyStrings()); } private String propertyStrings() { Set<String> properties = new HashSet<>(); for (Map.Entry<String, Object> entry : toProxy.getProperties().entrySet()) { Class<?> returnType = getDeclaredMethod(entry.getKey()).getReturnType(); String value = format(conversions.convertIfNecessary(entry.getValue(), returnType)); properties.add(entry.getKey() + "=" + value); } return Joiner.on(", ").join(properties); } private Method getDeclaredMethod(String name) { try { return annotationType.getDeclaredMethod(name); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } private String format(Object input) { if (!input.getClass().isArray()) { return "" + input; } List<String> elemToString = new ArrayList<>(); for (int i = 0; i < Array.getLength(input); i++) { elemToString.add("" + format(Array.get(input, i))); } return "[" + Joiner.on(", ").join(elemToString) + "]"; } } private static class Conversions { private final Set<Conversion<?>> conversions; private Conversions(Conversion<?>... conversions) { this.conversions = ImmutableSet.copyOf(conversions); } <T> Object convertIfNecessary(T result, Class<?> returnType) { if (returnType.isInstance(result)) { return result; } return tryFindConversionFor(returnType).or(new NoOpConversion<>()).convert(result, returnType); } private static class NoOpConversion<T> implements Conversion<T> { @Override public T convert(T input, Class<?> returnType) { return input; } @Override public boolean canHandle(Class<?> returnType) { return true; } } @SuppressWarnings("unchecked") // Trust sanity of canHandle(..) private <F> Optional<Conversion<F>> tryFindConversionFor(Class<?> returnType) { for (Conversion<?> conversion : conversions) { if (conversion.canHandle(returnType)) { return Optional.of((Conversion<F>) conversion); } } return Optional.absent(); } } private static class MethodKey { private final String name; private final List<String> paramTypeNames; private MethodKey(String name, String... paramTypeNames) { this(name, ImmutableList.copyOf(paramTypeNames)); } private MethodKey(String name, ImmutableList<String> paramTypeNames) { this.name = name; this.paramTypeNames = paramTypeNames; } @Override public int hashCode() { return Objects.hash(name, paramTypeNames); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final MethodKey other = (MethodKey) obj; return Objects.equals(this.name, other.name) && Objects.equals(this.paramTypeNames, other.paramTypeNames); } public static MethodKey of(Method method) { ImmutableList.Builder<String> paramTypeNames = ImmutableList.builder(); for (Class<?> type : method.getParameterTypes()) { paramTypeNames.add(type.getName()); } return new MethodKey(method.getName(), paramTypeNames.build()); } } }