1 /*
2 * Copyright 2017-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.core.support;
17
18 import lombok.AccessLevel;
19 import lombok.EqualsAndHashCode;
20 import lombok.Getter;
21 import lombok.RequiredArgsConstructor;
22
23 import java.lang.reflect.Method;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Optional;
31 import java.util.concurrent.ConcurrentHashMap;
32 import java.util.function.BiFunction;
33 import java.util.function.Supplier;
34 import java.util.stream.Collectors;
35 import java.util.stream.Stream;
36
37 import org.springframework.data.repository.core.RepositoryMetadata;
38 import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod;
39 import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate;
40 import org.springframework.data.util.Streamable;
41 import org.springframework.lang.Nullable;
42 import org.springframework.util.Assert;
43 import org.springframework.util.ClassUtils;
44 import org.springframework.util.ConcurrentReferenceHashMap;
45 import org.springframework.util.ReflectionUtils;
46
47 /**
48 * Composite implementation to back repository method implementations.
49 * <p />
50 * A {@link RepositoryComposition} represents an ordered collection of {@link RepositoryFragment fragments}. Each
51 * fragment contributes executable method signatures that are used by this composition to route method calls into the
52 * according {@link RepositoryFragment}.
53 * <p />
54 * Fragments are allowed to contribute multiple implementations for a single method signature exposed through the
55 * repository interface. {@link #withMethodLookup(MethodLookup) MethodLookup} selects the first matching method for
56 * invocation. A composition also supports argument conversion between the repository method signature and fragment
57 * implementation method through {@link #withArgumentConverter(BiFunction)}. Use argument conversion with a single
58 * implementation method that can be exposed accepting convertible types.
59 * <p />
60 * Composition objects are immutable and thread-safe.
61 *
62 * @author Mark Paluch
63 * @soundtrack Masterboy - Anybody (Fj Gauder Mix)
64 * @since 2.0
65 * @see RepositoryFragment
66 */
67 @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
68 @EqualsAndHashCode(of = "fragments")
69 public class RepositoryComposition {
70
71 private static final BiFunction<Method, Object[], Object[]> PASSTHRU_ARG_CONVERTER = (methodParameter, o) -> o;
72 private static final RepositoryComposition EMPTY = new RepositoryComposition(RepositoryFragments.empty(),
73 MethodLookups.direct(), PASSTHRU_ARG_CONVERTER);
74
75 private final Map<Method, Method> methodCache = new ConcurrentReferenceHashMap<>();
76 private final @Getter RepositoryFragments fragments;
77 private final @Getter MethodLookup methodLookup;
78 private final @Getter BiFunction<Method, Object[], Object[]> argumentConverter;
79
80 /**
81 * Create an empty {@link RepositoryComposition}.
82 *
83 * @return an empty {@link RepositoryComposition}.
84 */
85 public static RepositoryComposition empty() {
86 return EMPTY;
87 }
88
89 /**
90 * Create a {@link RepositoryComposition} for just a single {@code implementation} with {@link MethodLookups#direct())
91 * method lookup.
92 *
93 * @param implementation must not be {@literal null}.
94 * @return the {@link RepositoryComposition} for a single {@code implementation}.
95 */
96 public static RepositoryComposition just(Object implementation) {
97 return new RepositoryComposition(RepositoryFragments.just(implementation), MethodLookups.direct(),
98 PASSTHRU_ARG_CONVERTER);
99 }
100
101 /**
102 * Create a {@link RepositoryComposition} from {@link RepositoryFragment fragments} with
103 * {@link MethodLookups#direct()) method lookup.
104 *
105 * @param fragments must not be {@literal null}.
106 * @return the {@link RepositoryComposition} from {@link RepositoryFragment fragments}.
107 */
108 public static RepositoryComposition of(RepositoryFragment<?>... fragments) {
109 return of(Arrays.asList(fragments));
110 }
111
112 /**
113 * Create a {@link RepositoryComposition} from {@link RepositoryFragment fragments} with
114 * {@link MethodLookups#direct()) method lookup.
115 *
116 * @param fragments must not be {@literal null}.
117 * @return the {@link RepositoryComposition} from {@link RepositoryFragment fragments}.
118 */
119 public static RepositoryComposition of(List<RepositoryFragment<?>> fragments) {
120 return new RepositoryComposition(RepositoryFragments.from(fragments), MethodLookups.direct(),
121 PASSTHRU_ARG_CONVERTER);
122 }
123
124 /**
125 * Create a {@link RepositoryComposition} from {@link RepositoryFragments} and {@link RepositoryMetadata} with
126 * {@link MethodLookups#direct()) method lookup.
127 *
128 * @param fragments must not be {@literal null}.
129 * @return the {@link RepositoryComposition} from {@link RepositoryFragments fragments}.
130 */
131 public static RepositoryComposition of(RepositoryFragments fragments) {
132 return new RepositoryComposition(fragments, MethodLookups.direct(), PASSTHRU_ARG_CONVERTER);
133 }
134
135 /**
136 * Create a new {@link RepositoryComposition} retaining current configuration and append {@link RepositoryFragment} to
137 * the new composition. The resulting composition contains the appended {@link RepositoryFragment} as last element.
138 *
139 * @param fragment must not be {@literal null}.
140 * @return the new {@link RepositoryComposition}.
141 */
142 public RepositoryComposition append(RepositoryFragment<?> fragment) {
143 return new RepositoryComposition(fragments.append(fragment), methodLookup, argumentConverter);
144 }
145
146 /**
147 * Create a new {@link RepositoryComposition} retaining current configuration and append {@link RepositoryFragments}
148 * to the new composition. The resulting composition contains the appended {@link RepositoryFragments} as last
149 * element.
150 *
151 * @param fragments must not be {@literal null}.
152 * @return the new {@link RepositoryComposition}.
153 */
154 public RepositoryComposition append(RepositoryFragments fragments) {
155 return new RepositoryComposition(this.fragments.append(fragments), methodLookup, argumentConverter);
156 }
157
158 /**
159 * Create a new {@link RepositoryComposition} retaining current configuration and set {@code argumentConverter}.
160 *
161 * @param argumentConverter must not be {@literal null}.
162 * @return the new {@link RepositoryComposition}.
163 */
164 public RepositoryComposition withArgumentConverter(BiFunction<Method, Object[], Object[]> argumentConverter) {
165 return new RepositoryComposition(fragments, methodLookup, argumentConverter);
166 }
167
168 /**
169 * Create a new {@link RepositoryComposition} retaining current configuration and set {@code methodLookup}.
170 *
171 * @param methodLookup must not be {@literal null}.
172 * @return the new {@link RepositoryComposition}.
173 */
174 public RepositoryComposition withMethodLookup(MethodLookup methodLookup) {
175 return new RepositoryComposition(fragments, methodLookup, argumentConverter);
176 }
177
178 /**
179 * Return {@literal true} if this {@link RepositoryComposition} contains no {@link RepositoryFragment fragments}.
180 *
181 * @return {@literal true} if this {@link RepositoryComposition} contains no {@link RepositoryFragment fragments}.
182 */
183 public boolean isEmpty() {
184 return fragments.isEmpty();
185 }
186
187 /**
188 * Invoke a method on the repository by routing the invocation to the appropriate {@link RepositoryFragment}.
189 *
190 * @param method
191 * @param args
192 * @return
193 * @throws Throwable
194 */
195 public Object invoke(Method method, Object... args) throws Throwable {
196
197 Method methodToCall = getMethod(method);
198
199 if (methodToCall == null) {
200 throw new IllegalArgumentException(String.format("No fragment found for method %s", method));
201 }
202
203 ReflectionUtils.makeAccessible(methodToCall);
204
205 return fragments.invoke(method, methodToCall, argumentConverter.apply(methodToCall, args));
206 }
207
208 /**
209 * Find the implementation method for the given {@link Method} invoked on the composite interface.
210 *
211 * @param method must not be {@literal null}.
212 * @return
213 */
214 public Optional<Method> findMethod(Method method) {
215 return Optional.ofNullable(getMethod(method));
216 }
217
218 /**
219 * Find the implementation method for the given {@link Method} invoked on the composite interface.
220 *
221 * @param method must not be {@literal null}.
222 * @return
223 * @since 2.2
224 */
225 @Nullable
226 Method getMethod(Method method) {
227
228 return methodCache.computeIfAbsent(method,
229 key -> RepositoryFragments.findMethod(InvokedMethod.of(key), methodLookup, fragments::methods));
230 }
231
232 /**
233 * Validates that all {@link RepositoryFragment fragments} have an implementation.
234 */
235 public void validateImplementation() {
236
237 fragments.stream().forEach(it -> it.getImplementation() //
238 .orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.",
239 ClassUtils.getQualifiedName(it.getSignatureContributor())))));
240 }
241
242 /**
243 * Value object representing an ordered list of {@link RepositoryFragment fragments}.
244 *
245 * @author Mark Paluch
246 */
247 @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
248 @EqualsAndHashCode
249 public static class RepositoryFragments implements Streamable<RepositoryFragment<?>> {
250
251 static final RepositoryFragments EMPTY = new RepositoryFragments(Collections.emptyList());
252
253 private final Map<Method, RepositoryFragment<?>> fragmentCache = new ConcurrentReferenceHashMap<>();
254 private final Map<Method, ImplementationInvocationMetadata> invocationMetadataCache = new ConcurrentHashMap<>();
255 private final List<RepositoryFragment<?>> fragments;
256
257 /**
258 * Create empty {@link RepositoryFragments}.
259 *
260 * @return empty {@link RepositoryFragments}.
261 */
262 public static RepositoryFragments empty() {
263 return EMPTY;
264 }
265
266 /**
267 * Create {@link RepositoryFragments} from just implementation objects.
268 *
269 * @param implementations must not be {@literal null}.
270 * @return the {@link RepositoryFragments} for {@code implementations}.
271 */
272 public static RepositoryFragments just(Object... implementations) {
273
274 Assert.notNull(implementations, "Implementations must not be null!");
275 Assert.noNullElements(implementations, "Implementations must not contain null elements!");
276
277 return new RepositoryFragments(
278 Arrays.stream(implementations).map(RepositoryFragment::implemented).collect(Collectors.toList()));
279 }
280
281 /**
282 * Create {@link RepositoryFragments} from {@link RepositoryFragments fragments}.
283 *
284 * @param fragments must not be {@literal null}.
285 * @return the {@link RepositoryFragments} for {@code implementations}.
286 */
287 public static RepositoryFragments of(RepositoryFragment<?>... fragments) {
288
289 Assert.notNull(fragments, "RepositoryFragments must not be null!");
290 Assert.noNullElements(fragments, "RepositoryFragments must not contain null elements!");
291
292 return new RepositoryFragments(Arrays.asList(fragments));
293 }
294
295 /**
296 * Create {@link RepositoryFragments} from a {@link List} of {@link RepositoryFragment fragments}.
297 *
298 * @param fragments must not be {@literal null}.
299 * @return the {@link RepositoryFragments} for {@code implementations}.
300 */
301 public static RepositoryFragments from(List<RepositoryFragment<?>> fragments) {
302
303 Assert.notNull(fragments, "RepositoryFragments must not be null!");
304
305 return new RepositoryFragments(new ArrayList<>(fragments));
306 }
307
308 /**
309 * Create new {@link RepositoryFragments} from the current content appending {@link RepositoryFragment}.
310 *
311 * @param fragment must not be {@literal null}
312 * @return the new {@link RepositoryFragments} containing all existing fragments and the given
313 * {@link RepositoryFragment} as last element.
314 */
315 public RepositoryFragments append(RepositoryFragment<?> fragment) {
316
317 Assert.notNull(fragment, "RepositoryFragment must not be null!");
318
319 return concat(stream(), Stream.of(fragment));
320 }
321
322 /**
323 * Create new {@link RepositoryFragments} from the current content appending {@link RepositoryFragments}.
324 *
325 * @param fragments must not be {@literal null}
326 * @return the new {@link RepositoryFragments} containing all existing fragments and the given
327 * {@link RepositoryFragments} as last elements.
328 */
329 public RepositoryFragments append(RepositoryFragments fragments) {
330
331 Assert.notNull(fragments, "RepositoryFragments must not be null!");
332
333 return concat(stream(), fragments.stream());
334 }
335
336 private static RepositoryFragments concat(Stream<RepositoryFragment<?>> left, Stream<RepositoryFragment<?>> right) {
337 return from(Stream.concat(left, right).collect(Collectors.toList()));
338 }
339
340 /*
341 * (non-Javadoc)
342 * @see java.lang.Iterable#iterator()
343 */
344 @Override
345 public Iterator<RepositoryFragment<?>> iterator() {
346 return fragments.iterator();
347 }
348
349 /**
350 * @return {@link Stream} of {@link Method methods}.
351 */
352 public Stream<Method> methods() {
353 return stream().flatMap(RepositoryFragment::methods);
354 }
355
356 /**
357 * Invoke {@link Method} by resolving the
358 *
359 * @param invokedMethod invoked method as per invocation on the interface.
360 * @param methodToCall backend method that is backing the call.
361 * @param args
362 * @return
363 * @throws Throwable
364 */
365 @Nullable
366 public Object invoke(Method invokedMethod, Method methodToCall, Object[] args) throws Throwable {
367
368 RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(methodToCall, this::findImplementationFragment);
369 Optional<?> optional = fragment.getImplementation();
370
371 if (!optional.isPresent()) {
372 throw new IllegalArgumentException(String.format("No implementation found for method %s", methodToCall));
373 }
374
375 ImplementationInvocationMetadata invocationMetadata = invocationMetadataCache.get(invokedMethod);
376
377 if (invocationMetadata == null) {
378 invocationMetadata = new ImplementationInvocationMetadata(invokedMethod, methodToCall);
379 invocationMetadataCache.put(invokedMethod, invocationMetadata);
380 }
381
382 return invocationMetadata.invoke(methodToCall, optional.get(), args);
383 }
384
385 private RepositoryFragment<?> findImplementationFragment(Method key) {
386
387 return stream().filter(it -> it.hasMethod(key)) //
388 .filter(it -> it.getImplementation().isPresent()) //
389 .findFirst()
390 .orElseThrow(() -> new IllegalArgumentException(String.format("No fragment found for method %s", key)));
391 }
392
393 @Nullable
394 private static Method findMethod(InvokedMethod invokedMethod, MethodLookup lookup,
395 Supplier<Stream<Method>> methodStreamSupplier) {
396
397 for (MethodPredicate methodPredicate : lookup.getLookups()) {
398
399 Optional<Method> resolvedMethod = methodStreamSupplier.get()
400 .filter(it -> methodPredicate.test(invokedMethod, it)) //
401 .findFirst();
402
403 if (resolvedMethod.isPresent()) {
404 return resolvedMethod.get();
405 }
406 }
407
408 return null;
409 }
410
411 /*
412 * (non-Javadoc)
413 * @see java.lang.Object#toString()
414 */
415 @Override
416 public String toString() {
417 return fragments.toString();
418 }
419 }
420 }
421