DATACMNS-1371 - Improvements to custom implementation scanning.

CustomRepositoryImplementationDetector now works in two differend modes. If initialized with an ImplementationDetectionConfiguration, it will trigger a canonical, cached component scan for implementation types matching the configured name pattern. Individual custom implementation lookups will then select from this initially scanned set of bean definitions to pick the matching implementation class and potentially resolve ambiguities.
This commit is contained in:
Oliver Gierke
2018-08-09 18:00:56 +02:00
parent 3f613ff13f
commit 13b115068d
18 changed files with 746 additions and 363 deletions

View File

@@ -15,33 +15,26 @@
*/
package org.springframework.data.repository.cdi;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import java.util.stream.Stream;
import javax.enterprise.inject.CreationException;
import javax.enterprise.inject.UnsatisfiedResolutionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.config.CustomRepositoryImplementationDetector;
import org.springframework.data.repository.config.FragmentMetadata;
import org.springframework.data.repository.config.ImplementationDetectionConfiguration;
import org.springframework.data.repository.config.ImplementationLookupConfiguration;
import org.springframework.data.repository.config.RepositoryFragmentConfiguration;
import org.springframework.data.repository.config.RepositoryFragmentDiscovery;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
@@ -61,6 +54,7 @@ public class CdiRepositoryContext {
private final ClassLoader classLoader;
private final CustomRepositoryImplementationDetector detector;
private final MetadataReaderFactory metadataReaderFactory;
private final FragmentMetadata metdata;
/**
* Create a new {@link CdiRepositoryContext} given {@link ClassLoader} and initialize
@@ -69,16 +63,8 @@ public class CdiRepositoryContext {
* @param classLoader must not be {@literal null}.
*/
public CdiRepositoryContext(ClassLoader classLoader) {
Assert.notNull(classLoader, "ClassLoader must not be null!");
this.classLoader = classLoader;
Environment environment = new StandardEnvironment();
ResourceLoader resourceLoader = new PathMatchingResourcePatternResolver(classLoader);
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.detector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment, resourceLoader);
this(classLoader, new CustomRepositoryImplementationDetector(new StandardEnvironment(),
new PathMatchingResourcePatternResolver(classLoader)));
}
/**
@@ -97,6 +83,7 @@ public class CdiRepositoryContext {
this.classLoader = classLoader;
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.metdata = new FragmentMetadata(metadataReaderFactory);
this.detector = detector;
}
@@ -130,14 +117,11 @@ public class CdiRepositoryContext {
Stream<RepositoryFragmentConfiguration> getRepositoryFragments(CdiRepositoryConfiguration configuration,
Class<?> repositoryInterface) {
ClassMetadata classMetadata = getClassMetadata(metadataReaderFactory, repositoryInterface.getName());
CdiImplementationDetectionConfiguration config = new CdiImplementationDetectionConfiguration(configuration,
metadataReaderFactory);
RepositoryFragmentDiscovery fragmentConfiguration = new CdiRepositoryFragmentDiscovery(configuration);
return Arrays.stream(classMetadata.getInterfaceNames()) //
.filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) //
.map(it -> FragmentMetadata.of(it, fragmentConfiguration)) //
.map(this::detectRepositoryFragmentConfiguration) //
return metdata.getFragmentInterfaces(repositoryInterface.getName()) //
.map(it -> detectRepositoryFragmentConfiguration(it, config)) //
.flatMap(Optionals::toStream);
}
@@ -152,26 +136,22 @@ public class CdiRepositoryContext {
Optional<Class<?>> getCustomImplementationClass(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
String className = getCustomImplementationClassName(repositoryType, cdiRepositoryConfiguration);
ImplementationDetectionConfiguration configuration = new CdiImplementationDetectionConfiguration(
cdiRepositoryConfiguration, metadataReaderFactory);
ImplementationLookupConfiguration lookup = configuration.forFragment(repositoryType.getName());
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation( //
className, //
className, Collections.singleton(repositoryType.getPackage().getName()), //
Collections.emptySet(), //
BeanDefinition::getBeanClassName);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(lookup);
return beanDefinition.map(this::loadBeanClass);
}
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
FragmentMetadata configuration) {
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(String fragmentInterfaceName,
CdiImplementationDetectionConfiguration config) {
String className = configuration.getFragmentImplementationClassName();
ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterfaceName);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(lookup);
Optional<AbstractBeanDefinition> beanDefinition = detector.detectCustomImplementation(className, null,
configuration.getBasePackages(), configuration.getExclusions(), BeanDefinition::getBeanClassName);
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd));
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterfaceName, bd));
}
@Nullable
@@ -182,45 +162,37 @@ public class CdiRepositoryContext {
return beanClassName == null ? null : loadClass(beanClassName);
}
private static ClassMetadata getClassMetadata(MetadataReaderFactory metadataReaderFactory, String className) {
try {
return metadataReaderFactory.getMetadataReader(className).getClassMetadata();
} catch (IOException e) {
throw new CreationException(String.format("Cannot parse %s metadata.", className), e);
}
}
private static String getCustomImplementationClassName(Class<?> repositoryType,
CdiRepositoryConfiguration cdiRepositoryConfiguration) {
String configuredPostfix = cdiRepositoryConfiguration.getRepositoryImplementationPostfix();
Assert.hasText(configuredPostfix, "Configured repository postfix must not be null or empty!");
return ClassUtils.getShortName(repositoryType) + configuredPostfix;
}
@RequiredArgsConstructor
private static class CdiRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery {
private static class CdiImplementationDetectionConfiguration implements ImplementationDetectionConfiguration {
private final CdiRepositoryConfiguration configuration;
private final @Getter MetadataReaderFactory metadataReaderFactory;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters()
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getImplementationPostfix()
*/
@Override
public String getImplementationPostfix() {
return configuration.getRepositoryImplementationPostfix();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getBasePackages()
*/
@Override
public Streamable<String> getBasePackages() {
return Streamable.empty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getExcludeFilters()
*/
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
*/
@Override
public Optional<String> getRepositoryImplementationPostfix() {
return Optional.of(configuration.getRepositoryImplementationPostfix());
return Streamable.empty();
}
}
}

