/* * Copyright 2011 the original author or authors. * * 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 org.gradle.tooling.internal.adapter; import com.google.common.base.Optional; import org.gradle.api.Nullable; import org.gradle.internal.UncheckedException; import org.gradle.internal.reflect.DirectInstantiator; import org.gradle.internal.time.CountdownTimer; import org.gradle.internal.time.Timers; import org.gradle.internal.typeconversion.EnumFromCharSequenceNotationParser; import org.gradle.internal.typeconversion.NotationConverterToNotationParserAdapter; import org.gradle.internal.typeconversion.NotationParser; import org.gradle.internal.typeconversion.TypeConversionException; import org.gradle.tooling.model.DomainObjectSet; import org.gradle.tooling.model.internal.Exceptions; import org.gradle.tooling.model.internal.ImmutableDomainObjectSet; import java.io.IOException; import java.io.Serializable; import java.lang.ref.SoftReference; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Adapts some source object to some target view type. */ public class ProtocolToModelAdapter implements ObjectGraphAdapter { private static final ViewDecoration NO_OP_MAPPER = new NoOpDecoration(); private static final TargetTypeProvider IDENTITY_TYPE_PROVIDER = new TargetTypeProvider() { public <T> Class<? extends T> getTargetType(Class<T> initialTargetType, Object protocolObject) { return initialTargetType; } }; private static final ReflectionMethodInvoker REFLECTION_METHOD_INVOKER = new ReflectionMethodInvoker(); private static final TypeInspector TYPE_INSPECTOR = new TypeInspector(); private static final CollectionMapper COLLECTION_MAPPER = new CollectionMapper(); private static final Object[] EMPTY = new Object[0]; private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; private static final Method EQUALS_METHOD; private static final Method HASHCODE_METHOD; private final TargetTypeProvider targetTypeProvider; static { Method equalsMethod; Method hashCodeMethod; try { equalsMethod = Object.class.getMethod("equals", Object.class); hashCodeMethod = Object.class.getMethod("hashCode"); } catch (NoSuchMethodException e) { throw UncheckedException.throwAsUncheckedException(e); } EQUALS_METHOD = equalsMethod; HASHCODE_METHOD = hashCodeMethod; } public ProtocolToModelAdapter() { this(IDENTITY_TYPE_PROVIDER); } public ProtocolToModelAdapter(TargetTypeProvider targetTypeProvider) { this.targetTypeProvider = targetTypeProvider; } /** * Creates an adapter for a single object graph. Each object adapted by the returned adapter is treated as part of the same object graph, for the purposes of caching etc. */ public ObjectGraphAdapter newGraph() { final ViewGraphDetails graphDetails = new ViewGraphDetails(targetTypeProvider); return new ObjectGraphAdapter() { @Override public <T> T adapt(Class<T> targetType, Object sourceObject) { return createView(targetType, sourceObject, NO_OP_MAPPER, graphDetails); } @Override public <T> ViewBuilder<T> builder(Class<T> viewType) { return new DefaultViewBuilder<T>(viewType, graphDetails); } }; } /** * Adapts the source object to a view object. */ @Override public <T> T adapt(Class<T> targetType, Object sourceObject) { if (sourceObject == null) { return null; } return createView(targetType, sourceObject, NO_OP_MAPPER, new ViewGraphDetails(targetTypeProvider)); } /** * Creates a builder for views of the given type. */ @Override public <T> ViewBuilder<T> builder(final Class<T> viewType) { return new DefaultViewBuilder<T>(viewType); } private static <T> T createView(Class<T> targetType, Object sourceObject, ViewDecoration decoration, ViewGraphDetails graphDetails) { if (sourceObject == null) { return null; } // Calculate the actual type Class<? extends T> viewType = graphDetails.typeProvider.getTargetType(targetType, sourceObject); if (viewType.isInstance(sourceObject)) { return viewType.cast(sourceObject); } if (targetType.isEnum()) { return adaptToEnum(targetType, sourceObject); } // Restrict the decorations to those required to decorate all views reachable from this type ViewDecoration decorationsForThisType = decoration.isNoOp() ? decoration : decoration.restrictTo(TYPE_INSPECTOR.getReachableTypes(targetType)); ViewKey viewKey = new ViewKey(viewType, sourceObject, decorationsForThisType); Object view = graphDetails.views.get(viewKey); if (view != null) { return targetType.cast(view); } // Create a proxy InvocationHandlerImpl handler = new InvocationHandlerImpl(targetType, sourceObject, decorationsForThisType, graphDetails); Object proxy = Proxy.newProxyInstance(viewType.getClassLoader(), new Class<?>[]{viewType}, handler); handler.attachProxy(proxy); return viewType.cast(proxy); } private static <T, S> T adaptToEnum(Class<T> targetType, S sourceObject) { try { String literal; if (sourceObject instanceof Enum) { literal = ((Enum<?>) sourceObject).name(); } else if (sourceObject instanceof String) { literal = (String) sourceObject; } else { literal = sourceObject.toString(); } NotationParser<String, T> parser = new NotationConverterToNotationParserAdapter<String, T>(new EnumFromCharSequenceNotationParser(targetType)); T parsedLiteral = parser.parseNotation(literal); return targetType.cast(parsedLiteral); } catch (TypeConversionException e) { throw new IllegalArgumentException(String.format("Can't convert '%s' to enum type '%s'", sourceObject, targetType.getSimpleName()), e); } } private static Object convert(Type targetType, Object sourceObject, ViewDecoration decoration, ViewGraphDetails graphDetails) { if (targetType instanceof ParameterizedType) { ParameterizedType parameterizedTargetType = (ParameterizedType) targetType; if (parameterizedTargetType.getRawType() instanceof Class) { Class<?> rawClass = (Class<?>) parameterizedTargetType.getRawType(); if (Iterable.class.isAssignableFrom(rawClass)) { Type targetElementType = getElementType(parameterizedTargetType, 0); return convertCollectionInternal(rawClass, targetElementType, (Iterable<?>) sourceObject, decoration, graphDetails); } if (Map.class.isAssignableFrom(rawClass)) { Type targetKeyType = getElementType(parameterizedTargetType, 0); Type targetValueType = getElementType(parameterizedTargetType, 1); return convertMap(rawClass, targetKeyType, targetValueType, (Map<?, ?>) sourceObject, decoration, graphDetails); } } } if (targetType instanceof Class) { if (((Class) targetType).isPrimitive()) { return sourceObject; } return createView((Class) targetType, sourceObject, decoration, graphDetails); } throw new UnsupportedOperationException(String.format("Cannot convert object of %s to %s.", sourceObject.getClass(), targetType)); } private static Map<Object, Object> convertMap(Class<?> mapClass, Type targetKeyType, Type targetValueType, Map<?, ?> sourceObject, ViewDecoration decoration, ViewGraphDetails graphDetails) { Map<Object, Object> convertedElements = COLLECTION_MAPPER.createEmptyMap(mapClass); for (Map.Entry<?, ?> entry : sourceObject.entrySet()) { convertedElements.put(convert(targetKeyType, entry.getKey(), decoration, graphDetails), convert(targetValueType, entry.getValue(), decoration, graphDetails)); } return convertedElements; } private static Object convertCollectionInternal(Class<?> collectionClass, Type targetElementType, Iterable<?> sourceObject, ViewDecoration decoration, ViewGraphDetails graphDetails) { Collection<Object> convertedElements = COLLECTION_MAPPER.createEmptyCollection(collectionClass); convertCollectionInternal(convertedElements, targetElementType, sourceObject, decoration, graphDetails); if (collectionClass.equals(DomainObjectSet.class)) { return new ImmutableDomainObjectSet(convertedElements); } else { return convertedElements; } } private static void convertCollectionInternal(Collection<Object> targetCollection, Type targetElementType, Iterable<?> sourceObject, ViewDecoration viewDecoration, ViewGraphDetails graphDetails) { for (Object element : sourceObject) { targetCollection.add(convert(targetElementType, element, viewDecoration, graphDetails)); } } private static Type getElementType(ParameterizedType type, int index) { Type elementType = type.getActualTypeArguments()[index]; if (elementType instanceof WildcardType) { WildcardType wildcardType = (WildcardType) elementType; return wildcardType.getUpperBounds()[0]; } return elementType; } /** * Unpacks the source object from a given view object. */ public Object unpack(Object viewObject) { if (!Proxy.isProxyClass(viewObject.getClass()) || !(Proxy.getInvocationHandler(viewObject) instanceof InvocationHandlerImpl)) { throw new IllegalArgumentException("The given object is not a view object"); } InvocationHandlerImpl handler = (InvocationHandlerImpl) Proxy.getInvocationHandler(viewObject); return handler.sourceObject; } private static class ViewGraphDetails implements Serializable { // Transient, don't serialize all the views that happen to have been visited, recreate them when visited via the deserialized view private transient Map<ViewKey, Object> views = new HashMap<ViewKey, Object>(); private final TargetTypeProvider typeProvider; ViewGraphDetails(TargetTypeProvider typeProvider) { this.typeProvider = typeProvider; } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); views = new HashMap<ViewKey, Object>(); } } private static class ViewKey implements Serializable { private final Class<?> type; private final Object source; private final ViewDecoration viewDecoration; ViewKey(Class<?> type, Object source, ViewDecoration viewDecoration) { this.type = type; this.source = source; this.viewDecoration = viewDecoration; } @Override public boolean equals(Object obj) { ViewKey other = (ViewKey) obj; return other.source == source && other.type.equals(type) && other.viewDecoration.equals(viewDecoration); } @Override public int hashCode() { return type.hashCode() ^ System.identityHashCode(source) ^ viewDecoration.hashCode(); } } private static class InvocationHandlerImpl implements InvocationHandler, Serializable { private final Class<?> targetType; private final Object sourceObject; private final ViewDecoration decoration; private final ViewGraphDetails graphDetails; private Object proxy; // Recreate the invoker when deserialized, rather than serialize all its state private transient MethodInvoker invoker; InvocationHandlerImpl(Class<?> targetType, Object sourceObject, ViewDecoration decoration, ViewGraphDetails graphDetails) { this.targetType = targetType; this.sourceObject = sourceObject; this.decoration = decoration; this.graphDetails = graphDetails; setup(); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); setup(); graphDetails.views.put(new ViewKey(targetType, sourceObject, decoration), proxy); } private void setup() { List<MethodInvoker> invokers = new ArrayList<MethodInvoker>(); invokers.add(REFLECTION_METHOD_INVOKER); decoration.collectInvokers(sourceObject, targetType, invokers); MethodInvoker mixInMethodInvoker = invokers.size() == 1 ? invokers.get(0) : new ChainedMethodInvoker(invokers); invoker = new SupportedPropertyInvoker( new SafeMethodInvoker( new PropertyCachingMethodInvoker( new AdaptingMethodInvoker(decoration, graphDetails, mixInMethodInvoker)))); } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o == null || o.getClass() != getClass()) { return false; } InvocationHandlerImpl other = (InvocationHandlerImpl) o; return sourceObject.equals(other.sourceObject); } @Override public int hashCode() { return sourceObject.hashCode(); } public Object invoke(Object target, Method method, Object[] params) throws Throwable { if (EQUALS_METHOD.equals(method)) { Object param = params[0]; if (param == null || !Proxy.isProxyClass(param.getClass())) { return false; } InvocationHandler other = Proxy.getInvocationHandler(param); return equals(other); } else if (HASHCODE_METHOD.equals(method)) { return hashCode(); } MethodInvocation invocation = new MethodInvocation(method.getName(), method.getReturnType(), method.getGenericReturnType(), method.getParameterTypes(), target, targetType, sourceObject, params); invoker.invoke(invocation); if (!invocation.found()) { String methodName = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()"; throw Exceptions.unsupportedMethod(methodName); } return invocation.getResult(); } void attachProxy(Object proxy) { this.proxy = proxy; graphDetails.views.put(new ViewKey(targetType, sourceObject, decoration), proxy); } } private static class ChainedMethodInvoker implements MethodInvoker { private final MethodInvoker[] invokers; private ChainedMethodInvoker(List<MethodInvoker> invokers) { this.invokers = invokers.toArray(new MethodInvoker[0]); } public void invoke(MethodInvocation method) throws Throwable { for (int i = 0; !method.found() && i < invokers.length; i++) { MethodInvoker invoker = invokers[i]; invoker.invoke(method); } } } private static class AdaptingMethodInvoker implements MethodInvoker { private final ViewDecoration decoration; private final ViewGraphDetails graphDetails; private final MethodInvoker next; private AdaptingMethodInvoker(ViewDecoration decoration, ViewGraphDetails graphDetails, MethodInvoker next) { this.decoration = decoration; this.graphDetails = graphDetails; this.next = next; } public void invoke(MethodInvocation invocation) throws Throwable { next.invoke(invocation); if (invocation.found() && invocation.getResult() != null) { invocation.setResult(convert(invocation.getGenericReturnType(), invocation.getResult(), decoration, graphDetails)); } } } private static class MethodInvocationCache { private final Map<MethodInvocationKey, Optional<Method>> store = new HashMap<MethodInvocationKey, Optional<Method>>(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final static long MINIMAL_CLEANUP_INTERVAL = 30000; // For stats we don't really care about thread safety private int cacheMiss; private int cacheHit; private int evict; private CountdownTimer cleanupTimer = Timers.startTimer(MINIMAL_CLEANUP_INTERVAL); private static class MethodInvocationKey { private final SoftReference<Class<?>> lookupClass; private final String methodName; private final SoftReference<Class<?>[]> parameterTypes; private final int hashCode; private MethodInvocationKey(Class<?> lookupClass, String methodName, Class<?>[] parameterTypes) { this.lookupClass = new SoftReference<Class<?>>(lookupClass); this.methodName = methodName; this.parameterTypes = new SoftReference<Class<?>[]>(parameterTypes); // hashcode will always be used, so we precompute it in order to make sure we // won't compute it multiple times during comparisons int result = lookupClass != null ? lookupClass.hashCode() : 0; result = 31 * result + (methodName != null ? methodName.hashCode() : 0); result = 31 * result + Arrays.hashCode(parameterTypes); this.hashCode = result; } public boolean isDirty() { return lookupClass.get() == null || parameterTypes.get() == null; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } MethodInvocationKey that = (MethodInvocationKey) o; if (isDirty() && that.isDirty()) { return true; } if (!eq(lookupClass, that.lookupClass)) { return false; } if (!methodName.equals(that.methodName)) { return false; } return eq(parameterTypes, that.parameterTypes); } private static boolean eq(SoftReference<?> aRef, SoftReference<?> bRef) { Object a = aRef.get(); Object b = bRef.get(); return eq(a, b); } private static boolean eq(Object a, Object b) { if (a == b) { return true; } if (a == null) { return false; } if (a.getClass().isArray()) { return Arrays.equals((Object[]) a, (Object[]) b); } return a.equals(b); } @Override public int hashCode() { return hashCode; } } public Method get(MethodInvocation invocation) { Class<?> owner = invocation.getDelegate().getClass(); String name = invocation.getName(); Class<?>[] parameterTypes = invocation.getParameterTypes(); MethodInvocationKey key = new MethodInvocationKey( owner, name, parameterTypes ); lock.readLock().lock(); Optional<Method> cached = store.get(key); if (cached == null) { cacheMiss++; lock.readLock().unlock(); lock.writeLock().lock(); try { cached = store.get(key); if (cached == null) { cached = lookup(owner, name, parameterTypes); if (cacheMiss % 10 == 0) { removeDirtyEntries(); } store.put(key, cached); } lock.readLock().lock(); } finally { lock.writeLock().unlock(); } } else { cacheHit++; } try { return cached.orNull(); } finally { lock.readLock().unlock(); } } /** * Removes dirty entries from the cache. Calling System.currentTimeMillis() is costly so we should try to limit calls to this method. This method will only trigger cleanup at most once per * 30s. */ private void removeDirtyEntries() { if (!cleanupTimer.hasExpired()) { return; } lock.writeLock().lock(); try { for (MethodInvocationKey key : new LinkedList<MethodInvocationKey>(store.keySet())) { if (key.isDirty()) { evict++; store.remove(key); } } } finally { cleanupTimer.reset(); lock.writeLock().unlock(); } } private static Optional<Method> lookup(Class<?> sourceClass, String methodName, Class<?>[] parameterTypes) { Method match; try { match = sourceClass.getMethod(methodName, parameterTypes); } catch (NoSuchMethodException e) { return Optional.absent(); } LinkedList<Class<?>> queue = new LinkedList<Class<?>>(); queue.add(sourceClass); while (!queue.isEmpty()) { Class<?> c = queue.removeFirst(); try { match = c.getMethod(methodName, parameterTypes); } catch (NoSuchMethodException e) { // ignore } for (Class<?> interfaceType : c.getInterfaces()) { queue.addFirst(interfaceType); } if (c.getSuperclass() != null) { queue.addFirst(c.getSuperclass()); } } match.setAccessible(true); return Optional.of(match); } @Override public String toString() { return "Cache size: " + store.size() + " Hits: " + cacheHit + " Miss: " + cacheMiss + " Evicted: " + evict; } } private static class ReflectionMethodInvoker implements MethodInvoker { private final MethodInvocationCache lookupCache = new MethodInvocationCache(); public void invoke(MethodInvocation invocation) throws Throwable { Method targetMethod = locateMethod(invocation); if (targetMethod == null) { return; } Object returnValue; try { returnValue = targetMethod.invoke(invocation.getDelegate(), invocation.getParameters()); } catch (InvocationTargetException e) { throw e.getCause(); } invocation.setResult(returnValue); } private Method locateMethod(MethodInvocation invocation) { return lookupCache.get(invocation); } } private static class PropertyCachingMethodInvoker implements MethodInvoker { private final Map<String, Object> properties = new HashMap<String, Object>(); private final Set<String> unknown = new HashSet<String>(); private final MethodInvoker next; private PropertyCachingMethodInvoker(MethodInvoker next) { this.next = next; } public void invoke(MethodInvocation method) throws Throwable { if (method.isGetter()) { if (properties.containsKey(method.getName())) { method.setResult(properties.get(method.getName())); return; } if (unknown.contains(method.getName())) { return; } Object value; next.invoke(method); if (!method.found()) { unknown.add(method.getName()); return; } value = method.getResult(); properties.put(method.getName(), value); return; } next.invoke(method); } } private static class SafeMethodInvoker implements MethodInvoker { private final MethodInvoker next; private SafeMethodInvoker(MethodInvoker next) { this.next = next; } public void invoke(MethodInvocation invocation) throws Throwable { next.invoke(invocation); if (invocation.found() || invocation.getParameterTypes().length != 1 || !invocation.isIsOrGet()) { return; } MethodInvocation getterInvocation = new MethodInvocation(invocation.getName(), invocation.getReturnType(), invocation.getGenericReturnType(), EMPTY_CLASS_ARRAY, invocation.getView(), invocation.getViewType(), invocation.getDelegate(), EMPTY); next.invoke(getterInvocation); if (getterInvocation.found() && getterInvocation.getResult() != null) { invocation.setResult(getterInvocation.getResult()); } else { invocation.setResult(invocation.getParameters()[0]); } } } private static class SupportedPropertyInvoker implements MethodInvoker { private final MethodInvoker next; private SupportedPropertyInvoker(MethodInvoker next) { this.next = next; } public void invoke(MethodInvocation invocation) throws Throwable { next.invoke(invocation); if (invocation.found()) { return; } String methodName = invocation.getName(); boolean isSupportMethod = methodName.length() > 11 && methodName.startsWith("is") && methodName.endsWith("Supported"); if (!isSupportMethod) { return; } String getterName = "get" + methodName.substring(2, methodName.length() - 9); MethodInvocation getterInvocation = new MethodInvocation(getterName, invocation.getReturnType(), invocation.getGenericReturnType(), EMPTY_CLASS_ARRAY, invocation.getView(), invocation.getViewType(), invocation.getDelegate(), EMPTY); next.invoke(getterInvocation); invocation.setResult(getterInvocation.found()); } } private static class BeanMixInMethodInvoker implements MethodInvoker { private final Object instance; private final MethodInvoker next; BeanMixInMethodInvoker(Object instance, MethodInvoker next) { this.instance = instance; this.next = next; } @Override public void invoke(MethodInvocation invocation) throws Throwable { MethodInvocation beanInvocation = new MethodInvocation(invocation.getName(), invocation.getReturnType(), invocation.getGenericReturnType(), invocation.getParameterTypes(), invocation.getView(), invocation.getViewType(), instance, invocation.getParameters()); next.invoke(beanInvocation); if (beanInvocation.found()) { invocation.setResult(beanInvocation.getResult()); return; } if (!invocation.isGetter()) { return; } beanInvocation = new MethodInvocation(invocation.getName(), invocation.getReturnType(), invocation.getGenericReturnType(), new Class<?>[]{invocation.getViewType()}, invocation.getView(), invocation.getViewType(), instance, new Object[]{invocation.getView()}); next.invoke(beanInvocation); if (beanInvocation.found()) { invocation.setResult(beanInvocation.getResult()); } } } private static class ClassMixInMethodInvoker implements MethodInvoker { private Object instance; private final Class<?> mixInClass; private final MethodInvoker next; private final ThreadLocal<MethodInvocation> current = new ThreadLocal<MethodInvocation>(); ClassMixInMethodInvoker(Class<?> mixInClass, MethodInvoker next) { this.mixInClass = mixInClass; this.next = next; } public void invoke(MethodInvocation invocation) throws Throwable { if (current.get() != null) { // Already invoking a method on the mix-in return; } if (instance == null) { instance = DirectInstantiator.INSTANCE.newInstance(mixInClass, invocation.getView()); } MethodInvocation beanInvocation = new MethodInvocation(invocation.getName(), invocation.getReturnType(), invocation.getGenericReturnType(), invocation.getParameterTypes(), invocation.getView(), invocation.getViewType(), instance, invocation.getParameters()); current.set(beanInvocation); try { next.invoke(beanInvocation); } finally { current.set(null); } if (beanInvocation.found()) { invocation.setResult(beanInvocation.getResult()); } } } private interface ViewDecoration { void collectInvokers(Object sourceObject, Class<?> viewType, List<MethodInvoker> invokers); boolean isNoOp(); /** * Filter this decoration to apply only to the given view types. Return {@link #NO_OP_MAPPER} if this decoration does not apply to any of the types. */ ViewDecoration restrictTo(Set<Class<?>> viewTypes); } private static class NoOpDecoration implements ViewDecoration, Serializable { @Override public void collectInvokers(Object sourceObject, Class<?> viewType, List<MethodInvoker> invokers) { } @Override public boolean equals(Object obj) { return obj instanceof NoOpDecoration; } @Override public int hashCode() { return 0; } @Override public boolean isNoOp() { return true; } @Override public ViewDecoration restrictTo(Set<Class<?>> viewTypes) { return this; } } private static class MixInMappingAction implements ViewDecoration, Serializable { private final List<? extends ViewDecoration> decorations; private MixInMappingAction(List<? extends ViewDecoration> decorations) { assert decorations.size() >= 2; this.decorations = decorations; } static ViewDecoration chain(List<? extends ViewDecoration> decorations) { if (decorations.isEmpty()) { return NO_OP_MAPPER; } if (decorations.size() == 1) { return decorations.get(0); } return new MixInMappingAction(decorations); } @Override public int hashCode() { int v = 0; for (ViewDecoration decoration : decorations) { v = v ^ decoration.hashCode(); } return v; } @Override public boolean equals(Object obj) { if (!obj.getClass().equals(MixInMappingAction.class)) { return false; } MixInMappingAction other = (MixInMappingAction) obj; return decorations.equals(other.decorations); } @Override public boolean isNoOp() { for (ViewDecoration decoration : decorations) { if (!decoration.isNoOp()) { return false; } } return true; } @Override public ViewDecoration restrictTo(Set<Class<?>> viewTypes) { List<ViewDecoration> filtered = new ArrayList<ViewDecoration>(); for (ViewDecoration viewDecoration : decorations) { ViewDecoration filteredDecoration = viewDecoration.restrictTo(viewTypes); if (!filteredDecoration.isNoOp()) { filtered.add(filteredDecoration); } } if (filtered.size() == 0) { return NO_OP_MAPPER; } if (filtered.size() == 1) { return filtered.get(0); } if (filtered.equals(decorations)) { return this; } return new MixInMappingAction(filtered); } @Override public void collectInvokers(Object sourceObject, Class<?> viewType, List<MethodInvoker> invokers) { for (ViewDecoration decoration : decorations) { decoration.collectInvokers(sourceObject, viewType, invokers); } } } private static abstract class TypeSpecificMappingAction implements ViewDecoration, Serializable { protected final Class<?> targetType; TypeSpecificMappingAction(Class<?> targetType) { this.targetType = targetType; } @Override public boolean isNoOp() { return false; } @Override public ViewDecoration restrictTo(Set<Class<?>> viewTypes) { if (viewTypes.contains(targetType)) { return this; } return NO_OP_MAPPER; } @Override public void collectInvokers(Object sourceObject, Class<?> viewType, List<MethodInvoker> invokers) { if (targetType.isAssignableFrom(viewType)) { invokers.add(createInvoker()); } } protected abstract MethodInvoker createInvoker(); } private static class MixInBeanMappingAction extends TypeSpecificMappingAction { private final Object mixIn; MixInBeanMappingAction(Class<?> targetType, Object mixIn) { super(targetType); this.mixIn = mixIn; } @Override public int hashCode() { return targetType.hashCode() ^ mixIn.hashCode(); } @Override public boolean equals(Object obj) { if (!obj.getClass().equals(MixInBeanMappingAction.class)) { return false; } MixInBeanMappingAction other = (MixInBeanMappingAction) obj; return targetType.equals(other.targetType) && mixIn.equals(other.mixIn); } @Override protected MethodInvoker createInvoker() { return new BeanMixInMethodInvoker(mixIn, REFLECTION_METHOD_INVOKER); } } private static class MixInTypeMappingAction extends TypeSpecificMappingAction { private final Class<?> mixInType; MixInTypeMappingAction(Class<?> targetType, Class<?> mixInType) { super(targetType); this.mixInType = mixInType; } @Override public int hashCode() { return targetType.hashCode() ^ mixInType.hashCode(); } @Override public boolean equals(Object obj) { if (!obj.getClass().equals(MixInTypeMappingAction.class)) { return false; } MixInTypeMappingAction other = (MixInTypeMappingAction) obj; return targetType.equals(other.targetType) && mixInType.equals(other.mixInType); } @Override protected MethodInvoker createInvoker() { return new ClassMixInMethodInvoker(mixInType, REFLECTION_METHOD_INVOKER); } } private class DefaultViewBuilder<T> implements ViewBuilder<T> { private final Class<T> viewType; @Nullable private final ViewGraphDetails graphDetails; List<ViewDecoration> viewDecorations = new ArrayList<ViewDecoration>(); DefaultViewBuilder(Class<T> viewType) { this.viewType = viewType; this.graphDetails = null; } DefaultViewBuilder(Class<T> viewType, @Nullable ViewGraphDetails graphDetails) { this.viewType = viewType; this.graphDetails = graphDetails; } @Override public ViewBuilder<T> mixInTo(final Class<?> targetType, final Object mixIn) { viewDecorations.add(new MixInBeanMappingAction(targetType, mixIn)); return this; } @Override public ViewBuilder<T> mixInTo(final Class<?> targetType, final Class<?> mixInType) { viewDecorations.add(new MixInTypeMappingAction(targetType, mixInType)); return this; } @Override public T build(@Nullable final Object sourceObject) { if (sourceObject == null) { return null; } ViewDecoration viewDecoration = MixInMappingAction.chain(viewDecorations); return createView(viewType, sourceObject, viewDecoration, graphDetails != null ? graphDetails : new ViewGraphDetails(targetTypeProvider)); } } }