, SearchExtension
}
----
-====
[[repositories.customize-base-repository]]
== Customize the Base Repository
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
index 88965a4abe..0b73d66a0f 100644
--- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java
@@ -127,10 +127,16 @@ public RepositoryFactorySupport() {
* retrieval via the {@code RepositoryMethodContext} class. This is useful if an advised object needs to obtain
* repository information.
*
- * Default is {@literal "false"}, in order to avoid unnecessary extra interception. This means that no guarantees are provided
- * that {@code RepositoryMethodContext} access will work consistently within any method of the advised object.
- *
- * @since 3.4.0
+ * Default is {@literal "false"}, in order to avoid unnecessary extra interception. This means that no guarantees are
+ * provided that {@code RepositoryMethodContext} access will work consistently within any method of the advised
+ * object.
+ *
+ * Repository method metadata is also exposed if implementations within the {@link RepositoryFragments repository
+ * composition} implement {@link RepositoryMetadataAccess}.
+ *
+ * @since 3.4
+ * @see RepositoryMethodContext
+ * @see RepositoryMetadataAccess
*/
public void setExposeMetadata(boolean exposeMetadata) {
this.exposeMetadata = exposeMetadata;
@@ -342,10 +348,16 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
if (MethodInvocationValidator.supports(repositoryInterface)) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(LogMessage.format("Register MethodInvocationValidator for %s…", repositoryInterface.getName()));
+ }
result.addAdvice(new MethodInvocationValidator());
}
- if (this.exposeMetadata) {
+ if (this.exposeMetadata || shouldExposeMetadata(fragments)) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(LogMessage.format("Register ExposeMetadataInterceptor for %s…", repositoryInterface.getName()));
+ }
result.addAdvice(new ExposeMetadataInterceptor(metadata));
result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);
}
@@ -365,6 +377,9 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra
}
if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) {
+ if (logger.isTraceEnabled()) {
+ logger.trace(LogMessage.format("Register DefaultMethodInvokingMethodInterceptor for %s…", repositoryInterface.getName()));
+ }
result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
}
@@ -616,6 +631,23 @@ private Lazy createProjectionFactory() {
return Lazy.of(() -> getProjectionFactory(this.classLoader, this.beanFactory));
}
+ /**
+ * Checks if at least one {@link RepositoryFragment} indicates need to access to {@link RepositoryMetadata} by being
+ * flagged with {@link RepositoryMetadataAccess}.
+ *
+ * @param fragments
+ * @return {@literal true} if access to metadata is required.
+ */
+ private static boolean shouldExposeMetadata(RepositoryFragments fragments) {
+
+ for (RepositoryFragment> fragment : fragments) {
+ if (fragment.getImplementation().filter(RepositoryMetadataAccess.class::isInstance).isPresent()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Method interceptor that calls methods on the {@link RepositoryComposition}.
*
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java
index 96a8e9526d..45807de00e 100644
--- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFragment.java
@@ -20,6 +20,7 @@
import java.util.Optional;
import java.util.stream.Stream;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
@@ -41,6 +42,7 @@
* Fragments are immutable.
*
* @author Mark Paluch
+ * @author Christoph Strobl
* @since 2.0
* @see RepositoryComposition
*/
@@ -53,7 +55,7 @@ public interface RepositoryFragment {
* @return
*/
static RepositoryFragment implemented(T implementation) {
- return new ImplementedRepositoryFragment(Optional.empty(), implementation);
+ return new ImplementedRepositoryFragment<>((Class) null, implementation);
}
/**
@@ -64,7 +66,7 @@ static RepositoryFragment implemented(T implementation) {
* @return
*/
static RepositoryFragment implemented(Class interfaceClass, T implementation) {
- return new ImplementedRepositoryFragment<>(Optional.of(interfaceClass), implementation);
+ return new ImplementedRepositoryFragment<>(interfaceClass, implementation);
}
/**
@@ -134,7 +136,7 @@ public Class> getSignatureContributor() {
@Override
public RepositoryFragment withImplementation(T implementation) {
- return new ImplementedRepositoryFragment<>(Optional.of(interfaceOrImplementation), implementation);
+ return new ImplementedRepositoryFragment<>(interfaceOrImplementation, implementation);
}
@Override
@@ -164,9 +166,20 @@ public int hashCode() {
class ImplementedRepositoryFragment implements RepositoryFragment {
- private final Optional> interfaceClass;
+ private final @Nullable Class interfaceClass;
private final T implementation;
- private final Optional optionalImplementation;
+
+ /**
+ * Creates a new {@link ImplementedRepositoryFragment} for the given interface class and implementation.
+ *
+ * @param interfaceClass
+ * @param implementation
+ * @deprecated since 3.4 - use {@link ImplementedRepositoryFragment(Class, Object)} instead.
+ */
+ @Deprecated(since = "3.4")
+ public ImplementedRepositoryFragment(Optional> interfaceClass, T implementation) {
+ this(interfaceClass.orElse(null), implementation);
+ }
/**
* Creates a new {@link ImplementedRepositoryFragment} for the given interface class and implementation.
@@ -174,37 +187,36 @@ class ImplementedRepositoryFragment implements RepositoryFragment {
* @param interfaceClass must not be {@literal null}.
* @param implementation must not be {@literal null}.
*/
- public ImplementedRepositoryFragment(Optional> interfaceClass, T implementation) {
+ public ImplementedRepositoryFragment(@Nullable Class interfaceClass, T implementation) {
- Assert.notNull(interfaceClass, "Interface class must not be null");
Assert.notNull(implementation, "Implementation object must not be null");
- interfaceClass.ifPresent(it -> {
+ if(interfaceClass != null) {
- Assert.isTrue(ClassUtils.isAssignableValue(it, implementation),
- () -> String.format("Fragment implementation %s does not implement %s",
- ClassUtils.getQualifiedName(implementation.getClass()), ClassUtils.getQualifiedName(it)));
- });
+ Assert.isTrue(ClassUtils.isAssignableValue(interfaceClass, implementation),
+ () -> String.format("Fragment implementation %s does not implement %s",
+ ClassUtils.getQualifiedName(implementation.getClass()), ClassUtils.getQualifiedName(interfaceClass)));
+ };
this.interfaceClass = interfaceClass;
this.implementation = implementation;
- this.optionalImplementation = Optional.of(implementation);
}
- @SuppressWarnings({ "rawtypes", "unchecked" })
public Class> getSignatureContributor() {
- return interfaceClass.orElseGet(() -> {
- if(implementation instanceof Class type) {
- return type;
- }
- return (Class) implementation.getClass();
- });
+ if(interfaceClass != null) {
+ return interfaceClass;
+ }
+
+ if(implementation instanceof Class> type) {
+ return type;
+ }
+ return implementation.getClass();
}
@Override
public Optional getImplementation() {
- return optionalImplementation;
+ return Optional.of(implementation);
}
@Override
@@ -216,7 +228,7 @@ public RepositoryFragment withImplementation(T implementation) {
public String toString() {
return String.format("ImplementedRepositoryFragment %s%s",
- interfaceClass.map(ClassUtils::getShortName).map(it -> it + ":").orElse(""),
+ interfaceClass != null ? (ClassUtils.getShortName(interfaceClass) + ";") : "",
ClassUtils.getShortName(implementation.getClass()));
}
@@ -235,18 +247,13 @@ public boolean equals(Object o) {
return false;
}
- if (!ObjectUtils.nullSafeEquals(implementation, that.implementation)) {
- return false;
- }
-
- return ObjectUtils.nullSafeEquals(optionalImplementation, that.optionalImplementation);
+ return ObjectUtils.nullSafeEquals(implementation, that.implementation);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(interfaceClass);
result = 31 * result + ObjectUtils.nullSafeHashCode(implementation);
- result = 31 * result + ObjectUtils.nullSafeHashCode(optionalImplementation);
return result;
}
}
diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java
new file mode 100644
index 0000000000..fb4a7b82ce
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryMetadataAccess.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 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;
+
+/**
+ * Marker for repository fragment implementation that intend to access repository method invocation metadata.
+ *
+ * Note that this is a marker interface in the style of {@link java.io.Serializable}, semantically applying to a
+ * fragment implementation class. In other words, this marker applies to a particular repository composition that
+ * enables metadata access for the repository proxy when the composition contain fragments implementing this interface.
+ *
+ * Ideally, in a repository composition only the fragment implementation uses this interface while the fragment
+ * interface does not.
+ *
+ * @author Mark Paluch
+ * @since 3.4
+ * @see RepositoryMethodContext
+ */
+public interface RepositoryMetadataAccess {
+
+}
diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java
index 79f2862bf9..2661998cb2 100755
--- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java
+++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java
@@ -271,6 +271,27 @@ record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocati
assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname");
}
+ @Test // GH-3090
+ void capturesRepositoryMetadataWithMetadataAccess() {
+
+ record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocation) {
+ }
+
+ when(factory.queryOne.execute(any(Object[].class)))
+ .then(invocation -> new Metadata(RepositoryMethodContext.currentMethod(),
+ ExposeInvocationInterceptor.currentInvocation()));
+
+ var repository = factory.getRepository(ObjectRepository.class, new RepositoryMetadataAccess() {});
+ var metadataByLastname = repository.findMetadataByLastname();
+
+ assertThat(metadataByLastname).isInstanceOf(Metadata.class);
+
+ Metadata metadata = (Metadata) metadataByLastname;
+ assertThat(metadata.context().getMethod().getName()).isEqualTo("findMetadataByLastname");
+ assertThat(metadata.context().getRepository().getDomainType()).isEqualTo(Object.class);
+ assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname");
+ }
+
@Test // DATACMNS-509, DATACMNS-1764
void convertsWithSameElementType() {