/* * Copyright 2014-2017 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.springframework.data.projection; import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * A {@link ProjectionFactory} to create JDK proxies to back interfaces and handle method invocations on them. By * default accessor methods are supported. In case the delegating lookups result in an object of different type that the * projection interface method's return type, another projection will be created to transparently mitigate between the * types. * * @author Oliver Gierke * @author Christoph Strobl * @see SpelAwareProxyProjectionFactory * @since 1.10 */ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware, BeanClassLoaderAware { private static final boolean IS_JAVA_8 = org.springframework.util.ClassUtils.isPresent("java.util.Optional", ProxyProjectionFactory.class.getClassLoader()); private final List<MethodInterceptorFactory> factories; private ClassLoader classLoader; /** * Creates a new {@link ProxyProjectionFactory}. */ protected ProxyProjectionFactory() { this.factories = new ArrayList<>(); this.factories.add(MapAccessingMethodInterceptorFactory.INSTANCE); this.factories.add(PropertyAccessingMethodInvokerFactory.INSTANCE); } /** * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) * @deprecated rather set the {@link ClassLoader} directly via {@link #setBeanClassLoader(ClassLoader)}. */ @Override @Deprecated public void setResourceLoader(ResourceLoader resourceLoader) { this.classLoader = resourceLoader.getClassLoader(); } /* * (non-Javadoc) * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader) */ @Override public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } /** * Registers the given {@link MethodInterceptorFactory} to be used with the factory. Factories registered later enjoy * precedence over previously registered ones. * * @param factory must not be {@literal null}. * @since 1.13 */ public void registerMethodInvokerFactory(MethodInterceptorFactory factory) { Assert.notNull(factory, "MethodInterceptorFactory must not be null!"); this.factories.add(0, factory); } /* * (non-Javadoc) * @see org.springframework.data.rest.core.projection.ProjectionFactory#createProjection(java.lang.Object, java.lang.Class) */ @Override @SuppressWarnings("unchecked") public <T> T createProjection(Class<T> projectionType, Object source) { Assert.notNull(projectionType, "Projection type must not be null!"); Assert.isTrue(projectionType.isInterface(), "Projection type must be an interface!"); if (source == null) { return null; } ProxyFactory factory = new ProxyFactory(); factory.setTarget(source); factory.setOpaque(true); factory.setInterfaces(projectionType, TargetAware.class); if (IS_JAVA_8) { factory.addAdvice(new DefaultMethodInvokingMethodInterceptor()); } factory.addAdvice(new TargetAwareMethodInterceptor(source.getClass())); factory.addAdvice(getMethodInterceptor(source, projectionType)); return (T) factory.getProxy(classLoader == null ? ClassUtils.getDefaultClassLoader() : classLoader); } /* * (non-Javadoc) * @see org.springframework.data.projection.ProjectionFactory#createProjection(java.lang.Class) */ @Override public <T> T createProjection(Class<T> projectionType) { Assert.notNull(projectionType, "Projection type must not be null!"); return createProjection(projectionType, new HashMap<String, Object>()); } /* * (non-Javadoc) * @see org.springframework.data.projection.ProjectionFactory#getProperties(java.lang.Class) */ @Override public List<String> getInputProperties(Class<?> projectionType) { Assert.notNull(projectionType, "Projection type must not be null!"); List<String> result = new ArrayList<>(); for (PropertyDescriptor descriptor : getProjectionInformation(projectionType).getInputProperties()) { result.add(descriptor.getName()); } return result; } /* * (non-Javadoc) * @see org.springframework.data.projection.ProjectionFactory#getProjectionInformation(java.lang.Class) */ @Override public ProjectionInformation getProjectionInformation(Class<?> projectionType) { return new DefaultProjectionInformation(projectionType); } /** * Returns the {@link MethodInterceptor} to add to the proxy. * * @param source must not be {@literal null}. * @param projectionType must not be {@literal null}. * @return */ private MethodInterceptor getMethodInterceptor(Object source, Class<?> projectionType) { MethodInterceptor propertyInvocationInterceptor = getFactoryFor(source, projectionType) .createMethodInterceptor(source, projectionType); return new ProjectingMethodInterceptor(this, postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType)); } /** * Returns the {@link MethodInterceptorFactory} to be used with the given source object and target type. * * @param source must not be {@literal null}. * @param projectionType must not be {@literal null}. * @return */ private MethodInterceptorFactory getFactoryFor(Object source, Class<?> projectionType) { for (MethodInterceptorFactory factory : factories) { if (factory.supports(source, projectionType)) { return factory; } } throw new IllegalStateException("No MethodInterceptorFactory found for type ".concat(source.getClass().getName())); } /** * Post-process the given {@link MethodInterceptor} for the given source instance and projection type. Default * implementation will simply return the given interceptor. * * @param interceptor will never be {@literal null}. * @param source will never be {@literal null}. * @param projectionType will never be {@literal null}. * @return */ protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor interceptor, Object source, Class<?> projectionType) { return interceptor; } /** * Custom {@link MethodInterceptor} to expose the proxy target class even if we set * {@link ProxyFactory#setOpaque(boolean)} to true to prevent properties on {@link Advised} to be rendered. * * @author Oliver Gierke */ private static class TargetAwareMethodInterceptor implements MethodInterceptor { private static final Method GET_TARGET_CLASS_METHOD; private static final Method GET_TARGET_METHOD; private final Class<?> targetType; static { try { GET_TARGET_CLASS_METHOD = TargetAware.class.getMethod("getTargetClass"); GET_TARGET_METHOD = TargetAware.class.getMethod("getTarget"); } catch (NoSuchMethodException e) { throw new IllegalStateException(e); } } /** * Creates a new {@link TargetAwareMethodInterceptor} with the given target class. * * @param targetType must not be {@literal null}. */ public TargetAwareMethodInterceptor(Class<?> targetType) { Assert.notNull(targetType, "Target type must not be null!"); this.targetType = targetType; } /* * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) */ @Override public Object invoke(MethodInvocation invocation) throws Throwable { if (invocation.getMethod().equals(GET_TARGET_CLASS_METHOD)) { return targetType; } else if (invocation.getMethod().equals(GET_TARGET_METHOD)) { return invocation.getThis(); } return invocation.proceed(); } } /** * {@link MethodInterceptorFactory} handling {@link Map}s as target objects. * * @author Oliver Gierke */ private static enum MapAccessingMethodInterceptorFactory implements MethodInterceptorFactory { INSTANCE; /* * (non-Javadoc) * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class) */ @Override @SuppressWarnings("unchecked") public MethodInterceptor createMethodInterceptor(Object source, Class<?> targetType) { return new MapAccessingMethodInterceptor((Map<String, Object>) source); } /* * (non-Javadoc) * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class) */ @Override public boolean supports(Object source, Class<?> targetType) { return Map.class.isInstance(source); } } /** * {@link MethodInterceptorFactory} to create a {@link PropertyAccessingMethodInterceptor} for arbitrary objects. * * @author Oliver Gierke */ private static enum PropertyAccessingMethodInvokerFactory implements MethodInterceptorFactory { INSTANCE; /* * (non-Javadoc) * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class) */ @Override public MethodInterceptor createMethodInterceptor(Object source, Class<?> targetType) { return new PropertyAccessingMethodInterceptor(source); } /* * (non-Javadoc) * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class) */ @Override public boolean supports(Object source, Class<?> targetType) { return true; } } }