1 /*
2  * Copyright 2008-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.repository.query;
17
18 import static java.lang.String.*;
19
20 import java.lang.reflect.Method;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Optional;
26
27 import org.springframework.core.MethodParameter;
28 import org.springframework.core.ResolvableType;
29 import org.springframework.data.domain.Pageable;
30 import org.springframework.data.domain.Sort;
31 import org.springframework.data.repository.util.ClassUtils;
32 import org.springframework.data.repository.util.QueryExecutionConverters;
33 import org.springframework.data.util.ClassTypeInformation;
34 import org.springframework.data.util.Lazy;
35 import org.springframework.data.util.TypeInformation;
36 import org.springframework.util.Assert;
37
38 /**
39  * Class to abstract a single parameter of a query method. It is held in the context of a {@link Parameters} instance.
40  *
41  * @author Oliver Gierke
42  * @author Mark Paluch
43  * @author Jens Schauder
44  */

45 public class Parameter {
46
47     static final List<Class<?>> TYPES;
48
49     private static final String NAMED_PARAMETER_TEMPLATE = ":%s";
50     private static final String POSITION_PARAMETER_TEMPLATE = "?%s";
51
52     private final MethodParameter parameter;
53     private final Class<?> parameterType;
54     private final boolean isDynamicProjectionParameter;
55     private final Lazy<Optional<String>> name;
56
57     static {
58
59         List<Class<?>> types = new ArrayList<>(Arrays.asList(Pageable.class, Sort.class));
60
61         // consider Kotlin Coroutines Continuation a special parameter. That parameter is synthetic and should not get
62         // bound to any query.
63
64         ClassUtils.ifPresent("kotlin.coroutines.Continuation", Parameter.class.getClassLoader(), types::add);
65
66         TYPES = Collections.unmodifiableList(types);
67     }
68
69     /**
70      * Creates a new {@link Parameter} for the given {@link MethodParameter}.
71      *
72      * @param parameter must not be {@literal null}.
73      */

74     protected Parameter(MethodParameter parameter) {
75
76         Assert.notNull(parameter, "MethodParameter must not be null!");
77
78         this.parameter = parameter;
79         this.parameterType = potentiallyUnwrapParameterType(parameter);
80         this.isDynamicProjectionParameter = isDynamicProjectionParameter(parameter);
81         this.name = TYPES.contains(parameter.getParameterType()) ? Lazy.of(Optional.empty()) : Lazy.of(() -> {
82             Param annotation = parameter.getParameterAnnotation(Param.class);
83             return Optional.ofNullable(annotation == null ? parameter.getParameterName() : annotation.value());
84         });
85     }
86
87     /**
88      * Returns whether the parameter is a special parameter.
89      *
90      * @return
91      * @see #TYPES
92      */

93     public boolean isSpecialParameter() {
94         return isDynamicProjectionParameter || TYPES.contains(parameter.getParameterType());
95     }
96
97     /**
98      * Returns whether the {@link Parameter} is to be bound to a query.
99      *
100      * @return
101      */

102     public boolean isBindable() {
103         return !isSpecialParameter();
104     }
105
106     /**
107      * Returns whether the current {@link Parameter} is the one used for dynamic projections.
108      *
109      * @return
110      */

111     public boolean isDynamicProjectionParameter() {
112         return isDynamicProjectionParameter;
113     }
114
115     /**
116      * Returns the placeholder to be used for the parameter. Can either be a named one or positional.
117      *
118      * @return
119      */

120     public String getPlaceholder() {
121
122         if (isNamedParameter()) {
123             return format(NAMED_PARAMETER_TEMPLATE, getName().get());
124         } else {
125             return format(POSITION_PARAMETER_TEMPLATE, getIndex());
126         }
127     }
128
129     /**
130      * Returns the position index the parameter is bound to in the context of its surrounding {@link Parameters}.
131      *
132      * @return
133      */

134     public int getIndex() {
135         return parameter.getParameterIndex();
136     }
137
138     /**
139      * Returns whether the parameter is annotated with {@link Param}.
140      *
141      * @return
142      */

143     public boolean isNamedParameter() {
144         return !isSpecialParameter() && getName().isPresent();
145     }
146
147     /**
148      * Returns the name of the parameter (through {@link Param} annotation).
149      *
150      * @return
151      */

152     public Optional<String> getName() {
153         return this.name.get();
154     }
155
156     /**
157      * Returns the type of the {@link Parameter}.
158      *
159      * @return the type
160      */

161     public Class<?> getType() {
162         return parameterType;
163     }
164
165     /**
166      * Returns whether the parameter is named explicitly, i.e. annotated with {@link Param}.
167      *
168      * @return
169      * @since 1.11
170      */

171     public boolean isExplicitlyNamed() {
172         return parameter.hasParameterAnnotation(Param.class);
173     }
174
175     /*
176      * (non-Javadoc)
177      * @see java.lang.Object#toString()
178      */

179     @Override
180     public String toString() {
181         return format("%s:%s", isNamedParameter() ? getName() : "#" + getIndex(), getType().getName());
182     }
183
184     /**
185      * Returns whether the {@link Parameter} is a {@link Pageable} parameter.
186      *
187      * @return
188      */

189     boolean isPageable() {
190         return Pageable.class.isAssignableFrom(getType());
191     }
192
193     /**
194      * Returns whether the {@link Parameter} is a {@link Sort} parameter.
195      *
196      * @return
197      */

198     boolean isSort() {
199         return Sort.class.isAssignableFrom(getType());
200     }
201
202     /**
203      * Returns whether the given {@link MethodParameter} is a dynamic projection parameter, which means it carries a
204      * dynamic type parameter which is identical to the type parameter of the actually returned type.
205      * <p>
206      * <code>
207      * <T> Collection<T> findBy…(…, Class<T> type);
208      * </code>
209      *
210      * @param parameter must not be {@literal null}.
211      * @return
212      */

213     private static boolean isDynamicProjectionParameter(MethodParameter parameter) {
214
215         Method method = parameter.getMethod();
216
217         if (method == null) {
218             throw new IllegalStateException(String.format("Method parameter %s is not backed by a method!", parameter));
219         }
220
221         ClassTypeInformation<?> ownerType = ClassTypeInformation.from(parameter.getDeclaringClass());
222         TypeInformation<?> parameterTypes = ownerType.getParameterTypes(method).get(parameter.getParameterIndex());
223
224         if (!parameterTypes.getType().equals(Class.class)) {
225             return false;
226         }
227
228         TypeInformation<?> bound = parameterTypes.getTypeArguments().get(0);
229         TypeInformation<Object> returnType = ClassTypeInformation.fromReturnTypeOf(method);
230
231         return bound.equals(QueryExecutionConverters.unwrapWrapperTypes(returnType));
232     }
233
234     /**
235      * Returns whether the {@link MethodParameter} is wrapped in a wrapper type.
236      *
237      * @param parameter must not be {@literal null}.
238      * @return
239      * @see QueryExecutionConverters
240      */

241     private static boolean isWrapped(MethodParameter parameter) {
242         return QueryExecutionConverters.supports(parameter.getParameterType());
243     }
244
245     /**
246      * Returns whether the {@link MethodParameter} should be unwrapped.
247      *
248      * @param parameter must not be {@literal null}.
249      * @return
250      * @see QueryExecutionConverters
251      */

252     private static boolean shouldUnwrap(MethodParameter parameter) {
253         return QueryExecutionConverters.supportsUnwrapping(parameter.getParameterType());
254     }
255
256     /**
257      * Returns the component type if the given {@link MethodParameter} is a wrapper type and the wrapper should be
258      * unwrapped.
259      *
260      * @param parameter must not be {@literal null}.
261      * @return
262      */

263     private static Class<?> potentiallyUnwrapParameterType(MethodParameter parameter) {
264
265         Class<?> originalType = parameter.getParameterType();
266
267         if (isWrapped(parameter) && shouldUnwrap(parameter)) {
268             return ResolvableType.forMethodParameter(parameter).getGeneric(0).resolve(Object.class);
269         }
270
271         return originalType;
272     }
273 }
274