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 lombok.NonNull;
19 import lombok.RequiredArgsConstructor;
20
21 import java.lang.reflect.Array;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Map.Entry;
28
29 import javax.annotation.Nonnull;
30
31 import org.aopalliance.intercept.MethodInterceptor;
32 import org.aopalliance.intercept.MethodInvocation;
33 import org.springframework.core.CollectionFactory;
34 import org.springframework.core.convert.ConversionService;
35 import org.springframework.data.util.ClassTypeInformation;
36 import org.springframework.data.util.TypeInformation;
37 import org.springframework.lang.Nullable;
38 import org.springframework.util.Assert;
39 import org.springframework.util.ClassUtils;
40 import org.springframework.util.ObjectUtils;
41
42 /**
43  * {@link MethodInterceptor} to delegate the invocation to a different {@link MethodInterceptor} but creating a
44  * projecting proxy in case the returned value is not of the return type of the invoked method.
45  *
46  * @author Oliver Gierke
47  * @author Mark Paluch
48  * @since 1.10
49  */

50 @RequiredArgsConstructor
51 class ProjectingMethodInterceptor implements MethodInterceptor {
52
53     private final @NonNull ProjectionFactory factory;
54     private final @NonNull MethodInterceptor delegate;
55     private final @NonNull ConversionService conversionService;
56
57     /*
58      * (non-Javadoc)
59      * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
60      */

61     @Nullable
62     @Override
63     public Object invoke(@SuppressWarnings("null") @Nonnull MethodInvocation invocation) throws Throwable {
64
65         Object result = delegate.invoke(invocation);
66
67         if (result == null) {
68             return null;
69         }
70
71         TypeInformation<?> type = ClassTypeInformation.fromReturnTypeOf(invocation.getMethod());
72         Class<?> rawType = type.getType();
73
74         if (type.isCollectionLike() && !ClassUtils.isPrimitiveArray(rawType)) {
75             return projectCollectionElements(asCollection(result), type);
76         } else if (type.isMap()) {
77             return projectMapValues((Map<?, ?>) result, type);
78         } else if (conversionRequiredAndPossible(result, rawType)) {
79             return conversionService.convert(result, rawType);
80         } else {
81             return getProjection(result, rawType);
82         }
83     }
84
85     /**
86      * Creates projections of the given {@link Collection}'s elements if necessary and returns a new collection containing
87      * the projection results.
88      *
89      * @param sources must not be {@literal null}.
90      * @param type must not be {@literal null}.
91      * @return
92      */

93     private Object projectCollectionElements(Collection<?> sources, TypeInformation<?> type) {
94
95         Class<?> rawType = type.getType();
96         TypeInformation<?> componentType = type.getComponentType();
97         Collection<Object> result = CollectionFactory.createCollection(rawType.isArray() ? List.class : rawType,
98                 componentType != null ? componentType.getType() : null, sources.size());
99
100         for (Object source : sources) {
101             result.add(getProjection(source, type.getRequiredComponentType().getType()));
102         }
103
104         if (rawType.isArray()) {
105             return result.toArray((Object[]) Array.newInstance(type.getRequiredComponentType().getType(), result.size()));
106         }
107
108         return result;
109     }
110
111     /**
112      * Creates projections of the given {@link Map}'s values if necessary and returns an new {@link Map} with the handled
113      * values.
114      *
115      * @param sources must not be {@literal null}.
116      * @param type must not be {@literal null}.
117      * @return
118      */

119     private Map<Object, Object> projectMapValues(Map<?, ?> sources, TypeInformation<?> type) {
120
121         Map<Object, Object> result = CollectionFactory.createMap(type.getType(), sources.size());
122
123         for (Entry<?, ?> source : sources.entrySet()) {
124             result.put(source.getKey(), getProjection(source.getValue(), type.getRequiredMapValueType().getType()));
125         }
126
127         return result;
128     }
129
130     @Nullable
131     private Object getProjection(Object result, Class<?> returnType) {
132         return result == null || ClassUtils.isAssignable(returnType, result.getClass()) ? result
133                 : factory.createProjection(returnType, result);
134     }
135
136     /**
137      * Returns whether the source object needs to be converted to the given target type and whether we can convert it at
138      * all.
139      *
140      * @param source can be {@literal null}.
141      * @param targetType must not be {@literal null}.
142      * @return
143      */

144     private boolean conversionRequiredAndPossible(Object source, Class<?> targetType) {
145
146         if (source == null || targetType.isInstance(source)) {
147             return false;
148         }
149
150         return conversionService.canConvert(source.getClass(), targetType);
151     }
152
153     /**
154      * Turns the given value into a {@link Collection}. Will turn an array into a collection an wrap all other values into
155      * a single-element collection.
156      *
157      * @param source must not be {@literal null}.
158      * @return
159      */

160     private static Collection<?> asCollection(Object source) {
161
162         Assert.notNull(source, "Source object must not be null!");
163
164         if (source instanceof Collection) {
165             return (Collection<?>) source;
166         } else if (source.getClass().isArray()) {
167             return Arrays.asList(ObjectUtils.toObjectArray(source));
168         } else {
169             return Collections.singleton(source);
170         }
171     }
172 }
173