1 /*
2  * Copyright 2014-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.projection;
17
18 import java.lang.reflect.Method;
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23
24 import org.aopalliance.intercept.MethodInterceptor;
25 import org.aopalliance.intercept.MethodInvocation;
26 import org.springframework.aop.framework.Advised;
27 import org.springframework.aop.framework.ProxyFactory;
28 import org.springframework.beans.factory.BeanClassLoaderAware;
29 import org.springframework.core.convert.ConversionService;
30 import org.springframework.core.convert.support.DefaultConversionService;
31 import org.springframework.lang.Nullable;
32 import org.springframework.util.Assert;
33 import org.springframework.util.ClassUtils;
34 import org.springframework.util.ConcurrentReferenceHashMap;
35
36 /**
37  * A {@link ProjectionFactory} to create JDK proxies to back interfaces and handle method invocations on them. By
38  * default accessor methods are supported. In case the delegating lookups result in an object of different type that the
39  * projection interface method's return type, another projection will be created to transparently mitigate between the
40  * types.
41  *
42  * @author Oliver Gierke
43  * @author Christoph Strobl
44  * @author Mark Paluch
45  * @see SpelAwareProxyProjectionFactory
46  * @since 1.10
47  */

48 class ProxyProjectionFactory implements ProjectionFactory, BeanClassLoaderAware {
49
50     private final List<MethodInterceptorFactory> factories;
51     private final ConversionService conversionService;
52     private final Map<Class<?>, ProjectionInformation> projectionInformationCache = new ConcurrentReferenceHashMap<>();
53     private @Nullable ClassLoader classLoader;
54
55     /**
56      * Creates a new {@link ProxyProjectionFactory}.
57      */

58     protected ProxyProjectionFactory() {
59
60         this.factories = new ArrayList<>();
61         this.factories.add(MapAccessingMethodInterceptorFactory.INSTANCE);
62         this.factories.add(PropertyAccessingMethodInvokerFactory.INSTANCE);
63
64         this.conversionService = DefaultConversionService.getSharedInstance();
65     }
66
67     /*
68      * (non-Javadoc)
69      * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
70      */

71     @Override
72     public void setBeanClassLoader(ClassLoader classLoader) {
73         this.classLoader = classLoader;
74     }
75
76     /**
77      * Registers the given {@link MethodInterceptorFactory} to be used with the factory. Factories registered later enjoy
78      * precedence over previously registered ones.
79      *
80      * @param factory must not be {@literal null}.
81      * @since 1.13
82      */

83     public void registerMethodInvokerFactory(MethodInterceptorFactory factory) {
84
85         Assert.notNull(factory, "MethodInterceptorFactory must not be null!");
86
87         this.factories.add(0, factory);
88     }
89
90     /*
91      * (non-Javadoc)
92      * @see org.springframework.data.rest.core.projection.ProjectionFactory#createProjection(java.lang.Object, java.lang.Class)
93      */

94     @Override
95     @SuppressWarnings("unchecked")
96     public <T> T createProjection(Class<T> projectionType, Object source) {
97
98         Assert.notNull(projectionType, "Projection type must not be null!");
99         Assert.notNull(source, "Source must not be null!");
100         Assert.isTrue(projectionType.isInterface(), "Projection type must be an interface!");
101
102         if (projectionType.isInstance(source)) {
103             return (T) source;
104         }
105
106         ProxyFactory factory = new ProxyFactory();
107         factory.setTarget(source);
108         factory.setOpaque(true);
109         factory.setInterfaces(projectionType, TargetAware.class);
110
111         factory.addAdvice(new DefaultMethodInvokingMethodInterceptor());
112         factory.addAdvice(new TargetAwareMethodInterceptor(source.getClass()));
113         factory.addAdvice(getMethodInterceptor(source, projectionType));
114
115         return (T) factory.getProxy(classLoader == null ? ClassUtils.getDefaultClassLoader() : classLoader);
116     }
117
118     /*
119      * (non-Javadoc)
120      * @see org.springframework.data.projection.ProjectionFactory#createProjection(java.lang.Class)
121      */

122     @Override
123     public <T> T createProjection(Class<T> projectionType) {
124
125         Assert.notNull(projectionType, "Projection type must not be null!");
126
127         return createProjection(projectionType, new HashMap<String, Object>());
128     }
129
130     /*
131      * (non-Javadoc)
132      * @see org.springframework.data.projection.ProjectionFactory#getProjectionInformation(java.lang.Class)
133      */

134     @Override
135     public final ProjectionInformation getProjectionInformation(Class<?> projectionType) {
136
137         return projectionInformationCache.computeIfAbsent(projectionType, this::createProjectionInformation);
138     }
139
140     /**
141      * Post-process the given {@link MethodInterceptor} for the given source instance and projection type. Default
142      * implementation will simply return the given interceptor.
143      *
144      * @param interceptor will never be {@literal null}.
145      * @param source will never be {@literal null}.
146      * @param projectionType will never be {@literal null}.
147      * @return
148      */

149     protected MethodInterceptor postProcessAccessorInterceptor(MethodInterceptor interceptor, Object source,
150             Class<?> projectionType) {
151         return interceptor;
152     }
153
154     /**
155      * Creates a fresh, cacheable {@link ProjectionInformation} instance for the given projection type.
156      *
157      * @param projectionType must not be {@literal null}.
158      * @return
159      */

160     protected ProjectionInformation createProjectionInformation(Class<?> projectionType) {
161         return new DefaultProjectionInformation(projectionType);
162     }
163
164     /**
165      * Returns the {@link MethodInterceptor} to add to the proxy.
166      *
167      * @param source must not be {@literal null}.
168      * @param projectionType must not be {@literal null}.
169      * @return
170      */

171     private MethodInterceptor getMethodInterceptor(Object source, Class<?> projectionType) {
172
173         MethodInterceptor propertyInvocationInterceptor = getFactoryFor(source, projectionType)
174                 .createMethodInterceptor(source, projectionType);
175
176         return new ProjectingMethodInterceptor(this,
177                 postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType), conversionService);
178     }
179
180     /**
181      * Returns the {@link MethodInterceptorFactory} to be used with the given source object and target type.
182      *
183      * @param source must not be {@literal null}.
184      * @param projectionType must not be {@literal null}.
185      * @return
186      */

