1 /*
2  * Copyright 2011-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.mapping;
17
18 import lombok.EqualsAndHashCode;
19
20 import java.lang.annotation.Annotation;
21 import java.lang.reflect.Constructor;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.concurrent.ConcurrentHashMap;
26
27 import org.springframework.beans.factory.annotation.Value;
28 import org.springframework.data.annotation.PersistenceConstructor;
29 import org.springframework.data.util.Lazy;
30 import org.springframework.data.util.TypeInformation;
31 import org.springframework.lang.Nullable;
32 import org.springframework.util.Assert;
33 import org.springframework.util.ReflectionUtils;
34 import org.springframework.util.StringUtils;
35
36 /**
37  * Value object to encapsulate the constructor to be used when mapping persistent data to objects.
38  *
39  * @author Oliver Gierke
40  * @author Jon Brisbin
41  * @author Thomas Darimont
42  * @author Christoph Strobl
43  * @author Mark Paluch
44  * @author Myeonghyeon Lee
45  */

46 public class PreferredConstructor<T, P extends PersistentProperty<P>> {
47
48     private final Constructor<T> constructor;
49     private final List<Parameter<Object, P>> parameters;
50     private final Map<PersistentProperty<?>, Boolean> isPropertyParameterCache = new ConcurrentHashMap<>();
51
52     /**
53      * Creates a new {@link PreferredConstructor} from the given {@link Constructor} and {@link Parameter}s.
54      *
55      * @param constructor must not be {@literal null}.
56      * @param parameters must not be {@literal null}.
57      */

58     @SafeVarargs
59     public PreferredConstructor(Constructor<T> constructor, Parameter<Object, P>... parameters) {
60
61         Assert.notNull(constructor, "Constructor must not be null!");
62         Assert.notNull(parameters, "Parameters must not be null!");
63
64         ReflectionUtils.makeAccessible(constructor);
65         this.constructor = constructor;
66         this.parameters = Arrays.asList(parameters);
67     }
68
69     /**
70      * Returns the underlying {@link Constructor}.
71      *
72      * @return
73      */

74     public Constructor<T> getConstructor() {
75         return constructor;
76     }
77
78     /**
79      * Returns the {@link Parameter}s of the constructor.
80      *
81      * @return
82      */

83     public List<Parameter<Object, P>> getParameters() {
84         return parameters;
85     }
86
87     /**
88      * Returns whether the constructor has {@link Parameter}s.
89      *
90      * @see #isNoArgConstructor()
91      * @return
92      */

93     public boolean hasParameters() {
94         return !parameters.isEmpty();
95     }
96
97     /**
98      * Returns whether the constructor does not have any arguments.
99      *
100      * @see #hasParameters()
101      * @return
102      */

103     public boolean isNoArgConstructor() {
104         return parameters.isEmpty();
105     }
106
107     /**
108      * Returns whether the constructor was explicitly selected (by {@link PersistenceConstructor}).
109      *
110      * @return
111      */

112     public boolean isExplicitlyAnnotated() {
113         return constructor.isAnnotationPresent(PersistenceConstructor.class);
114     }
115
116     /**
117      * Returns whether the given {@link PersistentProperty} is referenced in a constructor argument of the
118      * {@link PersistentEntity} backing this {@link PreferredConstructor}.
119      *
120      * @param property must not be {@literal null}.
121      * @return {@literal trueif the {@link PersistentProperty} is used in the constructor.
122      */

123     public boolean isConstructorParameter(PersistentProperty<?> property) {
124
125         Assert.notNull(property, "Property must not be null!");
126
127         Boolean cached = isPropertyParameterCache.get(property);
128
129         if (cached != null) {
130             return cached;
131         }
132
133         boolean result = false;
134         for (Parameter<?, P> parameter : parameters) {
135             if (parameter.maps(property)) {
136                 isPropertyParameterCache.put(property, true);
137                 result = true;
138                 break;
139             }
140         }
141
142         return result;
143     }
144
145     /**
146      * Returns whether the given {@link Parameter} is one referring to an enclosing class. That is in case the class this
147      * {@link PreferredConstructor} belongs to is a member class actually. If that's the case the compiler creates a first
148      * constructor argument of the enclosing class type.
149      *
150      * @param parameter must not be {@literal null}.
151      * @return {@literal trueif the {@link PersistentProperty} maps to the enclosing class.
152      */

153     public boolean isEnclosingClassParameter(Parameter<?, P> parameter) {
154
155         Assert.notNull(parameter, "Parameter must not be null!");
156
157         if (parameters.isEmpty() || !parameter.isEnclosingClassParameter()) {
158             return false;
159         }
160
161         return parameters.get(0).equals(parameter);
162     }
163
164     /**
165      * Value object to represent constructor parameters.
166      *
167      * @param <T> the type of the parameter
168      * @author Oliver Gierke
169      */

170     @EqualsAndHashCode(exclude = { "enclosingClassCache""hasSpelExpression" })
171     public static class Parameter<T, P extends PersistentProperty<P>> {
172
173         private final @Nullable String name;
174         private final TypeInformation<T> type;
175         private final String key;
176         private final @Nullable PersistentEntity<T, P> entity;
177
178         private final Lazy<Boolean> enclosingClassCache;
179         private final Lazy<Boolean> hasSpelExpression;
180
181         /**
182          * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of
183          * {@link Annotation}s. Will inspect the annotations for an {@link Value} annotation to lookup a key or an SpEL
184          * expression to be evaluated.
185          *
186          * @param name the name of the parameter, can be {@literal null}
187          * @param type must not be {@literal null}
188          * @param annotations must not be {@literal null} but can be empty
189          * @param entity must not be {@literal null}.
190          */

191         public Parameter(@Nullable String name, TypeInformation<T> type, Annotation[] annotations,
192                 @Nullable PersistentEntity<T, P> entity) {
193
194             Assert.notNull(type, "Type must not be null!");
195             Assert.notNull(annotations, "Annotations must not be null!");
196
197             this.name = name;
198             this.type = type;
199             this.key = getValue(annotations);
200             this.entity = entity;
201
202             this.enclosingClassCache = Lazy.of(() -> {
203
204                 if (entity == null) {
205                     throw new IllegalStateException();
206                 }
207
208                 Class<T> owningType = entity.getType();
209                 return owningType.isMemberClass() && type.getType().equals(owningType.getEnclosingClass());
210             });
211
212             this.hasSpelExpression = Lazy.of(() -> StringUtils.hasText(getSpelExpression()));
213         }
214
215         @Nullable
216         private static String getValue(Annotation[] annotations) {
217
218             return Arrays.stream(annotations)//
219                     .filter(it -> it.annotationType() == Value.class)//
220                     .findFirst().map(it -> ((Value) it).value())//
221                     .filter(StringUtils::hasText).orElse(null);
222         }
223
224         /**
225          * Returns the name of the parameter.
226          *
227          * @return
228          */

229         @Nullable
230         public String getName() {
231             return name;
232         }
233
234         /**
235          * Returns the {@link TypeInformation} of the parameter.
236          *
237          * @return
238          */

239         public TypeInformation<T> getType() {
240             return type;
241         }
242
243         /**
244          * Returns the raw resolved type of the parameter.
245          *
246          * @return
247          */

248         public Class<T> getRawType() {
249             return type.getType();
250         }
251
252         /**
253          * Returns the key to be used when looking up a source data structure to populate the actual parameter value.
254          *
255          * @return
256          */

257         public String getSpelExpression() {
258             return key;
259         }
260
261         /**
262          * Returns whether the constructor parameter is equipped with a SpEL expression.
263          *
264          * @return
265          */

266         public boolean hasSpelExpression() {
267             return this.hasSpelExpression.get();
268         }
269
270         /**
271          * Returns whether the {@link Parameter} maps the given {@link PersistentProperty}.
272          *
273          * @param property
274          * @return
275          */

276         boolean maps(PersistentProperty<?> property) {
277
278             PersistentEntity<T, P> entity = this.entity;
279             String name = this.name;
280
281             P referencedProperty = entity == null ? null : name == null ? null : entity.getPersistentProperty(name);
282
283             return property.equals(referencedProperty);
284         }
285
286         private boolean isEnclosingClassParameter() {
287             return enclosingClassCache.get();
288         }
289     }
290 }
291