Skip to content

Commit 0a3b71f

Browse files
mp911deodrotbohm
authored andcommitted
DATACMNS-1233 - Allow CDI Repositories to be composed of an arbitrary number of implementation classes.
We now support repository fragments for repositories exported through CDI. Original pull request: #272.
1 parent 919090b commit 0a3b71f

22 files changed

+1006
-214
lines changed

src/main/java/org/springframework/data/repository/cdi/CdiRepositoryBean.java

Lines changed: 147 additions & 104 deletions
Large diffs are not rendered by default.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.cdi;
17+
18+
import lombok.RequiredArgsConstructor;
19+
20+
import java.io.IOException;
21+
import java.util.Arrays;
22+
import java.util.Collections;
23+
import java.util.Optional;
24+
import java.util.stream.Stream;
25+
26+
import javax.enterprise.inject.CreationException;
27+
import javax.enterprise.inject.UnsatisfiedResolutionException;
28+
29+
import org.springframework.beans.factory.config.BeanDefinition;
30+
import org.springframework.beans.factory.support.AbstractBeanDefinition;
31+
import org.springframework.core.env.Environment;
32+
import org.springframework.core.env.StandardEnvironment;
33+
import org.springframework.core.io.ResourceLoader;
34+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
35+
import org.springframework.core.type.ClassMetadata;
36+
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
37+
import org.springframework.core.type.classreading.MetadataReaderFactory;
38+
import org.springframework.core.type.filter.AnnotationTypeFilter;
39+
import org.springframework.core.type.filter.TypeFilter;
40+
import org.springframework.data.repository.NoRepositoryBean;
41+
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
42+
import org.springframework.data.repository.config.FragmentMetadata;
43+
import org.springframework.data.repository.config.RepositoryFragmentConfiguration;
44+
import org.springframework.data.repository.config.RepositoryFragmentDiscovery;
45+
import org.springframework.data.util.Optionals;
46+
import org.springframework.data.util.Streamable;
47+
import org.springframework.util.Assert;
48+
import org.springframework.util.ClassUtils;
49+
50+
/**
51+
* Context for CDI repositories. This class provides {@link ClassLoader} and
52+
* {@link org.springframework.data.repository.core.support.RepositoryFragment detection} which are commonly used within
53+
* CDI.
54+
*
55+
* @author Mark Paluch
56+
* @since 2.1
57+
*/
58+
public class CdiRepositoryContext {
59+
60+
private final ClassLoader classLoader;
61+
private final CustomRepositoryImplementationDetector detector;
62+
private final MetadataReaderFactory metadataReaderFactory;
63+
64+
/**
65+
* Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and initialize
66+
* {@link CachingMetadataReaderFactory}.
67+
*
68+
* @param classLoader must not be {@literal null}.
69+
*/
70+
public CdiRepositoryContext(ClassLoader classLoader) {
71+
72+
Assert.notNull(classLoader, "ClassLoader must not be null!");
73+
74+
this.classLoader = classLoader;
75+
76+
Environment environment = new StandardEnvironment();
77+
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(classLoader);
78+
79+
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
80+
this.detector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, resourceLoader);
81+
}
82+
83+
/**
84+
* Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and
85+
* {@link CustomRepositoryImplementationDetector}.
86+
*
87+
* @param classLoader must not be {@literal null}.
88+
* @param detector must not be {@literal null}.
89+
*/
90+
public CdiRepositoryContext(ClassLoader classLoader, CustomRepositoryImplementationDetector detector) {
91+
92+
Assert.notNull(classLoader, "ClassLoader must not be null!");
93+
Assert.notNull(detector, "CustomRepositoryImplementationDetector must not be null!");
94+
95+
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(classLoader);
96+
97+
this.classLoader = classLoader;
98+
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
99+
this.detector = detector;
100+
}
101+
102+
CustomRepositoryImplementationDetector getCustomRepositoryImplementationDetector() {
103+
return detector;
104+
}
105+
106+
/**
107+
* Load a {@link Class} using the CDI {@link ClassLoader}.
108+
*
109+
* @param className
110+
* @return
111+
* @throws UnsatisfiedResolutionException if the class cannot be found.
112+
*/
113+
Class<?> loadClass(String className) {
114+
115+
try {
116+
return ClassUtils.forName(className, classLoader);
117+
} catch (ClassNotFoundException e) {
118+
throw new UnsatisfiedResolutionException(String.format("Unable to resolve class for '%s'", className), e);
119+
}
120+
}
121+
122+
/**
123+
* Discover {@link RepositoryFragmentConfiguration fragment configurations} for a {@link Class repository interface}.
124+
*
125+
* @param configuration must not be {@literal null}.
126+
* @param repositoryInterface must not be {@literal null}.
127+
* @return {@link Stream} of {@link RepositoryFragmentConfiguration fragment configurations}.
128+
*/
129+
Stream<RepositoryFragmentConfiguration> getRepositoryFragments(CdiRepositoryConfiguration configuration,
130+
Class<?> repositoryInterface) {
131+
132+
ClassMetadata classMetadata = getClassMetadata(metadataReaderFactory, repositoryInterface.getName());
133+
134+
RepositoryFragmentDiscovery fragmentConfiguration = new CdiRepositoryFragmentDiscovery(configuration);
135+
136+
return Arrays.stream(classMetadata.getInterfaceNames()) //
137+
.filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) //
138+
.map(it -> FragmentMetadata.of(it, fragmentConfiguration)) //
139+
.map(this::detectRepositoryFragmentConfiguration) //
140+
.flatMap(Optionals::toStream);
141+
}
142+
143+
/**
144+
* Retrieves a custom repository interfaces from a repository type. This works for the whole class hierarchy and can
145+
* find also a custom repository which is inherited over many levels.
146+
*
147+
* @param repositoryType The class representing the repository.
148+
* @param cdiRepositoryConfiguration The configuration for CDI usage.
149+
* @return the interface class or {@literal null}.
150+
*/
151+
Optional<Class<?>> getCustomImplementationClass(Class<?> repositoryType,
152+
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
153+
154+
String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
155+
156+
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation( //
157+
className, //
158+
className, Collections.singleton(repositoryType.getPackage().getName()), //
159+
Collections.emptySet(), //
160+
BeanDefinition::getBeanClassName);
161+
162+
return beanDefinition.map(it -> loadClass(it.getBeanClassName()));
163+
}
164+
165+
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
166+
FragmentMetadata configuration) {
167+
168+
String className = configuration.getFragmentImplementationClassName();
169+
170+
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(className, null,
171+
configuration.getBasePackages(), configuration.getExclusions(), BeanDefinition::getBeanClassName);
172+
173+
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd));
174+
}
175+
176+
private static ClassMetadata getClassMetadata(MetadataReaderFactory metadataReaderFactory, String className) {
177+
178+
try {
179+
return metadataReaderFactory.getMetadataReader(className).getClassMetadata();
180+
} catch (IOException e) {
181+
throw new CreationException(String.format("Cannot parse %s metadata.", className), e);
182+
}
183+
}
184+
185+
private static String getCustomImplementationClassName(Class<?> repositoryType,
186+
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
187+
188+
String configuredPostfix = cdiRepositoryConfiguration.getRepositoryImplementationPostfix();
189+
Assert.hasText(configuredPostfix, "Configured repository postfix must not be null or empty!");
190+
191+
return ClassUtils.getShortName(repositoryType) + configuredPostfix;
192+
}
193+
194+
@RequiredArgsConstructor
195+
private static class CdiRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery {
196+
197+
private final CdiRepositoryConfiguration configuration;
198+
199+
/*
200+
* (non-Javadoc)
201+
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters()
202+
*/
203+
@Override
204+
public Streamable<TypeFilter> getExcludeFilters() {
205+
return Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class));
206+
}
207+
208+
/*
209+
* (non-Javadoc)
210+
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
211+
*/
212+
@Override
213+
public Optional<String> getRepositoryImplementationPostfix() {
214+
return Optional.of(configuration.getRepositoryImplementationPostfix());
215+
}
216+
}
217+
}

