Skip to content

Fallback to default fragment implementation if no candidates found #3018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.springframework.beans.factory.config.BeanDefinition;
Expand All @@ -29,6 +28,7 @@
import org.springframework.data.util.Lazy;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository} instance. If
Expand All @@ -43,6 +43,7 @@
* @author Peter Rietzler
* @author Jens Schauder
* @author Mark Paluch
* @author Yanming Zhou
*/
public class CustomRepositoryImplementationDetector {

Expand Down Expand Up @@ -97,7 +98,7 @@ public CustomRepositoryImplementationDetector(Environment environment, ResourceL
* Tries to detect a custom implementation for a repository bean by classpath scanning.
*
* @param lookup must not be {@literal null}.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found.
* @return the {@code Optional<AbstractBeanDefinition>} of the custom implementation or empty {@code Optional} if none found.
*/
public Optional<AbstractBeanDefinition> detectCustomImplementation(ImplementationLookupConfiguration lookup) {

Expand All @@ -108,7 +109,7 @@ public Optional<AbstractBeanDefinition> detectCustomImplementation(Implementatio
.filter(lookup::matches) //
.collect(StreamUtils.toUnmodifiableSet());

return selectImplementationCandidate(lookup, definitions);
return definitions.isEmpty() ? findDefaultBeanDefinition(lookup) : selectImplementationCandidate(lookup, definitions);
}

private static Optional<AbstractBeanDefinition> selectImplementationCandidate(
Expand Down Expand Up @@ -143,6 +144,28 @@ private Set<BeanDefinition> findCandidateBeanDefinitions(ImplementationDetection
.collect(Collectors.toSet());
}

private Optional<AbstractBeanDefinition> findDefaultBeanDefinition(ImplementationLookupConfiguration lookup) {

if (lookup instanceof DefaultImplementationLookupConfiguration defaultLookup) {

String interfaceName = defaultLookup.getInterfaceName();
String packageName = ClassUtils.getPackageName(interfaceName);
String className = ClassUtils.getShortName(interfaceName);
String defaultImplementationClass = className + lookup.getImplementationPostfix() + ".class";

ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false,
environment);
provider.setResourceLoader(resourceLoader);
provider.setResourcePattern(defaultImplementationClass);
provider.setMetadataReaderFactory(lookup.getMetadataReaderFactory());
provider.addIncludeFilter((reader, factory) -> true);

return provider.findCandidateComponents(packageName).stream().map(AbstractBeanDefinition.class::cast).findAny();
}

return Optional.empty();
}

private static Optional<BeanDefinition> throwAmbiguousCustomImplementationException(
Collection<BeanDefinition> definitions) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* @author Oliver Gierke
* @author Mark Paluch
* @author Kyrylo Merzlikin
* @author Yanming Zhou
* @since 2.1
*/
class DefaultImplementationLookupConfiguration implements ImplementationLookupConfiguration {
Expand All @@ -53,6 +54,10 @@ class DefaultImplementationLookupConfiguration implements ImplementationLookupCo
this.beanName = beanName;
}

public String getInterfaceName() {
return this.interfaceName;
}

@Override
public String getImplementationBeanName() {
return beanName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetectorUnitTests.First.CanonicalSampleRepositoryTestImpl;
import org.springframework.data.repository.config.lib.MyExtension;
import org.springframework.data.repository.config.lib.MyExtensionImpl;
import org.springframework.data.util.Streamable;
import org.springframework.mock.env.MockEnvironment;

Expand All @@ -39,6 +41,7 @@
*
* @author Jens Schauder
* @author Mark Paluch
* @author Yanming Zhou
*/
class CustomRepositoryImplementationDetectorUnitTests {

Expand Down Expand Up @@ -115,6 +118,30 @@ void throwsExceptionWhenMultipleImplementationAreFound() {
});
}

@Test
void returnsDefaultBeanDefinitionIfNoCandidatesFound() {

Class<?> type = MyExtension.class;
// use a basePackage where no candidates present
String basePackage = this.getClass().getPackage().getName() + ".other";

when(configuration.getImplementationPostfix()).thenReturn("Impl");
when(configuration.getBasePackages()).thenReturn(Streamable.of(basePackage));

RepositoryConfiguration<?> repositoryConfiguration = mock(RepositoryConfiguration.class);
when(repositoryConfiguration.getRepositoryInterface()).thenReturn(type.getName());
when(repositoryConfiguration.getImplementationBeanName())
.thenReturn(Introspector.decapitalize(type.getSimpleName()) + "Impl");
when(repositoryConfiguration.getImplementationBasePackages())
.thenReturn(Streamable.of(basePackage));

var lookup = configuration.forRepositoryConfiguration(repositoryConfiguration);

assertThat(detector.detectCustomImplementation(lookup)) //
.hasValueSatisfying(
it -> assertThat(it.getBeanClassName()).isEqualTo(MyExtensionImpl.class.getName()));
}

private RepositoryConfiguration<?> configFor(Class<?> type) {

RepositoryConfiguration<?> configuration = mock(RepositoryConfiguration.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2019-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.config;

import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mapping.Child;
import org.springframework.data.repository.config.app.ChildRepository;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* @author Yanming Zhou
*/
class DefaultFragmentImplementationIntegrationTests {

@Test
void defaultFragmentImplementationIsUsing() {

var context = new AnnotationConfigApplicationContext(Config.class);

assertThatThrownBy(() -> context.getBean(ChildRepository.class).extensionMethod(new Child(1, "", "")))
.isInstanceOf(UnsupportedOperationException.class).hasMessage("Not Implemented");
}

@Configuration
@EnableRepositories(basePackageClasses = ChildRepository.class)
static class Config {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2022-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.config.app;

import org.springframework.data.mapping.Child;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.config.lib.MyExtension;

/**
* @author Yanming Zhou
*/
public interface ChildRepository extends CrudRepository<Child, String>, MyExtension<Child> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2022-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.config.lib;

/**
* @author Yanming Zhou
*/
public interface MyExtension<T> {

void extensionMethod(T entity);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2022-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.config.lib;

/**
* @author Yanming Zhou
*/
public class MyExtensionImpl<T> implements MyExtension<T> {

@Override
public void extensionMethod(T entity) {
throw new UnsupportedOperationException("Not Implemented");
}
}