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 trueif this {@link RepositoryComposition} contains no {@link RepositoryFragment fragments}.
180      *
181      * @return {@literal trueif 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