187     private MethodInterceptorFactory getFactoryFor(Object source, Class<?> projectionType) {
188
189         for (MethodInterceptorFactory factory : factories) {
190             if (factory.supports(source, projectionType)) {
191                 return factory;
192             }
193         }
194
195         throw new IllegalStateException("No MethodInterceptorFactory found for type ".concat(source.getClass().getName()));
196     }
197
198     /**
199      * Custom {@link MethodInterceptor} to expose the proxy target class even if we set
200      * {@link ProxyFactory#setOpaque(boolean)} to true to prevent properties on {@link Advised} to be rendered.
201      *
202      * @author Oliver Gierke
203      */

204     private static class TargetAwareMethodInterceptor implements MethodInterceptor {
205
206         private static final Method GET_TARGET_CLASS_METHOD;
207         private static final Method GET_TARGET_METHOD;
208
209         private final Class<?> targetType;
210
211         static {
212             try {
213                 GET_TARGET_CLASS_METHOD = TargetAware.class.getMethod("getTargetClass");
214                 GET_TARGET_METHOD = TargetAware.class.getMethod("getTarget");
215             } catch (NoSuchMethodException e) {
216                 throw new IllegalStateException(e);
217             }
218         }
219
220         /**
221          * Creates a new {@link TargetAwareMethodInterceptor} with the given target class.
222          *
223          * @param targetType must not be {@literal null}.
224          */

225         public TargetAwareMethodInterceptor(Class<?> targetType) {
226
227             Assert.notNull(targetType, "Target type must not be null!");
228             this.targetType = targetType;
229         }
230
231         /*
232          * (non-Javadoc)
233          * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
234          */

235         @Nullable
236         @Override
237         public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
238
239             if (invocation.getMethod().equals(GET_TARGET_CLASS_METHOD)) {
240                 return targetType;
241             } else if (invocation.getMethod().equals(GET_TARGET_METHOD)) {
242                 return invocation.getThis();
243             }
244
245             return invocation.proceed();
246         }
247     }
248
249     /**
250      * {@link MethodInterceptorFactory} handling {@link Map}s as target objects.
251      *
252      * @author Oliver Gierke
253      */

254     private static enum MapAccessingMethodInterceptorFactory implements MethodInterceptorFactory {
255
256         INSTANCE;
257
258         /*
259          * (non-Javadoc)
260          * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class)
261          */

262         @Override
263         @SuppressWarnings("unchecked")
264         public MethodInterceptor createMethodInterceptor(Object source, Class<?> targetType) {
265             return new MapAccessingMethodInterceptor((Map<String, Object>) source);
266         }
267
268         /*
269          * (non-Javadoc)
270          * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class)
271          */

272         @Override
273         public boolean supports(Object source, Class<?> targetType) {
274             return Map.class.isInstance(source);
275         }
276     }
277
278     /**
279      * {@link MethodInterceptorFactory} to create a {@link PropertyAccessingMethodInterceptor} for arbitrary objects.
280      *
281      * @author Oliver Gierke
282      */

283     private static enum PropertyAccessingMethodInvokerFactory implements MethodInterceptorFactory {
284
285         INSTANCE;
286
287         /*
288          * (non-Javadoc)
289          * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class)
290          */

291         @Override
292         public MethodInterceptor createMethodInterceptor(Object source, Class<?> targetType) {
293             return new PropertyAccessingMethodInterceptor(source);
294         }
295
296         /*
297          * (non-Javadoc)
298          * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class)
299          */

300         @Override
301         public boolean supports(Object source, Class<?> targetType) {
302             return true;
303         }
304     }
305 }
306