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