src/main/java/org/springframework/data/repository/cdi/CdiRepositoryExtensionSupport.java

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@
3636
import org.slf4j.Logger;
3737
import org.slf4j.LoggerFactory;
3838
import org.springframework.core.annotation.AnnotationUtils;
39-
import org.springframework.core.env.Environment;
40-
import org.springframework.core.env.StandardEnvironment;
41-
import org.springframework.core.io.ResourceLoader;
42-
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
43-
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
44-
import org.springframework.core.type.classreading.MetadataReaderFactory;
4539
import org.springframework.data.repository.NoRepositoryBean;
4640
import org.springframework.data.repository.Repository;
4741
import org.springframework.data.repository.RepositoryDefinition;
@@ -61,16 +55,10 @@ public abstract class CdiRepositoryExtensionSupport implements Extension {
6155

6256
private final Map<Class<?>, Set<Annotation>> repositoryTypes = new HashMap<>();
6357
private final Set<CdiRepositoryBean<?>> eagerRepositories = new HashSet<>();
64-
private final CustomRepositoryImplementationDetector customImplementationDetector;
58+
private final CdiRepositoryContext context;
6559

6660
protected CdiRepositoryExtensionSupport() {
67-
68-
Environment environment = new StandardEnvironment();
69-
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(getClass().getClassLoader());
70-
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
71-
72-
this.customImplementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment,
73-
resourceLoader);
61+
context = new CdiRepositoryContext(getClass().getClassLoader());
7462
}
7563

