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.Iterator;
24 import java.util.List;
25
26 import org.springframework.core.DefaultParameterNameDiscoverer;
27 import org.springframework.core.MethodParameter;
28 import org.springframework.core.ParameterNameDiscoverer;
29 import org.springframework.data.domain.Pageable;
30 import org.springframework.data.domain.Sort;
31 import org.springframework.data.util.Lazy;
32 import org.springframework.data.util.Streamable;
33 import org.springframework.util.Assert;
34
35 /**
36  * Abstracts method parameters that have to be bound to query parameters or applied to the query independently.
37  *
38  * @author Oliver Gierke
39  * @author Christoph Strobl
40  */

41 public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter> implements Streamable<T> {
42
43     public static final List<Class<?>> TYPES = Arrays.asList(Pageable.class, Sort.class);
44
45     private static final String PARAM_ON_SPECIAL = format("You must not user @%s on a parameter typed %s or %s",
46             Param.class.getSimpleName(), Pageable.class.getSimpleName(), Sort.class.getSimpleName());
47     private static final String ALL_OR_NOTHING = String.format(
48             "Either use @%s on all parameters except %s and %s typed once, or none at all!", Param.class.getSimpleName(),
49             Pageable.class.getSimpleName(), Sort.class.getSimpleName());
50
51     private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
52
53     private final int pageableIndex;
54     private final int sortIndex;
55     private final List<T> parameters;
56     private final Lazy<S> bindable;
57
58     private int dynamicProjectionIndex;
59
60     /**
61      * Creates a new instance of {@link Parameters}.
62      *
63      * @param method must not be {@literal null}.
64      */

65     public Parameters(Method method) {
66
67         Assert.notNull(method, "Method must not be null!");
68
69         int parameterCount = method.getParameterCount();
70
71         this.parameters = new ArrayList<>(parameterCount);
72         this.dynamicProjectionIndex = -1;
73
74         int pageableIndex = -1;
75         int sortIndex = -1;
76
77         for (int i = 0; i < parameterCount; i++) {
78
79             MethodParameter methodParameter = new MethodParameter(method, i);
80             methodParameter.initParameterNameDiscovery(PARAMETER_NAME_DISCOVERER);
81
82             T parameter = createParameter(methodParameter);
83
84             if (parameter.isSpecialParameter() && parameter.isNamedParameter()) {
85                 throw new IllegalArgumentException(PARAM_ON_SPECIAL);
86             }
87
88             if (parameter.isDynamicProjectionParameter()) {
89                 this.dynamicProjectionIndex = parameter.getIndex();
90             }
91
92             if (Pageable.class.isAssignableFrom(parameter.getType())) {
93                 pageableIndex = i;
94             }
95
96             if (Sort.class.isAssignableFrom(parameter.getType())) {
97                 sortIndex = i;
98             }
99
100             parameters.add(parameter);
101         }
102
103         this.pageableIndex = pageableIndex;
104         this.sortIndex = sortIndex;
105         this.bindable = Lazy.of(this::getBindable);
106
107         assertEitherAllParamAnnotatedOrNone();
108     }
109
110     /**
111      * Creates a new {@link Parameters} instance with the given {@link Parameter}s put into new context.
112      *
113      * @param originals
114      */

115     protected Parameters(List<T> originals) {
116
117         this.parameters = new ArrayList<>(originals.size());
118
119         int pageableIndexTemp = -1;
120         int sortIndexTemp = -1;
121         int dynamicProjectionTemp = -1;
122
123         for (int i = 0; i < originals.size(); i++) {
124
125             T original = originals.get(i);
126             this.parameters.add(original);
127
128             pageableIndexTemp = original.isPageable() ? i : -1;
129             sortIndexTemp = original.isSort() ? i : -1;
130             dynamicProjectionTemp = original.isDynamicProjectionParameter() ? i : -1;
131         }
132
133         this.pageableIndex = pageableIndexTemp;
134         this.sortIndex = sortIndexTemp;
135         this.dynamicProjectionIndex = dynamicProjectionTemp;
136         this.bindable = Lazy.of(() -> (S) this);
137     }
138
139     private S getBindable() {
140
141         List<T> bindables = new ArrayList<>();
142
143         for (T candidate : this) {
144
145             if (candidate.isBindable()) {
146                 bindables.add(candidate);
147             }
148         }
149
150         return createFrom(bindables);
151     }
152
153     /**
154      * Creates a {@link Parameter} instance for the given {@link MethodParameter}.
155      *
156      * @param parameter will never be {@literal null}.
157      * @return
158      */

159     protected abstract T createParameter(MethodParameter parameter);
160
161     /**
162      * Returns whether the method the {@link Parameters} was created for contains a {@link Pageable} argument.
163      *
164      * @return
165      */

166     public boolean hasPageableParameter() {
167         return pageableIndex != -1;
168     }
169
170     /**
171      * Returns the index of the {@link Pageable} {@link Method} parameter if available. Will return {@literal -1} if there
172      * is no {@link Pageable} argument in the {@link Method}'s parameter list.
173      *
174      * @return the pageableIndex
175      */

176     public int getPageableIndex() {
177         return pageableIndex;
178     }
179
180     /**
181      * Returns the index of the {@link Sort} {@link Method} parameter if available. Will return {@literal -1} if there is
182      * no {@link Sort} argument in the {@link Method}'s parameter list.
183      *
184      * @return
185      */

186     public int getSortIndex() {
187         return sortIndex;
188     }
189
190     /**
191      * Returns whether the method the {@link Parameters} was created for contains a {@link Sort} argument.
192      *
193      * @return
194      */

195     public boolean hasSortParameter() {
196         return sortIndex != -1;
197     }
198
199     /**
200      * Returns the index of the parameter that represents the dynamic projection type. Will return {@literal -1} if no
201      * such parameter exists.
202      *
203      * @return
204      */

205     public int getDynamicProjectionIndex() {
206         return dynamicProjectionIndex;
207     }
208
209     /**
210      * Returns whether a parameter expressing a dynamic projection exists.
211      *
212      * @return
213      */

214     public boolean hasDynamicProjection() {
215         return dynamicProjectionIndex != -1;
216     }
217
218     /**
219      * Returns whether we potentially find a {@link Sort} parameter in the parameters.
220      *
221      * @return
222      */

223     public boolean potentiallySortsDynamically() {
224         return hasSortParameter() || hasPageableParameter();
225     }
226
227     /**
228      * Returns the parameter with the given index.
229      *
230      * @param index
231      * @return
232      */

233     public T getParameter(int index) {
234
235         try {
236             return parameters.get(index);
237         } catch (IndexOutOfBoundsException e) {
238             throw new ParameterOutOfBoundsException(
239                     "Invalid parameter index! You seem to have declared too little query method parameters!", e);
240         }
241     }
242
243     /**
244      * Returns whether we have a parameter at the given position.
245      *
246      * @param position
247      * @return
248      */

249     public boolean hasParameterAt(int position) {
250
251         try {
252             return null != getParameter(position);
253         } catch (ParameterOutOfBoundsException e) {
254             return false;
255         }
256     }
257
258     /**
259      * Returns whether the method signature contains one of the special parameters ({@link Pageable}, {@link Sort}).
260      *
261      * @return
262      */

263     public boolean hasSpecialParameter() {
264         return hasSortParameter() || hasPageableParameter();
265     }
266
267     /**
268      * Returns the number of parameters.
269      *
270      * @return
271      */

272     public int getNumberOfParameters() {
273         return parameters.size();
274     }
275
276     /**
277      * Returns a {@link Parameters} instance with effectively all special parameters removed.
278      *
279      * @return
280      * @see Parameter#TYPES
281      * @see Parameter#isSpecialParameter()
282      */

283     public S getBindableParameters() {
284         return this.bindable.get();
285     }
286
287     protected abstract S createFrom(List<T> parameters);
288
289     /**
290      * Returns a bindable parameter with the given index. So for a method with a signature of
291      * {@code (Pageable pageable, String name)} a call to {@code #getBindableParameter(0)} will return the {@link String}
292      * parameter.
293      *
294      * @param bindableIndex
295      * @return
296      */

297     public T getBindableParameter(int bindableIndex) {
298         return getBindableParameters().getParameter(bindableIndex);
299     }
300
301     /**
302      * Asserts that either all of the non special parameters ({@link Pageable}, {@link Sort}) are annotated with
303      * {@link Param} or none of them is.
304      *
305      * @param method
306      */

307     private void assertEitherAllParamAnnotatedOrNone() {
308
309         boolean nameFound = false;
310         int index = 0;
311
312         for (T parameter : this.getBindableParameters()) {
313
314             if (parameter.isNamedParameter()) {
315                 Assert.isTrue(nameFound || index == 0, ALL_OR_NOTHING);
316                 nameFound = true;
317             } else {
318                 Assert.isTrue(!nameFound, ALL_OR_NOTHING);
319             }
320
321             index++;
322         }
323     }
324
325     /**
326      * Returns whether the given type is a bindable parameter.
327      *
328      * @param type
329      * @return
330      */

331     public static boolean isBindable(Class<?> type) {
332         return !TYPES.contains(type);
333     }
334
335     /*
336      * (non-Javadoc)
337      * @see java.lang.Iterable#iterator()
338      */

339     public Iterator<T> iterator() {
340         return parameters.iterator();
341     }
342 }
343