diff --git a/pom.xml b/pom.xml index 7ec86c87a1..efa483af19 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.0.0-SNAPSHOT + 3.0.0-dependencies-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. @@ -32,6 +32,7 @@ 2.11.7 1.4.24 + 1.0.0 spring.data.commons @@ -334,13 +335,6 @@ true - - de.schauderhaft.degraph - degraph-check - 0.1.4 - test - - org.jmolecules.integrations jmolecules-spring @@ -348,6 +342,13 @@ true + + com.tngtech.archunit + archunit + ${archunit.version} + test + + diff --git a/src/main/java/org/springframework/data/querydsl/QuerydslRepositoryRuntimeHints.java b/src/main/java/org/springframework/data/querydsl/QuerydslRepositoryRuntimeHints.java new file mode 100644 index 0000000000..542298de6e --- /dev/null +++ b/src/main/java/org/springframework/data/querydsl/QuerydslRepositoryRuntimeHints.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022 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.querydsl; + +import com.querydsl.core.types.Predicate; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.domain.Example; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsFactoryBean; +import org.springframework.data.repository.core.support.TransactionalRepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; +import org.springframework.data.repository.query.FluentQuery.ReactiveFluentQuery; +import org.springframework.data.repository.query.QueryByExampleExecutor; +import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +import java.util.Arrays; +import java.util.Properties; + +/** + * {@link RuntimeHintsRegistrar} holding required hints to bootstrap Querydsl repositories.
+ * Already registered via {@literal aot.factories}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 3.0 + */ +class QuerydslRepositoryRuntimeHints implements RuntimeHintsRegistrar { + + private static final boolean PROJECT_REACTOR_PRESENT = ClassUtils.isPresent("reactor.core.publisher.Flux", + QuerydslRepositoryRuntimeHints.class.getClassLoader()); + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + + if (QuerydslUtils.QUERY_DSL_PRESENT) { + + // repository infrastructure + hints.reflection().registerTypes(Arrays.asList( // + TypeReference.of(Predicate.class), // + TypeReference.of(QuerydslPredicateExecutor.class)), builder -> { + builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); + }); + + if (PROJECT_REACTOR_PRESENT) { + // repository infrastructure + hints.reflection().registerTypes(Arrays.asList( // + TypeReference.of(ReactiveQuerydslPredicateExecutor.class)), builder -> { + builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); + }); + } + } + } +} diff --git a/src/main/java/org/springframework/data/repository/aot/FallbackRepositoryAotConfigurationPostProcessor.java b/src/main/java/org/springframework/data/repository/aot/FallbackRepositoryAotConfigurationPostProcessor.java new file mode 100644 index 0000000000..1c3fdac53a --- /dev/null +++ b/src/main/java/org/springframework/data/repository/aot/FallbackRepositoryAotConfigurationPostProcessor.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022 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.aot; + +import org.springframework.data.repository.config.RepositoryConfigurationPostProcessor; + +/** + * default post processing to be applied when no other {@link RepositoryConfigurationPostProcessor} is applied. + * + * @author Mark Paluch + * @author Jens Schauder + * @since 3.0 + */ +class FallbackRepositoryAotConfigurationPostProcessor extends RepositoryRegistrationAotProcessor implements RepositoryConfigurationPostProcessor.FallbackRepositoryConfigurationPostProcessor { +} diff --git a/src/main/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessor.java b/src/main/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessor.java index 7a562e7705..9209c1517d 100644 --- a/src/main/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/repository/aot/RepositoryRegistrationAotProcessor.java @@ -38,6 +38,7 @@ import org.springframework.data.aot.TypeContributor; import org.springframework.data.repository.config.RepositoryConfiguration; import org.springframework.data.repository.config.RepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryConfigurationPostProcessor; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -56,16 +57,14 @@ * provide custom logic for contributing additional (eg. reflection) configuration. By default, reflection configuration * will be added for types reachable from the repository declaration and query methods as well as all used * {@link Annotation annotations} from the {@literal org.springframework.data} namespace. - *

- * The processor is typically configured via {@link RepositoryConfigurationExtension#getRepositoryAotProcessor()} and - * gets added by the {@link org.springframework.data.repository.config.RepositoryConfigurationDelegate}. + * * @author Christoph Strobl * @author John Blum * @since 3.0 */ @SuppressWarnings("unused") -public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotProcessor, BeanFactoryAware { +public class RepositoryRegistrationAotProcessor implements BeanRegistrationAotProcessor, BeanFactoryAware, RepositoryConfigurationPostProcessor { private ConfigurableListableBeanFactory beanFactory; @@ -117,6 +116,7 @@ protected ConfigurableListableBeanFactory getBeanFactory() { return this.beanFactory; } + @Override public void setConfigMap(@Nullable Map> configMap) { this.configMap = configMap; } diff --git a/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java b/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java index 568e02d7be..002b52f96f 100644 --- a/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java +++ b/src/main/java/org/springframework/data/repository/aot/hint/RepositoryRuntimeHints.java @@ -87,24 +87,6 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) }); } - if (QuerydslUtils.QUERY_DSL_PRESENT) { - - // repository infrastructure - hints.reflection().registerTypes(Arrays.asList( // - TypeReference.of(Predicate.class), // - TypeReference.of(QuerydslPredicateExecutor.class)), builder -> { - builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - }); - - if (PROJECT_REACTOR_PRESENT) { - // repository infrastructure - hints.reflection().registerTypes(Arrays.asList( // - TypeReference.of(ReactiveQuerydslPredicateExecutor.class)), builder -> { - builder.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS); - }); - } - } - // named queries hints.reflection().registerTypes(Arrays.asList( // TypeReference.of(Properties.class), // diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java index e34474c694..965c2fd7ba 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -46,6 +46,7 @@ import org.springframework.core.log.LogMessage; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; +import org.springframework.data.repository.config.RepositoryConfigurationPostProcessor.FallbackRepositoryConfigurationPostProcessor; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -83,11 +84,11 @@ public class RepositoryConfigurationDelegate { * {@link ResourceLoader} and {@link Environment}. * * @param configurationSource must not be {@literal null}. - * @param resourceLoader must not be {@literal null}. - * @param environment must not be {@literal null}. + * @param resourceLoader must not be {@literal null}. + * @param environment must not be {@literal null}. */ public RepositoryConfigurationDelegate(RepositoryConfigurationSource configurationSource, - ResourceLoader resourceLoader, Environment environment) { + ResourceLoader resourceLoader, Environment environment) { this.isXml = configurationSource instanceof XmlRepositoryConfigurationSource; boolean isAnnotation = configurationSource instanceof AnnotationRepositoryConfigurationSource; @@ -106,13 +107,13 @@ public RepositoryConfigurationDelegate(RepositoryConfigurationSource configurati * Defaults the environment in case the given one is null. Used as fallback, in case the legacy constructor was * invoked. * - * @param environment can be {@literal null}. + * @param environment can be {@literal null}. * @param resourceLoader can be {@literal null}. * @return the given {@link Environment} if not {@literal null}, a configured {@link Environment}, or a default - * {@link Environment}. + * {@link Environment}. */ private static Environment defaultEnvironment(@Nullable Environment environment, - @Nullable ResourceLoader resourceLoader) { + @Nullable ResourceLoader resourceLoader) { if (environment != null) { return environment; @@ -125,14 +126,14 @@ private static Environment defaultEnvironment(@Nullable Environment environment, /** * Registers the discovered repositories in the given {@link BeanDefinitionRegistry}. * - * @param registry {@link BeanDefinitionRegistry} in which to register the repository bean. + * @param registry {@link BeanDefinitionRegistry} in which to register the repository bean. * @param extension {@link RepositoryConfigurationExtension} for the module. * @return {@link BeanComponentDefinition}s for all repository bean definitions found. * @see org.springframework.data.repository.config.RepositoryConfigurationExtension * @see org.springframework.beans.factory.support.BeanDefinitionRegistry */ public List registerRepositoriesIn(BeanDefinitionRegistry registry, - RepositoryConfigurationExtension extension) { + RepositoryConfigurationExtension extension) { if (logger.isInfoEnabled()) { logger.info(LogMessage.format("Bootstrapping Spring Data %s repositories in %s mode.", // @@ -210,29 +211,52 @@ public List registerRepositoriesIn(BeanDefinitionRegist watch.getLastTaskTimeMillis(), configurations.size(), extension.getModuleName())); } - // TODO: AOT Processing -> guard this one with a flag so it's not always present - // TODO: With regard to AOT Processing, perhaps we need to be smart and detect whether "core" AOT components are - // (or rather configuration is) present on the classpath to enable Spring Data AOT component registration. - registerAotComponents(registry, extension, metadataByRepositoryBeanName); + registerPostProcessorComponents(registry, extension, metadataByRepositoryBeanName); return definitions; } - private void registerAotComponents(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension, - Map> metadataByRepositoryBeanName) { + private void registerPostProcessorComponents(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension, + Map> metadataByRepositoryBeanName) { - // module-specific repository aot processor - String repositoryAotProcessorBeanName = String.format("data-%s.repository-aot-processor" /* might be duplicate */, - extension.getModuleIdentifier()); + boolean hasPostProcessors = false; + List processors = SpringFactoriesLoader.loadFactories(RepositoryConfigurationPostProcessor.class, resourceLoader.getClassLoader()); + for (RepositoryConfigurationPostProcessor processor : processors) { - if (!registry.isBeanNameInUse(repositoryAotProcessorBeanName)) { + if (!processor.supports(extension)) { + continue; + } + + hasPostProcessors = true; + registerRepositoryPostProcessor(registry, extension, processor.getClass().getName(), metadataByRepositoryBeanName); + } + + if (!hasPostProcessors) { - BeanDefinitionBuilder repositoryAotProcessor = BeanDefinitionBuilder - .rootBeanDefinition(extension.getRepositoryAotProcessor()).setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + List fallback = SpringFactoriesLoader.loadFactories(FallbackRepositoryConfigurationPostProcessor.class, resourceLoader.getClassLoader()); - repositoryAotProcessor.addPropertyValue("configMap", metadataByRepositoryBeanName); + for (RepositoryConfigurationPostProcessor processor : fallback) { - registry.registerBeanDefinition(repositoryAotProcessorBeanName, repositoryAotProcessor.getBeanDefinition()); + if (!processor.supports(extension)) { + continue; + } + + registerRepositoryPostProcessor(registry, extension, processor.getClass().getName(), metadataByRepositoryBeanName); + } + } + } + + private static void registerRepositoryPostProcessor(BeanDefinitionRegistry registry, RepositoryConfigurationExtension extension, String className, Map> metadataByRepositoryBeanName) { + + String repositoryProcessorBeanName = String.format("data-%s.repository-post-processor.%s", + extension.getModuleIdentifier(), className); + + if (!registry.isBeanNameInUse(repositoryProcessorBeanName)) { + BeanDefinitionBuilder repositoryPostProcessor = BeanDefinitionBuilder + .rootBeanDefinition(className).setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + repositoryPostProcessor.addPropertyValue("configMap", metadataByRepositoryBeanName); + registry.registerBeanDefinition(repositoryProcessorBeanName, repositoryPostProcessor.getBeanDefinition()); } } @@ -242,10 +266,10 @@ private void registerAotComponents(BeanDefinitionRegistry registry, RepositoryCo * augment the {@link LazyRepositoryInjectionPointResolver}'s configuration if there already is one configured. * * @param configurations must not be {@literal null}. - * @param registry must not be {@literal null}. + * @param registry must not be {@literal null}. */ private static void potentiallyLazifyRepositories(Map> configurations, - BeanDefinitionRegistry registry, BootstrapMode mode) { + BeanDefinitionRegistry registry, BootstrapMode mode) { if (!DefaultListableBeanFactory.class.isInstance(registry) || BootstrapMode.DEFAULT.equals(mode)) { return; @@ -283,7 +307,7 @@ private static void potentiallyLazifyRepositories(Map> configurations) { diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java index e7ab33fd63..99d124b20c 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java @@ -18,13 +18,10 @@ import java.util.Collection; import java.util.Locale; -import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.io.ResourceLoader; -import org.springframework.data.repository.aot.RepositoryRegistrationAotProcessor; -import org.springframework.lang.NonNull; /** * SPI to implement store specific extension to the repository bean definition registration process. @@ -54,20 +51,6 @@ default String getModuleIdentifier() { */ String getModuleName(); - /** - * Returns the {@link BeanRegistrationAotProcessor} type responsible for contributing AOT/native configuration - * required by the Spring Data Repository infrastructure components at native runtime. - * - * @return the {@link BeanRegistrationAotProcessor} type responsible for contributing AOT/native configuration. - * Defaults to {@link RepositoryRegistrationAotProcessor}. Must not be {@literal null}. - * @see org.springframework.beans.factory.aot.BeanRegistrationAotProcessor - * @since 3.0 - */ - @NonNull - default Class getRepositoryAotProcessor() { - return RepositoryRegistrationAotProcessor.class; - } - /** * Returns all {@link RepositoryConfiguration}s obtained through the given {@link RepositoryConfigurationSource}. * diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationPostProcessor.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationPostProcessor.java new file mode 100644 index 0000000000..82ac8027de --- /dev/null +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationPostProcessor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 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 java.util.Map; + +/** + * Interface for components that post-process repository configuration of discovered repository interfaces. + * + * @author Jens Schauder + * @author Mark Paluch + * @since 3.0 + */ +public interface RepositoryConfigurationPostProcessor { + + /** + * Set the repository configuration map that contains repository bean names to {@link RepositoryConfiguration}. + * + * @param configMap + */ + void setConfigMap(Map> configMap); + + + /** + * Method to test whether this configuration post-processor can be applied to the given {@link RepositoryConfigurationExtension}. + * + * @param extension + * @return {@code true} if applicable; {@code false} otherwise to skip processing. + */ + default boolean supports(RepositoryConfigurationExtension extension) { + return true; + } + + /** + * Marker interface for default fallback post-processors. + */ + interface FallbackRepositoryConfigurationPostProcessor extends RepositoryConfigurationPostProcessor { + } + +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories index fc6274377f..8ab5772dd3 100644 --- a/src/main/resources/META-INF/spring.factories +++ b/src/main/resources/META-INF/spring.factories @@ -1,3 +1,4 @@ org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections, \ org.springframework.data.util.CustomCollections.EclipseCollections +org.springframework.data.repository.config.RepositoryConfigurationPostProcessor$FallbackRepositoryConfigurationPostProcessor=org.springframework.data.repository.aot.FallbackRepositoryAotConfigurationPostProcessor diff --git a/src/main/resources/META-INF/spring/aot.factories b/src/main/resources/META-INF/spring/aot.factories index bd2257e5e9..11efe4fbb0 100644 --- a/src/main/resources/META-INF/spring/aot.factories +++ b/src/main/resources/META-INF/spring/aot.factories @@ -2,7 +2,8 @@ org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\ org.springframework.data.aot.ManagedTypesBeanFactoryInitializationAotProcessor org.springframework.aot.hint.RuntimeHintsRegistrar=\ - org.springframework.data.repository.aot.hint.RepositoryRuntimeHints + org.springframework.data.repository.aot.hint.RepositoryRuntimeHints,\ + org.springframework.data.querydsl.QuerydslRepositoryRuntimeHints org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ org.springframework.data.aot.AuditingBeanRegistrationAotProcessor diff --git a/src/test/java/org/springframework/data/DependencyTests.java b/src/test/java/org/springframework/data/DependencyTests.java index 8a3e7a5114..a5ddaae75c 100644 --- a/src/test/java/org/springframework/data/DependencyTests.java +++ b/src/test/java/org/springframework/data/DependencyTests.java @@ -15,29 +15,160 @@ */ package org.springframework.data; -import static de.schauderhaft.degraph.check.JCheck.*; -import static org.junit.Assert.*; - -import org.junit.jupiter.api.Disabled; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.dependencies.SliceAssignment; +import com.tngtech.archunit.library.dependencies.SliceIdentifier; +import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; +import org.springframework.data.repository.core.RepositoryMetadata; /** + * Tests for package and slice cycles. + * + * All packages that have the same start including the part after {@literal data} are considered one slice. + * For example {@literal org.springframework.data.repository} and {@literal org.springframework.data.repository.support} + * are part of the same slice {@literal repository}. + * * @author Jens Schauder */ -@Disabled("Requires newer version of ASM 5.1") public class DependencyTests { + JavaClasses importedClasses = new ClassFileImporter() // + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) // + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS) // we just analyze the code of this module. + .importPackages("org.springframework.data") // + .that(onlySpringData()) // + .that(ignore(RepositoryMetadata.class)) // new cycle + ; + + @Test + void cycleFreeSlices() { + + ArchRule rule = SlicesRuleDefinition.slices() // + .matching("org.springframework.data.(*)..") // + .should() // + .beFreeOfCycles(); + + rule.check(importedClasses); + } + + @Test + void cycleFreePackages() { + + ArchRule rule = SlicesRuleDefinition.slices() // + .matching("org.springframework.data.(**)") // + .should() // + .beFreeOfCycles(); + + rule.check(importedClasses); + } + @Test - public void noInternalPackageCycles() { - - assertThat( - classpath() // - .noJars() // - .including("org.springframework.data.**") // - .filterClasspath("*target/classes") // - .printOnFailure("degraph.graphml"), // - violationFree() // - ); + void testGetFirstPackagePart() { + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(getFirstPackagePart("a.b.c")).isEqualTo("a"); + softly.assertThat(getFirstPackagePart("a")).isEqualTo("a"); + }); + } + + @Test + void testSubModule() { + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(subModule("a.b", "a.b.c.d")).isEqualTo("c"); + softly.assertThat(subModule("a.b", "a.b.c")).isEqualTo("c"); + softly.assertThat(subModule("a.b", "a.b")).isEqualTo(""); + }); + } + + private DescribedPredicate onlySpringData() { + + return new DescribedPredicate<>("Spring Data Classes") { + @Override + public boolean test(JavaClass input) { + return input.getPackageName().startsWith("org.springframework.data"); + } + }; + } + + private DescribedPredicate ignore(Class type) { + + return new DescribedPredicate<>("ignored class " + type.getName()) { + @Override + public boolean test(JavaClass input) { + return !input.getFullName().startsWith(type.getName()); + } + }; + } + + private DescribedPredicate ignorePackage(String type) { + + return new DescribedPredicate<>("ignored class " + type) { + @Override + public boolean test(JavaClass input) { + return !input.getPackageName().equals(type); + } + }; + } + + private String getFirstPackagePart(String subpackage) { + + int index = subpackage.indexOf("."); + if (index < 0) { + return subpackage; + } + return subpackage.substring(0, index); + } + + private String subModule(String basePackage, String packageName) { + + if (packageName.startsWith(basePackage) && packageName.length() > basePackage.length()) { + + final int index = basePackage.length() + 1; + String subpackage = packageName.substring(index); + return getFirstPackagePart(subpackage); + } + return ""; + } + + private SliceAssignment subModuleSlicing() { + return new SliceAssignment() { + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + + String packageName = javaClass.getPackageName(); + + String subModule = subModule("org.springframework.data.jdbc", packageName); + if (!subModule.isEmpty()) { + return SliceIdentifier.of(subModule); + } + + subModule = subModule("org.springframework.data.relational", packageName); + if (!subModule.isEmpty()) { + return SliceIdentifier.of(subModule); + } + + subModule = subModule("org.springframework.data", packageName); + if (!subModule.isEmpty()) { + return SliceIdentifier.of(subModule); + } + + return SliceIdentifier.ignore(); + } + + @Override + public String getDescription() { + return "Submodule"; + } + }; } }