7664
/**
@@ -91,8 +79,8 @@ protected <X> void processAnnotatedType(@Observes ProcessAnnotatedType<X> proces
9179
Set<Annotation> qualifiers = getQualifiers(repositoryType);
9280

9381
if (LOGGER.isDebugEnabled()) {
94-
LOGGER.debug(String.format("Discovered repository type '%s' with qualifiers %s.", repositoryType.getName(),
95-
qualifiers));
82+
LOGGER.debug(
83+
String.format("Discovered repository type '%s' with qualifiers %s.", repositoryType.getName(), qualifiers));
9684
}
9785
// Store the repository type using its qualifiers.
9886
repositoryTypes.put(repositoryType, qualifiers);
@@ -184,7 +172,15 @@ protected void registerBean(CdiRepositoryBean<?> bean) {
184172
* @return the {@link CustomRepositoryImplementationDetector} to scan for the custom implementation
185173
*/
186174
protected CustomRepositoryImplementationDetector getCustomImplementationDetector() {
187-
return customImplementationDetector;
175+
return context.getCustomRepositoryImplementationDetector();
176+
}
177+
178+
/**
179+
* @return the {@link CdiRepositoryContext} encapsulating the CDI-specific class loaders and fragment scanning.
180+
* @since 2.1
181+
*/
182+
protected CdiRepositoryContext getRepositoryContext() {
183+
return context;
188184
}
189185

190186
@SuppressWarnings("all")
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.config;
17+
18+
import lombok.Value;
19+
20+
import java.io.IOException;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.stream.Stream;
24+
25+
import org.springframework.beans.factory.BeanDefinitionStoreException;
26+
import org.springframework.core.type.AnnotationMetadata;
27+
import org.springframework.core.type.classreading.MetadataReaderFactory;
28+
import org.springframework.core.type.filter.AnnotationTypeFilter;
29+
import org.springframework.core.type.filter.TypeFilter;
30+
import org.springframework.data.repository.NoRepositoryBean;
31+
import org.springframework.data.util.StreamUtils;
32+
import org.springframework.util.Assert;
33+
import org.springframework.util.ClassUtils;
34+
35+
/**
36+
* Value object for a discovered Repository fragment interface.
37+
*
38+
* @author Mark Paluch
39+
* @since 2.1
40+
*/
41+
@Value(staticConstructor = "of")
42+
public class FragmentMetadata {
43+
44+
private String fragmentInterfaceName;
45+
private RepositoryFragmentDiscovery configuration;
46+
47+
/**
48+
* Returns whether the given interface is a fragment candidate.
49+
*
50+
* @param interfaceName must not be {@literal null} or empty.
51+
* @param factory must not be {@literal null}.
52+
* @return
53+
*/
54+
public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) {
55+
56+
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
57+
Assert.notNull(factory, "MetadataReaderFactory must not be null!");
58+
59+
AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory);
60+
61+
return !metadata.hasAnnotation(NoRepositoryBean.class.getName());
62+
}
63+
64+
/**
65+
* Returns the exclusions to be used when scanning for fragment implementations.
66+
*
67+
* @return
68+
*/
69+
public List<TypeFilter> getExclusions() {
70+
71+
Stream<TypeFilter> configurationExcludes = configuration.getExcludeFilters().stream();
72+
Stream<AnnotationTypeFilter> noRepositoryBeans = Stream.of(new AnnotationTypeFilter(NoRepositoryBean.class));
73+
74+
return Stream.concat(configurationExcludes, noRepositoryBeans).collect(StreamUtils.toUnmodifiableList());
75+
}
76+
77+
/**
78+
* Returns the name of the implementation class to be detected for the fragment interface.
79+
*
80+
* @return
81+
*/
82+
public String getFragmentImplementationClassName() {
83+
84+
String postfix = configuration.getRepositoryImplementationPostfix().orElse("Impl");
85+
86+
return ClassUtils.getShortName(fragmentInterfaceName).concat(postfix);
87+
}
88+
89+
/**
90+
* Returns the base packages to be scanned to find implementations of the current fragment interface.
91+
*
92+
* @return
93+
*/
94+
public Iterable<String> getBasePackages() {
95+
return Collections.singleton(ClassUtils.getPackageName(fragmentInterfaceName));
96+
}
97+
98+
private static AnnotationMetadata getAnnotationMetadata(String className,
99+
MetadataReaderFactory metadataReaderFactory) {
100+
101+
try {
102+
return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata();
103+
} catch (IOException e) {
104+
throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e);
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)