View File

@@ -15,29 +15,27 @@
*/
package org.springframework.data.repository.config;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;
/**
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository}
* Detects the custom implementation for a {@link org.springframework.data.repository.Repository} instance. If
* configured with a {@link ImplementationDetectionConfiguration} at construction time, the necessary component scan is
* executed on first access, cached and its result is the filtered on every further implementation lookup according to
* the given {@link ImplementationDetectionConfiguration}. If none is given initially, every invocation to
* {@link #detectCustomImplementation(String, String, ImplementationDetectionConfiguration)} will issue a new component
* scan.
*
* @author Oliver Gierke
* @author Mark Paluch
@@ -46,76 +44,91 @@ import org.springframework.util.Assert;
* @author Jens Schauder
* @author Mark Paluch
*/
@RequiredArgsConstructor
public class CustomRepositoryImplementationDetector {
private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/%s.class";
private static final String CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN = "**/*%s.class";
private static final String AMBIGUOUS_CUSTOM_IMPLEMENTATIONS = "Ambiguous custom implementations detected! Found %s but expected a single implementation!";
private final @NonNull MetadataReaderFactory metadataReaderFactory;
private final @NonNull Environment environment;
private final @NonNull ResourceLoader resourceLoader;
private final Environment environment;
private final ResourceLoader resourceLoader;
private final Lazy<Set<BeanDefinition>> implementationCandidates;
/**
* Tries to detect a custom implementation for a repository bean by classpath scanning.
*
* @param configuration the {@link RepositoryConfiguration} to consider.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found.
* Creates a new {@link CustomRepositoryImplementationDetector} with the given {@link Environment},
* {@link ResourceLoader} and {@link ImplementationDetectionConfiguration}. The latter will be registered for a
* one-time component scan for implementation candidates that will the be used and filtered in all subsequent calls to
* {@link #detectCustomImplementation(RepositoryConfiguration)}.
*
* @param environment must not be {@literal null}.
* @param resourceLoader must not be {@literal null}.
* @param configuration must not be {@literal null}.
*/
@SuppressWarnings("deprecation")
public Optional<AbstractBeanDefinition> detectCustomImplementation(RepositoryConfiguration<?> configuration) {
public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader,
ImplementationDetectionConfiguration configuration) {
// TODO 2.0: Extract into dedicated interface for custom implementation lookup configuration.
Assert.notNull(environment, "Environment must not be null!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
Assert.notNull(configuration, "ImplementationDetectionConfiguration must not be null!");
return detectCustomImplementation( //
configuration.getImplementationClassName(), //
configuration.getImplementationBeanName(), //
configuration.getImplementationBasePackages(), //
configuration.getExcludeFilters(), //
bd -> configuration.getConfigurationSource().generateBeanName(bd));
this.environment = environment;
this.resourceLoader = resourceLoader;
this.implementationCandidates = Lazy.of(() -> findCandidateBeanDefinitions(configuration));
}
/**
* Creates a new {@link CustomRepositoryImplementationDetector} with the given {@link Environment} and
* {@link ResourceLoader}. Calls to {@link #detectCustomImplementation(ImplementationLookupConfiguration)} will issue
* scans for
*
* @param environment must not be {@literal null}.
* @param resourceLoader must not be {@literal null}.
*/
public CustomRepositoryImplementationDetector(Environment environment, ResourceLoader resourceLoader) {
Assert.notNull(environment, "Environment must not be null!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
this.environment = environment;
this.resourceLoader = resourceLoader;
this.implementationCandidates = Lazy.empty();
}
/**
* Tries to detect a custom implementation for a repository bean by classpath scanning.
*
* @param className must not be {@literal null}.
* @param beanName may be {@literal null}
* @param basePackages must not be {@literal null}.
* @param excludeFilters must not be {@literal null}.
* @param beanNameGenerator must not be {@literal null}.
* @param lookup must not be {@literal null}.
* @return the {@code AbstractBeanDefinition} of the custom implementation or {@literal null} if none found.
*/
public Optional<AbstractBeanDefinition> detectCustomImplementation(String className, @Nullable String beanName,
Iterable<String> basePackages, Iterable<TypeFilter> excludeFilters,
Function<BeanDefinition, String> beanNameGenerator) {
public Optional<AbstractBeanDefinition> detectCustomImplementation(ImplementationLookupConfiguration lookup) {
Assert.notNull(className, "ClassName must not be null!");
Assert.notNull(basePackages, "BasePackages must not be null!");
Assert.notNull(lookup, "ImplementationLookupConfiguration must not be null!");
Set<BeanDefinition> definitions = findCandidateBeanDefinitions(className, basePackages, excludeFilters);
Set<BeanDefinition> definitions = implementationCandidates.getOptional()
.orElseGet(() -> findCandidateBeanDefinitions(lookup)).stream() //
.filter(lookup::matches) //
.collect(StreamUtils.toUnmodifiableSet());
return SelectionSet //
.of(definitions, c -> c.isEmpty() ? Optional.empty() : throwAmbiguousCustomImplementationException(c)) //
.filterIfNecessary(bd -> beanName != null && beanName.equals(beanNameGenerator.apply(bd)))//
.uniqueResult().map(it -> AbstractBeanDefinition.class.cast(it));
.filterIfNecessary(lookup::hasMatchingBeanName) //
.uniqueResult() //
.map(AbstractBeanDefinition.class::cast);
}
Set<BeanDefinition> findCandidateBeanDefinitions(String className, Iterable<String> basePackages,
Iterable<TypeFilter> excludeFilters) {
private Set<BeanDefinition> findCandidateBeanDefinitions(ImplementationDetectionConfiguration config) {
// Build pattern to lookup implementation class
String postfix = config.getImplementationPostfix();
// Build classpath scanner and lookup bean definition
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false,
environment);
provider.setResourceLoader(resourceLoader);
provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, className));
provider.setMetadataReaderFactory(metadataReaderFactory);
provider.setResourcePattern(String.format(CUSTOM_IMPLEMENTATION_RESOURCE_PATTERN, postfix));
provider.setMetadataReaderFactory(config.getMetadataReaderFactory());
provider.addIncludeFilter((reader, factory) -> true);
excludeFilters.forEach(it -> provider.addExcludeFilter(it));
config.getExcludeFilters().forEach(it -> provider.addExcludeFilter(it));
return Streamable.of(basePackages).stream()//
return config.getBasePackages().stream()//
.flatMap(it -> provider.findCandidateComponents(it).stream())//
.collect(Collectors.toSet());
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2018 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
*
* http://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 java.io.IOException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link ImplementationLookupConfiguration}.
*
* @author Oliver Gierke
* @since 2.1
*/
class DefaultImplementationLookupConfiguration implements ImplementationLookupConfiguration {
private final ImplementationDetectionConfiguration config;
private final String interfaceName;
private final String beanName;
/**
* Creates a new {@link DefaultImplementationLookupConfiguration} for the given
* {@link ImplementationDetectionConfiguration} and interface name.
*
* @param config must not be {@literal null}.
* @param implementationClassName must not be {@literal null} or empty.
*/
DefaultImplementationLookupConfiguration(ImplementationDetectionConfiguration config, String interfaceName) {
Assert.notNull(config, "ImplementationDetectionConfiguration must not be null!");
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
this.config = config;
this.interfaceName = interfaceName;
this.beanName = StringUtils
.uncapitalize(ClassUtils.getShortName(interfaceName).concat(config.getImplementationPostfix()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationLookupConfiguration#getImplementationBeanName()
*/
@Override
public String getImplementationBeanName() {
return beanName;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getImplementationPostfix()
*/
@Override
public String getImplementationPostfix() {
return config.getImplementationPostfix();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getExcludeFilters()
*/
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return config.getExcludeFilters().and(Streamable.of(new AnnotationTypeFilter(NoRepositoryBean.class)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getMetadataReaderFactory()
*/
@Override
public MetadataReaderFactory getMetadataReaderFactory() {
return config.getMetadataReaderFactory();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationDetectionConfiguration#getBasePackages()
*/
@Override
public Streamable<String> getBasePackages() {
return Streamable.of(ClassUtils.getPackageName(interfaceName));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationLookupConfiguration#getImplementationClassName()
*/
@Override
public String getImplementationClassName() {
return ClassUtils.getShortName(interfaceName).concat(getImplementationPostfix());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationLookupConfiguration#hasMatchingBeanName(org.springframework.beans.factory.config.BeanDefinition)
*/
@Override
public boolean hasMatchingBeanName(BeanDefinition definition) {
Assert.notNull(definition, "BeanDefinition must not be null!");
return beanName != null && beanName.equals(config.generateBeanName(definition));
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.ImplementationLookupConfiguration#matches(org.springframework.beans.factory.config.BeanDefinition, org.springframework.core.type.classreading.MetadataReaderFactory)
*/
@Override
public boolean matches(BeanDefinition definition) {
Assert.notNull(definition, "BeanDefinition must not be null!");
String beanClassName = definition.getBeanClassName();
if (beanClassName == null || isExcluded(beanClassName, getExcludeFilters())) {
return false;
}
String beanPackage = ClassUtils.getPackageName(beanClassName);
String shortName = ClassUtils.getShortName(beanClassName);
String localName = shortName.substring(shortName.lastIndexOf('.') + 1);
return localName.equals(getImplementationClassName()) //
&& getBasePackages().stream().anyMatch(it -> beanPackage.startsWith(it));
}
private boolean isExcluded(String beanClassName, Streamable<TypeFilter> filters) {
try {
MetadataReader reader = getMetadataReaderFactory().getMetadataReader(beanClassName);
return filters.stream().anyMatch(it -> matches(it, reader));
} catch (IOException o_O) {
return true;
}
}
private boolean matches(TypeFilter filter, MetadataReader reader) {
try {
return filter.match(reader, getMetadataReaderFactory());
} catch (IOException e) {
return false;
}
}
}

View File

@@ -21,11 +21,13 @@ import lombok.RequiredArgsConstructor;
import java.util.Optional;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.config.ConfigurationUtils;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
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.StringUtils;
@@ -179,4 +181,28 @@ public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSou
public Streamable<TypeFilter> getExcludeFilters() {
return configurationSource.getExcludeFilters();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfiguration#toImplementationDetectionConfiguration(org.springframework.core.type.classreading.MetadataReaderFactory)
*/
@Override
public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) {
Assert.notNull(factory, "MetadataReaderFactory must not be null!");
return configurationSource.toImplementationDetectionConfiguration(factory);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfiguration#toLookupConfiguration(org.springframework.core.type.classreading.MetadataReaderFactory)
*/
@Override
public ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory) {
Assert.notNull(factory, "MetadataReaderFactory must not be null!");
return toImplementationDetectionConfiguration(factory).forRepositoryConfiguration(this);
}
}

View File

@@ -15,34 +15,44 @@
*/
package org.springframework.data.repository.config;
import lombok.Value;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Stream;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.util.StreamUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Value object for a discovered Repository fragment interface.
*
* @author Mark Paluch
* @author Oliver Gierke
* @since 2.1
*/
@Value(staticConstructor = "of")
@RequiredArgsConstructor
public class FragmentMetadata {
private String fragmentInterfaceName;
private RepositoryFragmentDiscovery configuration;
private final MetadataReaderFactory factory;
/**
* Returns all interfaces to be considered fragment ones for the given source interface.
*
* @param interfaceName must not be {@literal null} or empty.
* @return
*/
public Stream<String> getFragmentInterfaces(String interfaceName) {
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
return Arrays.stream(getClassMetadata(interfaceName).getInterfaceNames()) //
.filter(this::isCandidate);
}
/**
* Returns whether the given interface is a fragment candidate.
@@ -51,55 +61,28 @@ public class FragmentMetadata {
* @param factory must not be {@literal null}.
* @return
*/
public static boolean isCandidate(String interfaceName, MetadataReaderFactory factory) {
private boolean isCandidate(String interfaceName) {
Assert.hasText(interfaceName, "Interface name must not be null or empty!");
Assert.notNull(factory, "MetadataReaderFactory must not be null!");
AnnotationMetadata metadata = getAnnotationMetadata(interfaceName, factory);
AnnotationMetadata metadata = getAnnotationMetadata(interfaceName);
return !metadata.hasAnnotation(NoRepositoryBean.class.getName());
}
/**
* Returns the exclusions to be used when scanning for fragment implementations.
*
* @return
*/
public List<TypeFilter> getExclusions() {
Stream<TypeFilter> configurationExcludes = configuration.getExcludeFilters().stream();
Stream<AnnotationTypeFilter> noRepositoryBeans = Stream.of(new AnnotationTypeFilter(NoRepositoryBean.class));
return Stream.concat(configurationExcludes, noRepositoryBeans).collect(StreamUtils.toUnmodifiableList());
}
/**
* Returns the name of the implementation class to be detected for the fragment interface.
*
* @return
*/
public String getFragmentImplementationClassName() {
String postfix = configuration.getRepositoryImplementationPostfix().orElse("Impl");
return ClassUtils.getShortName(fragmentInterfaceName).concat(postfix);
}
/**
* Returns the base packages to be scanned to find implementations of the current fragment interface.
*
* @return
*/
public Iterable<String> getBasePackages() {
return Collections.singleton(ClassUtils.getPackageName(fragmentInterfaceName));
}
private static AnnotationMetadata getAnnotationMetadata(String className,
MetadataReaderFactory metadataReaderFactory) {
private AnnotationMetadata getAnnotationMetadata(String className) {
try {
return metadataReaderFactory.getMetadataReader(className).getAnnotationMetadata();
return factory.getMetadataReader(className).getAnnotationMetadata();
} catch (IOException e) {
throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e);
}
}
private ClassMetadata getClassMetadata(String className) {
try {
return factory.getMetadataReader(className).getClassMetadata();
} catch (IOException e) {
throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e);
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2018 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
*
* http://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.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Expresses configuration to be used to detect implementation classes for repositories and repository fragments.
*
* @author Oliver Gierke
* @since 2.1
*/
public interface ImplementationDetectionConfiguration {
/**
* Returns the postfix to be used to calculate the implementation type's name.
*
* @return must not be {@literal null}.
*/
String getImplementationPostfix();
/**
* Return the base packages to be scanned for implementation types.
*
* @return must not be {@literal null}.
*/
Streamable<String> getBasePackages();
/**
* Returns the exclude filters to be used for the implementation class scanning.
*
* @return must not be {@literal null}.
*/
Streamable<TypeFilter> getExcludeFilters();
/**
* Returns the {@link MetadataReaderFactory} to be used for implementation class scanning.
*
* @return must not be {@literal null}.
*/
MetadataReaderFactory getMetadataReaderFactory();
/**
* Generate the bean name for the given {@link BeanDefinition}.
*
* @param definition must not be {@literal null}.
* @return
*/
default String generateBeanName(BeanDefinition definition) {
Assert.notNull(definition, "BeanDefinition must not be null!");
String beanName = definition.getBeanClassName();
if (beanName == null) {
throw new IllegalStateException("Cannot generate bean name for BeanDefinition without bean class name!");
}
return StringUtils.uncapitalize(ClassUtils.getShortName(beanName));
}
/**
* Returns the final lookup configuration for the given fully-qualified fragment interface name.
*
* @param fragmentInterfaceName must not be {@literal null} or empty.
* @return
*/
default ImplementationLookupConfiguration forFragment(String fragmentInterfaceName) {
Assert.hasText(fragmentInterfaceName, "Fragment interface name must not be null or empty!");
return new DefaultImplementationLookupConfiguration(this, fragmentInterfaceName);
}
/**
* Returns the final lookup configuration for the given {@link RepositoryConfiguration}.
*
* @param config must not be {@literal null}.
* @return
*/
default ImplementationLookupConfiguration forRepositoryConfiguration(RepositoryConfiguration<?> config) {
Assert.notNull(config, "RepositoryConfiguration must not be null!");
return new DefaultImplementationLookupConfiguration(this, config.getRepositoryInterface()) {
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.DefaultImplementationLookupConfiguration#getBasePackages()
*/
@Override
public Streamable<String> getBasePackages() {
return config.getImplementationBasePackages();
}
};
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2018 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
*
* http://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.springframework.beans.factory.config.BeanDefinition;
/**
* Configuration that's used to lookup an implementation type for a repository or fragment interface.
*
* @author Oliver Gierke
* @since 2.1
*/
public interface ImplementationLookupConfiguration extends ImplementationDetectionConfiguration {
/**
* Returns the bean name of the implementation to be looked up.
*
* @return must not be {@literal null}.
*/
String getImplementationBeanName();
/**
* Returns the simple name of the class to be looked up.
*
* @return must not be {@literal null}.
*/
String getImplementationClassName();
/**
* Return whether the given {@link BeanDefinition} matches the lookup configuration.
*
* @param definition must not be {@literal null}.
* @return
*/
boolean matches(BeanDefinition definition);
/**
* Returns whether the bean name created for the given bean definition results in the one required. Will be used to
* disambiguate between multiple {@link BeanDefinition}s matching in general.
*
* @param definition must not be {@literal null}.
* @return
* @see #matches(BeanDefinition)
*/
boolean hasMatchingBeanName(BeanDefinition definition);
}

View File

@@ -15,10 +15,6 @@
*/
package org.springframework.data.repository.config;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -26,21 +22,17 @@ import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.config.ParsingUtils;
import org.springframework.data.repository.core.support.RepositoryFragment;
import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
/**
@@ -60,6 +52,7 @@ class RepositoryBeanDefinitionBuilder {
private final ResourceLoader resourceLoader;
private final MetadataReaderFactory metadataReaderFactory;
private final FragmentMetadata fragmentMetadata;
private final CustomRepositoryImplementationDetector implementationDetector;
/**
@@ -72,7 +65,7 @@ class RepositoryBeanDefinitionBuilder {
* @param environment must not be {@literal null}.
*/
public RepositoryBeanDefinitionBuilder(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension,
ResourceLoader resourceLoader, Environment environment) {
RepositoryConfigurationSource configurationSource, ResourceLoader resourceLoader, Environment environment) {
Assert.notNull(extension, "RepositoryConfigurationExtension must not be null!");
Assert.notNull(resourceLoader, "ResourceLoader must not be null!");
@@ -81,9 +74,12 @@ class RepositoryBeanDefinitionBuilder {
this.registry = registry;
this.extension = extension;
this.resourceLoader = resourceLoader;
this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
this.implementationDetector = new CustomRepositoryImplementationDetector(metadataReaderFactory, environment,
resourceLoader);
this.fragmentMetadata = new FragmentMetadata(metadataReaderFactory);
this.implementationDetector = new CustomRepositoryImplementationDetector(environment, resourceLoader,
configurationSource.toImplementationDetectionConfiguration(metadataReaderFactory));
}
/**
@@ -135,22 +131,23 @@ class RepositoryBeanDefinitionBuilder {
return builder;
}
@SuppressWarnings("deprecation")
private Optional<String> registerCustomImplementation(RepositoryConfiguration<?> configuration) {
String beanName = configuration.getImplementationBeanName();
ImplementationLookupConfiguration lookup = configuration.toLookupConfiguration(metadataReaderFactory);
String beanName = lookup.getImplementationBeanName();
// Already a bean configured?
if (registry.containsBeanDefinition(beanName)) {
return Optional.of(beanName);
}
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(configuration);
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(lookup);
return beanDefinition.map(it -> {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registering custom repository implementation: " + configuration.getImplementationBeanName() + " "
LOGGER.debug("Registering custom repository implementation: " + lookup.getImplementationBeanName() + " "
+ it.getBeanClassName());
}
@@ -164,27 +161,23 @@ class RepositoryBeanDefinitionBuilder {
private Stream<RepositoryFragmentConfiguration> registerRepositoryFragmentsImplementation(
RepositoryConfiguration<?> configuration) {
ClassMetadata classMetadata = getClassMetadata(configuration.getRepositoryInterface());
RepositoryFragmentDiscovery fragmentConfiguration = new DefaultRepositoryFragmentDiscovery(configuration);
ImplementationDetectionConfiguration config = configuration
.toImplementationDetectionConfiguration(metadataReaderFactory);
return Arrays.stream(classMetadata.getInterfaceNames()) //
.filter(it -> FragmentMetadata.isCandidate(it, metadataReaderFactory)) //
.map(it -> FragmentMetadata.of(it, fragmentConfiguration)) //
.map(it -> detectRepositoryFragmentConfiguration(it, configuration.getConfigurationSource())) //
return fragmentMetadata.getFragmentInterfaces(configuration.getRepositoryInterface()) //
.map(it -> detectRepositoryFragmentConfiguration(it, config)) //
.flatMap(Optionals::toStream) //
.peek(it -> potentiallyRegisterFragmentImplementation(configuration, it)) //
.peek(it -> potentiallyRegisterRepositoryFragment(configuration, it));
}
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(
FragmentMetadata configuration, RepositoryConfigurationSource configurationSource) {
private Optional<RepositoryFragmentConfiguration> detectRepositoryFragmentConfiguration(String fragmentInterface,
ImplementationDetectionConfiguration config) {
String className = configuration.getFragmentImplementationClassName();
ImplementationLookupConfiguration lookup = config.forFragment(fragmentInterface);
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(lookup);
Optional<AbstractBeanDefinition> beanDefinition = implementationDetector.detectCustomImplementation(className, null,
configuration.getBasePackages(), configuration.getExclusions(), configurationSource::generateBeanName);
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(configuration.getFragmentInterfaceName(), bd));
return beanDefinition.map(bd -> new RepositoryFragmentConfiguration(fragmentInterface, bd));
}
private void potentiallyRegisterFragmentImplementation(RepositoryConfiguration<?> repositoryConfiguration,
@@ -232,37 +225,4 @@ class RepositoryBeanDefinitionBuilder {
registry.registerBeanDefinition(beanName,
ParsingUtils.getSourceBeanDefinition(fragmentBuilder, configuration.getSource()));
}
private ClassMetadata getClassMetadata(String className) {
try {
return metadataReaderFactory.getMetadataReader(className).getClassMetadata();
} catch (IOException e) {
throw new BeanDefinitionStoreException(String.format("Cannot parse %s metadata.", className), e);
}
}
@RequiredArgsConstructor
private static class DefaultRepositoryFragmentDiscovery implements RepositoryFragmentDiscovery {
private final RepositoryConfiguration<?> configuration;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getExcludeFilters()
*/
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return configuration.getExcludeFilters();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryFragmentDiscovery#getRepositoryImplementationPostfix()
*/
@Override
public Optional<String> getRepositoryImplementationPostfix() {
return configuration.getConfigurationSource().getRepositoryImplementationPostfix();
}
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.repository.config;
import java.util.Optional;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.util.Streamable;
@@ -67,24 +68,6 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
*/
Optional<String> getNamedQueriesLocation();
/**
* Returns the class name of the custom implementation.
*
* @return
* @deprecated since 2.0. Use repository compositions by creating mixins.
*/
@Deprecated
String getImplementationClassName();
/**
* Returns the bean name of the custom implementation.
*
* @return
* @deprecated since 2.0. Use repository compositions by creating mixins.
*/
@Deprecated
String getImplementationBeanName();
/**
* Returns the name of the repository base class to be used or {@literal null} if the store specific defaults shall be
* applied.
@@ -129,4 +112,22 @@ public interface RepositoryConfiguration<T extends RepositoryConfigurationSource
* @return
*/
Streamable<TypeFilter> getExcludeFilters();
/**
* Returns the {@link ImplementationDetectionConfiguration} to be used for this repository.
*
* @param factory must not be {@literal null}.
* @return will never be {@literal null}.
* @since 2.1
*/
ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory);
/**
* Returns the {@link ImplementationLookupConfiguration} for the given {@link MetadataReaderFactory}.
*
* @param factory must not be {@literal null}.
* @return will never be {@literal null}.
* @since 2.1
*/
ImplementationLookupConfiguration toLookupConfiguration(MetadataReaderFactory factory);
}

View File

@@ -113,8 +113,8 @@ public class RepositoryConfigurationDelegate {
extension.registerBeansForRoot(registry, configurationSource);
RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension, resourceLoader,
environment);
RepositoryBeanDefinitionBuilder builder = new RepositoryBeanDefinitionBuilder(registry, extension,
configurationSource, resourceLoader, environment);
List<BeanComponentDefinition> definitions = new ArrayList<>();
if (LOG.isDebugEnabled()) {

View File

@@ -19,6 +19,7 @@ import java.util.Optional;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.util.Streamable;
@@ -126,4 +127,14 @@ public interface RepositoryConfigurationSource {
* @since 2.0
*/
String generateBeanName(BeanDefinition beanDefinition);
/**
* Returns the {@link ImplementationDetectionConfiguration} to be used to scan for custom implementations of the
* repository instances to be created from this {@link RepositoryConfigurationSource}.
*
* @param factory
* @return will never be {@literal null}.
* @since 2.1
*/
ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory);
}

View File

@@ -15,12 +15,16 @@
*/
package org.springframework.data.repository.config;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Collections;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.util.Streamable;
import org.springframework.util.Assert;
@@ -117,4 +121,57 @@ public abstract class RepositoryConfigurationSourceSupport implements Repository
public boolean shouldConsiderNestedRepositories() {
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationSource#toImplementationDetectionConfiguration()
*/
@Override
public ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory) {
return new SpringImplementationDetectionConfiguration(this, factory);
}
@RequiredArgsConstructor
private class SpringImplementationDetectionConfiguration implements ImplementationDetectionConfiguration {
private final RepositoryConfigurationSource source;
private final @Getter MetadataReaderFactory metadataReaderFactory;
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getImplementationPostfix()
*/
@Override
public String getImplementationPostfix() {
return source.getRepositoryImplementationPostfix()
.orElse(DefaultRepositoryConfiguration.DEFAULT_REPOSITORY_IMPLEMENTATION_POSTFIX);
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getBasePackages()
*/
@Override
public Streamable<String> getBasePackages() {
return source.getBasePackages();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#getExcludeFilters()
*/
@Override
public Streamable<TypeFilter> getExcludeFilters() {
return source.getExcludeFilters();
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.CustomRepositoryImplementationDetector.ImplementationDetectionConfiguration#generateBeanName(org.springframework.beans.factory.config.BeanDefinition)
*/
@Override
public String generateBeanName(BeanDefinition definition) {
return source.generateBeanName(definition);
}
}
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2018 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
*
* http://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 java.util.Optional;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.util.Streamable;
/**
* Interface containing the configurable options for the Spring Data repository fragment subsystem.
*
* @author Mark Paluch
* @since 2.1
* @see RepositoryConfigurationSource
*/
public interface RepositoryFragmentDiscovery {
/**
* Returns the {@link TypeFilter}s to be used to exclude packages from repository scanning.
*
* @return
*/
Streamable<TypeFilter> getExcludeFilters();
/**
* Returns the configured postfix to be used for looking up custom implementation classes.
*
* @return the postfix to use or {@link Optional#empty()} in case none is configured.
*/
Optional<String> getRepositoryImplementationPostfix();
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.util;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
@@ -35,9 +37,12 @@ import org.springframework.util.Assert;
* @since 2.0
*/
@RequiredArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Lazy<T> implements Supplier<T> {
private static final Lazy<?> EMPTY = new Lazy<>(() -> null, null, true);
private final Supplier<? extends T> supplier;
private @Nullable T value = null;
private boolean resolved = false;
@@ -67,6 +72,17 @@ public class Lazy<T> implements Supplier<T> {
return new Lazy<>(() -> value);
}
/**
* Creates a pre-resolved empty {@link Lazy}.
*
* @return
* @since 2.1
*/
@SuppressWarnings("unchecked")
public static <T> Lazy<T> empty() {
return (Lazy<T>) EMPTY;
}
/**
* Returns the value created by the configured {@link Supplier}. Will return the calculated instance for subsequent
* lookups.

View File

@@ -30,9 +30,10 @@ import org.springframework.util.Assert;
*
* @author Oliver Gierke
* @author Christoph Strobl
* @since 2.0
*/
@FunctionalInterface
public interface Streamable<T> extends Iterable<T> {
public interface Streamable<T> extends Iterable<T>, Supplier<Stream<T>> {
/**
* Returns an empty {@link Streamable}.
@@ -130,4 +131,26 @@ public interface Streamable<T> extends Iterable<T> {
default boolean isEmpty() {
return !iterator().hasNext();
}
/**
* Creates a new {@link Streamable} from the current one and the given {@link Stream} concatenated.
*
* @param stream must not be {@literal null}.
* @return
* @since 2.1
*/
default Streamable<T> and(Supplier<? extends Stream<? extends T>> stream) {
Assert.notNull(stream, "Stream must not be null!");
return Streamable.of(() -> Stream.concat(this.stream(), stream.get()));
}
/*
* (non-Javadoc)
* @see java.util.function.Supplier#get()
*/
default Stream<T> get() {
return stream();
}
}