/*
 * Copyright 2017-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.data.repository.core.support;

import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.MethodLookup.InvokedMethod;
import org.springframework.data.repository.core.support.MethodLookup.MethodPredicate;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;

Composite implementation to back repository method implementations.

A RepositoryComposition represents an ordered collection of fragments. Each fragment contributes executable method signatures that are used by this composition to route method calls into the according RepositoryFragment.

Fragments are allowed to contribute multiple implementations for a single method signature exposed through the repository interface. MethodLookup selects the first matching method for invocation. A composition also supports argument conversion between the repository method signature and fragment implementation method through withArgumentConverter(BiFunction<Method,Object[],Object[]>). Use argument conversion with a single implementation method that can be exposed accepting convertible types.

Composition objects are immutable and thread-safe.
Author:Mark Paluch
See Also:
@soundtrackMasterboy - Anybody (Fj Gauder Mix)
Since:2.0
/** * Composite implementation to back repository method implementations. * <p /> * A {@link RepositoryComposition} represents an ordered collection of {@link RepositoryFragment fragments}. Each * fragment contributes executable method signatures that are used by this composition to route method calls into the * according {@link RepositoryFragment}. * <p /> * Fragments are allowed to contribute multiple implementations for a single method signature exposed through the * repository interface. {@link #withMethodLookup(MethodLookup) MethodLookup} selects the first matching method for * invocation. A composition also supports argument conversion between the repository method signature and fragment * implementation method through {@link #withArgumentConverter(BiFunction)}. Use argument conversion with a single * implementation method that can be exposed accepting convertible types. * <p /> * Composition objects are immutable and thread-safe. * * @author Mark Paluch * @soundtrack Masterboy - Anybody (Fj Gauder Mix) * @since 2.0 * @see RepositoryFragment */
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode(of = "fragments") public class RepositoryComposition { private static final BiFunction<Method, Object[], Object[]> PASSTHRU_ARG_CONVERTER = (methodParameter, o) -> o; private static final RepositoryComposition EMPTY = new RepositoryComposition(RepositoryFragments.empty(), MethodLookups.direct(), PASSTHRU_ARG_CONVERTER); private final Map<Method, Method> methodCache = new ConcurrentReferenceHashMap<>(); private final @Getter RepositoryFragments fragments; private final @Getter MethodLookup methodLookup; private final @Getter BiFunction<Method, Object[], Object[]> argumentConverter;
Create an empty RepositoryComposition.
Returns:an empty RepositoryComposition.
/** * Create an empty {@link RepositoryComposition}. * * @return an empty {@link RepositoryComposition}. */
public static RepositoryComposition empty() { return EMPTY; }
Create a RepositoryComposition for just a single implementation with method lookup.
Params:
  • implementation – must not be null.
Returns:the RepositoryComposition for a single implementation.
/** * Create a {@link RepositoryComposition} for just a single {@code implementation} with {@link MethodLookups#direct()) * method lookup. * * @param implementation must not be {@literal null}. * @return the {@link RepositoryComposition} for a single {@code implementation}. */
public static RepositoryComposition just(Object implementation) { return new RepositoryComposition(RepositoryFragments.just(implementation), MethodLookups.direct(), PASSTHRU_ARG_CONVERTER); }
Create a RepositoryComposition from fragments with MethodLookups#direct()) method lookup.
Params:
  • fragments – must not be null.
Returns:the RepositoryComposition from fragments.
/** * Create a {@link RepositoryComposition} from {@link RepositoryFragment fragments} with * {@link MethodLookups#direct()) method lookup. * * @param fragments must not be {@literal null}. * @return the {@link RepositoryComposition} from {@link RepositoryFragment fragments}. */
public static RepositoryComposition of(RepositoryFragment<?>... fragments) { return of(Arrays.asList(fragments)); }
Create a RepositoryComposition from fragments with MethodLookups#direct()) method lookup.
Params:
  • fragments – must not be null.
Returns:the RepositoryComposition from fragments.
/** * Create a {@link RepositoryComposition} from {@link RepositoryFragment fragments} with * {@link MethodLookups#direct()) method lookup. * * @param fragments must not be {@literal null}. * @return the {@link RepositoryComposition} from {@link RepositoryFragment fragments}. */
public static RepositoryComposition of(List<RepositoryFragment<?>> fragments) { return new RepositoryComposition(RepositoryFragments.from(fragments), MethodLookups.direct(), PASSTHRU_ARG_CONVERTER); }
Create a RepositoryComposition from RepositoryFragments and RepositoryMetadata with MethodLookups#direct()) method lookup.
Params:
  • fragments – must not be null.
Returns:the RepositoryComposition from fragments.
/** * Create a {@link RepositoryComposition} from {@link RepositoryFragments} and {@link RepositoryMetadata} with * {@link MethodLookups#direct()) method lookup. * * @param fragments must not be {@literal null}. * @return the {@link RepositoryComposition} from {@link RepositoryFragments fragments}. */
public static RepositoryComposition of(RepositoryFragments fragments) { return new RepositoryComposition(fragments, MethodLookups.direct(), PASSTHRU_ARG_CONVERTER); }
Create a new RepositoryComposition retaining current configuration and append RepositoryFragment to the new composition. The resulting composition contains the appended RepositoryFragment as last element.
Params:
  • fragment – must not be null.
Returns:the new RepositoryComposition.
/** * Create a new {@link RepositoryComposition} retaining current configuration and append {@link RepositoryFragment} to * the new composition. The resulting composition contains the appended {@link RepositoryFragment} as last element. * * @param fragment must not be {@literal null}. * @return the new {@link RepositoryComposition}. */
public RepositoryComposition append(RepositoryFragment<?> fragment) { return new RepositoryComposition(fragments.append(fragment), methodLookup, argumentConverter); }
Create a new RepositoryComposition retaining current configuration and append RepositoryFragments to the new composition. The resulting composition contains the appended RepositoryFragments as last element.
Params:
  • fragments – must not be null.
Returns:the new RepositoryComposition.
/** * Create a new {@link RepositoryComposition} retaining current configuration and append {@link RepositoryFragments} * to the new composition. The resulting composition contains the appended {@link RepositoryFragments} as last * element. * * @param fragments must not be {@literal null}. * @return the new {@link RepositoryComposition}. */
public RepositoryComposition append(RepositoryFragments fragments) { return new RepositoryComposition(this.fragments.append(fragments), methodLookup, argumentConverter); }
Create a new RepositoryComposition retaining current configuration and set argumentConverter.
Params:
  • argumentConverter – must not be null.
Returns:the new RepositoryComposition.
/** * Create a new {@link RepositoryComposition} retaining current configuration and set {@code argumentConverter}. * * @param argumentConverter must not be {@literal null}. * @return the new {@link RepositoryComposition}. */
public RepositoryComposition withArgumentConverter(BiFunction<Method, Object[], Object[]> argumentConverter) { return new RepositoryComposition(fragments, methodLookup, argumentConverter); }
Create a new RepositoryComposition retaining current configuration and set methodLookup.
Params:
  • methodLookup – must not be null.
Returns:the new RepositoryComposition.
/** * Create a new {@link RepositoryComposition} retaining current configuration and set {@code methodLookup}. * * @param methodLookup must not be {@literal null}. * @return the new {@link RepositoryComposition}. */
public RepositoryComposition withMethodLookup(MethodLookup methodLookup) { return new RepositoryComposition(fragments, methodLookup, argumentConverter); }
Return true if this RepositoryComposition contains no fragments.
Returns:true if this RepositoryComposition contains no fragments.
/** * Return {@literal true} if this {@link RepositoryComposition} contains no {@link RepositoryFragment fragments}. * * @return {@literal true} if this {@link RepositoryComposition} contains no {@link RepositoryFragment fragments}. */
public boolean isEmpty() { return fragments.isEmpty(); }
Invoke a method on the repository by routing the invocation to the appropriate RepositoryFragment.
Params:
  • method –
  • args –
Throws:
Returns:
/** * Invoke a method on the repository by routing the invocation to the appropriate {@link RepositoryFragment}. * * @param method * @param args * @return * @throws Throwable */
public Object invoke(Method method, Object... args) throws Throwable { Method methodToCall = getMethod(method); if (methodToCall == null) { throw new IllegalArgumentException(String.format("No fragment found for method %s", method)); } ReflectionUtils.makeAccessible(methodToCall); return fragments.invoke(methodToCall, argumentConverter.apply(methodToCall, args)); }
Find the implementation method for the given Method invoked on the composite interface.
Params:
  • method – must not be null.
Returns:
/** * Find the implementation method for the given {@link Method} invoked on the composite interface. * * @param method must not be {@literal null}. * @return */
public Optional<Method> findMethod(Method method) { return Optional.ofNullable(getMethod(method)); }
Find the implementation method for the given Method invoked on the composite interface.
Params:
  • method – must not be null.
Returns:
Since:2.2
/** * Find the implementation method for the given {@link Method} invoked on the composite interface. * * @param method must not be {@literal null}. * @return * @since 2.2 */
@Nullable Method getMethod(Method method) { return methodCache.computeIfAbsent(method, key -> RepositoryFragments.findMethod(InvokedMethod.of(key), methodLookup, fragments::methods)); }
Validates that all fragments have an implementation.
/** * Validates that all {@link RepositoryFragment fragments} have an implementation. */
public void validateImplementation() { fragments.stream().forEach(it -> it.getImplementation() // .orElseThrow(() -> new IllegalStateException(String.format("Fragment %s has no implementation.", ClassUtils.getQualifiedName(it.getSignatureContributor()))))); }
Value object representing an ordered list of fragments.
Author:Mark Paluch
/** * Value object representing an ordered list of {@link RepositoryFragment fragments}. * * @author Mark Paluch */
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode public static class RepositoryFragments implements Streamable<RepositoryFragment<?>> { static final RepositoryFragments EMPTY = new RepositoryFragments(Collections.emptyList()); private final Map<Method, RepositoryFragment<?>> fragmentCache = new ConcurrentReferenceHashMap<>(); private final List<RepositoryFragment<?>> fragments;
Create empty RepositoryFragments.
Returns:empty RepositoryFragments.
/** * Create empty {@link RepositoryFragments}. * * @return empty {@link RepositoryFragments}. */
public static RepositoryFragments empty() { return EMPTY; }
Create RepositoryFragments from just implementation objects.
Params:
  • implementations – must not be null.
Returns:the RepositoryFragments for implementations.
/** * Create {@link RepositoryFragments} from just implementation objects. * * @param implementations must not be {@literal null}. * @return the {@link RepositoryFragments} for {@code implementations}. */
public static RepositoryFragments just(Object... implementations) { Assert.notNull(implementations, "Implementations must not be null!"); Assert.noNullElements(implementations, "Implementations must not contain null elements!"); return new RepositoryFragments( Arrays.stream(implementations).map(RepositoryFragment::implemented).collect(Collectors.toList())); }
Params:
  • fragments – must not be null.
Returns:the RepositoryFragments for implementations.
/** * Create {@link RepositoryFragments} from {@link RepositoryFragments fragments}. * * @param fragments must not be {@literal null}. * @return the {@link RepositoryFragments} for {@code implementations}. */
public static RepositoryFragments of(RepositoryFragment<?>... fragments) { Assert.notNull(fragments, "RepositoryFragments must not be null!"); Assert.noNullElements(fragments, "RepositoryFragments must not contain null elements!"); return new RepositoryFragments(Arrays.asList(fragments)); }
Params:
  • fragments – must not be null.
Returns:the RepositoryFragments for implementations.
/** * Create {@link RepositoryFragments} from a {@link List} of {@link RepositoryFragment fragments}. * * @param fragments must not be {@literal null}. * @return the {@link RepositoryFragments} for {@code implementations}. */
public static RepositoryFragments from(List<RepositoryFragment<?>> fragments) { Assert.notNull(fragments, "RepositoryFragments must not be null!"); return new RepositoryFragments(new ArrayList<>(fragments)); }
Create new RepositoryFragments from the current content appending RepositoryFragment.
Params:
  • fragment – must not be null
Returns:the new RepositoryFragments containing all existing fragments and the given RepositoryFragment as last element.
/** * Create new {@link RepositoryFragments} from the current content appending {@link RepositoryFragment}. * * @param fragment must not be {@literal null} * @return the new {@link RepositoryFragments} containing all existing fragments and the given * {@link RepositoryFragment} as last element. */
public RepositoryFragments append(RepositoryFragment<?> fragment) { Assert.notNull(fragment, "RepositoryFragment must not be null!"); return concat(stream(), Stream.of(fragment)); }
Create new RepositoryFragments from the current content appending RepositoryFragments.
Params:
  • fragments – must not be null
Returns:the new RepositoryFragments containing all existing fragments and the given RepositoryFragments as last elements.
/** * Create new {@link RepositoryFragments} from the current content appending {@link RepositoryFragments}. * * @param fragments must not be {@literal null} * @return the new {@link RepositoryFragments} containing all existing fragments and the given * {@link RepositoryFragments} as last elements. */
public RepositoryFragments append(RepositoryFragments fragments) { Assert.notNull(fragments, "RepositoryFragments must not be null!"); return concat(stream(), fragments.stream()); } private static RepositoryFragments concat(Stream<RepositoryFragment<?>> left, Stream<RepositoryFragment<?>> right) { return from(Stream.concat(left, right).collect(Collectors.toList())); } /* * (non-Javadoc) * @see java.lang.Iterable#iterator() */ @Override public Iterator<RepositoryFragment<?>> iterator() { return fragments.iterator(); }
Returns:Stream of methods.
/** * @return {@link Stream} of {@link Method methods}. */
public Stream<Method> methods() { return stream().flatMap(RepositoryFragment::methods); }
Invoke Method by resolving the
Params:
  • method –
  • args –
Throws:
Returns:
/** * Invoke {@link Method} by resolving the * * @param method * @param args * @return * @throws Throwable */
public Object invoke(Method method, Object[] args) throws Throwable { RepositoryFragment<?> fragment = fragmentCache.computeIfAbsent(method, this::findImplementationFragment); Optional<?> optional = fragment.getImplementation(); if (!optional.isPresent()) { throw new IllegalArgumentException(String.format("No implementation found for method %s", method)); } return method.invoke(optional.get(), args); } private RepositoryFragment<?> findImplementationFragment(Method key) { return stream().filter(it -> it.hasMethod(key)) // .filter(it -> it.getImplementation().isPresent()) // .findFirst() .orElseThrow(() -> new IllegalArgumentException(String.format("No fragment found for method %s", key))); } @Nullable private static Method findMethod(InvokedMethod invokedMethod, MethodLookup lookup, Supplier<Stream<Method>> methodStreamSupplier) { for (MethodPredicate methodPredicate : lookup.getLookups()) { Optional<Method> resolvedMethod = methodStreamSupplier.get() .filter(it -> methodPredicate.test(invokedMethod, it)) // .findFirst(); if (resolvedMethod.isPresent()) { return resolvedMethod.get(); } } return null; } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return fragments.toString(); } } }