1 /*
2 * Copyright 2015-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.extern.slf4j.Slf4j;
19
20 import java.beans.PropertyDescriptor;
21 import java.io.IOException;
22 import java.lang.reflect.Method;
23 import java.util.Arrays;
24 import java.util.Comparator;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.stream.Collectors;
29 import java.util.stream.IntStream;
30 import java.util.stream.Stream;
31
32 import org.springframework.beans.BeanUtils;
33 import org.springframework.core.type.MethodMetadata;
34 import org.springframework.data.type.MethodsMetadata;
35 import org.springframework.data.type.classreading.MethodsMetadataReader;
36 import org.springframework.data.type.classreading.MethodsMetadataReaderFactory;
37 import org.springframework.data.util.StreamUtils;
38 import org.springframework.util.Assert;
39 import org.springframework.util.ClassUtils;
40
41 /**
42 * Default implementation of {@link ProjectionInformation}. Exposes all properties of the type as required input
43 * properties.
44 *
45 * @author Oliver Gierke
46 * @author Christoph Strobl
47 * @author Mark Paluch
48 * @since 1.12
49 */
50 class DefaultProjectionInformation implements ProjectionInformation {
51
52 private final Class<?> projectionType;
53 private final List<PropertyDescriptor> properties;
54
55 /**
56 * Creates a new {@link DefaultProjectionInformation} for the given type.
57 *
58 * @param type must not be {@literal null}.
59 */
60 DefaultProjectionInformation(Class<?> type) {
61
62 Assert.notNull(type, "Projection type must not be null!");
63
64 this.projectionType = type;
65 this.properties = new PropertyDescriptorSource(type).getDescriptors();
66 }
67
68 /*
69 * (non-Javadoc)
70 * @see org.springframework.data.projection.ProjectionInformation#getType()
71 */
72 @Override
73 public Class<?> getType() {
74 return projectionType;
75 }
76
77 /*
78 * (non-Javadoc)
79 * @see org.springframework.data.projection.ProjectionInformation#getInputProperties()
80 */
81 public List<PropertyDescriptor> getInputProperties() {
82
83 return properties.stream()//
84 .filter(this::isInputProperty)//
85 .distinct()//
86 .collect(Collectors.toList());
87 }
88
89 /*
90 * (non-Javadoc)
91 * @see org.springframework.data.projection.ProjectionInformation#isDynamic()
92 */
93 @Override
94 public boolean isClosed() {
95 return this.properties.equals(getInputProperties());
96 }
97
98 /**
99 * Returns whether the given {@link PropertyDescriptor} describes an input property for the projection, i.e. a
100 * property that needs to be present on the source to be able to create reasonable projections for the type the
101 * descriptor was looked up on.
102 *
103 * @param descriptor will never be {@literal null}.
104 * @return
105 */
106 protected boolean isInputProperty(PropertyDescriptor descriptor) {
107 return true;
108 }
109
110 /**
111 * Returns whether the given {@link PropertyDescriptor} has a getter that is a Java 8 default method.
112 *
113 * @param descriptor must not be {@literal null}.
114 * @return
115 */
116 private static boolean hasDefaultGetter(PropertyDescriptor descriptor) {
117
118 Method method = descriptor.getReadMethod();
119
120 return method != null && method.isDefault();
121 }
122
123 /**
124 * Internal helper to detect {@link PropertyDescriptor} instances for a given type.
125 *
126 * @author Mark Paluch
127 * @author Oliver Gierke
128 * @since 2.1
129 * @soundtrack The Meters - Cissy Strut (Here Comes The Meter Man)
130 */
131 @Slf4j
132 private static class PropertyDescriptorSource {
133
134 private final Class<?> type;
135 private final Optional<MethodsMetadata> metadata;
136
137 /**
138 * Creates a new {@link PropertyDescriptorSource} for the given type.
139 *
140 * @param type must not be {@literal null}.
141 */
142 PropertyDescriptorSource(Class<?> type) {
143
144 Assert.notNull(type, "Type must not be null!");
145
146 this.type = type;
147 this.metadata = getMetadata(type);
148 }
149
150 /**
151 * Returns {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces.
152 *
153 * @return
154 */
155 List<PropertyDescriptor> getDescriptors() {
156 return collectDescriptors().distinct().collect(StreamUtils.toUnmodifiableList());
157 }
158
159 /**
160 * Recursively collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super
161 * interfaces.
162 *
163 * @return
164 */
165 private Stream<PropertyDescriptor> collectDescriptors() {
166
167 Stream<PropertyDescriptor> allButDefaultGetters = Arrays.stream(BeanUtils.getPropertyDescriptors(type)) //
168 .filter(it -> !hasDefaultGetter(it));
169
170 Stream<PropertyDescriptor> ownDescriptors = metadata.map(it -> filterAndOrder(allButDefaultGetters, it))
171 .orElse(allButDefaultGetters);
172
173 Stream<PropertyDescriptor> superTypeDescriptors = metadata.map(this::fromMetadata) //
174 .orElseGet(this::fromType) //
175 .flatMap(it -> new PropertyDescriptorSource(it).collectDescriptors());
176
177 return Stream.concat(ownDescriptors, superTypeDescriptors);
178 }
179
180 /**
181 * Returns a {@link Stream} of {@link PropertyDescriptor} ordered following the given {@link MethodsMetadata} only
182 * returning methods seen by the given {@link MethodsMetadata}.
183 *
184 * @param source must not be {@literal null}.
185 * @param metadata must not be {@literal null}.
186 * @return
187 */
188 private static Stream<PropertyDescriptor> filterAndOrder(Stream<PropertyDescriptor> source,
189 MethodsMetadata metadata) {
190
191 Map<String, Integer> orderedMethods = getMethodOrder(metadata);
192
193 if (orderedMethods.isEmpty()) {
194 return source;
195 }
196
197 return source.filter(descriptor -> descriptor.getReadMethod() != null)
198 .filter(descriptor -> orderedMethods.containsKey(descriptor.getReadMethod().getName()))
199 .sorted(Comparator.comparingInt(left -> orderedMethods.get(left.getReadMethod().getName())));
200 }
201
202 /**
203 * Returns a {@link Stream} of interfaces using the given {@link MethodsMetadata} as primary source for ordering.
204 *
205 * @param metadata must not be {@literal null}.
206 * @return
207 */
208 private Stream<Class<?>> fromMetadata(MethodsMetadata metadata) {
209 return Arrays.stream(metadata.getInterfaceNames()).map(it -> findType(it, type.getInterfaces()));
210 }
211
212 /**
213 * Returns a {@link Stream} of interfaces using the given type as primary source for ordering.
214 *
215 * @return
216 */
217 private Stream<Class<?>> fromType() {
218 return Arrays.stream(type.getInterfaces());
219 }
220
221 /**
222 * Attempts to obtain {@link MethodsMetadata} from {@link Class}. Returns {@link Optional} containing
223 * {@link MethodsMetadata} if metadata was read successfully, {@link Optional#empty()} otherwise.
224 *
225 * @param type must not be {@literal null}.
226 * @return the optional {@link MethodsMetadata}.
227 */
228 private static Optional<MethodsMetadata> getMetadata(Class<?> type) {
229
230 try {
231
232 MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader());
233 MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type));
234
235 return Optional.of(metadataReader.getMethodsMetadata());
236
237 } catch (IOException e) {
238
239 LOG.info("Couldn't read class metadata for {}. Input property calculation might fail!", type);
240
241 return Optional.empty();
242 }
243 }
244
245 /**
246 * Find the type with the given name in the given array of {@link Class}.
247 *
248 * @param name must not be {@literal null} or empty.
249 * @param types must not be {@literal null}.
250 * @return
251 */
252 private static Class<?> findType(String name, Class<?>[] types) {
253
254 return Arrays.stream(types) //
255 .filter(it -> name.equals(it.getName())) //
256 .findFirst()
257 .orElseThrow(() -> new IllegalStateException(
258 String.format("Did not find type %s in %s!", name, Arrays.toString(types))));
259 }
260
261 /**
262 * Returns a {@link Map} containing method name to its positional index according to {@link MethodsMetadata}.
263 *
264 * @param metadata
265 * @return
266 */
267 private static Map<String, Integer> getMethodOrder(MethodsMetadata metadata) {
268
269 List<String> methods = metadata.getMethods() //
270 .stream() //
271 .map(MethodMetadata::getMethodName) //
272 .distinct() //
273 .collect(Collectors.toList());
274
275 return IntStream.range(0, methods.size()) //
276 .boxed() //
277 .collect(Collectors.toMap(methods::get, i -> i));
278 }
279 }
280 }
281