From 22b2efe18244c00c188e69e527202bdd79f63f98 Mon Sep 17 00:00:00 2001 From: Leon van Zantvoort Date: Sat, 11 Jul 2026 21:01:28 +0200 Subject: [PATCH] feat(spring): @EnableStormRepositories for switching on repository scanning Registering Storm repositories in plain Spring meant extending RepositoryBeanFactoryPostProcessor and overriding getRepositoryBasePackages(). That works, but it is not how Spring users expect to switch on a data library. - @EnableStormRepositories(basePackages/basePackageClasses, ormTemplateBeanName, repositoryPrefix) in storm-spring, mirroring @EnableJpaRepositories; without packages, the annotated class's package is scanned. The imported registrar binds repositories through the language stack on the classpath: the Kotlin adapter when storm-kotlin-spring is present, the Java adapter otherwise. - In Spring Boot the annotation doubles as the explicit override: the registrar-provided post-processor makes the starter's auto-configured scanning back off. Starter behavior without the annotation is unchanged. - The RepositoryBeanFactoryPostProcessor adapters gain configuring constructors (basePackages, ormTemplateBeanName, repositoryPrefix), so multi-template applications define one bean per repository set instead of one subclass per domain. Fixes #201 --- CHANGELOG.md | 1 + docs/spring-integration.md | 66 ++++++---- .../RepositoryBeanFactoryPostProcessor.kt | 17 ++- .../orm/spring/EnableStormRepositoriesTest.kt | 57 +++++++++ .../StormAutoConfigurationTest.java | 21 ++++ .../orm/spring/EnableStormRepositories.java | 73 +++++++++++ .../RepositoryBeanFactoryPostProcessor.java | 44 +++++++ .../impl/StormRepositoriesRegistrar.java | 90 +++++++++++++ .../spring/EnableStormRepositoriesTest.java | 119 ++++++++++++++++++ 9 files changed, 459 insertions(+), 29 deletions(-) create mode 100644 storm-kotlin-spring/src/test/kotlin/st/orm/spring/EnableStormRepositoriesTest.kt create mode 100644 storm-spring/src/main/java/st/orm/spring/EnableStormRepositories.java create mode 100644 storm-spring/src/main/java/st/orm/spring/impl/StormRepositoriesRegistrar.java create mode 100644 storm-spring/src/test/java/st/orm/spring/EnableStormRepositoriesTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b7edd7d05..fa002fb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Framework integration points are now instance-scoped (#198), and the Ktor integr - Shared Spring Boot auto-configurations in storm-spring (`st.orm.spring.boot`): `StormTransactionAutoConfiguration` (Spring-aware provider beans), `StormValidationAutoConfiguration` (startup schema validation), and the `storm.*` configuration properties (`StormProperties`), used by both starters. Configuration keys are unchanged. - SQL failures raised by Storm translate to Spring's `DataAccessException` hierarchy in Spring applications: `SpringExceptionMapper` (storm-spring) translates on vendor error codes with `SQLException` subclass and SQL state fallback, auto-configured by both starters (`storm.exception-translation.enabled=false` to disable) and applied by the `SpringOrmTemplate.of` / `springOrmTemplate` compositions. Failures without a `SQLException` cause keep Storm's own exceptions. - The Spring Boot starters bind query observations automatically: with an `ObservationRegistry` bean present (Actuator provides one), every query executed by the auto-configured template reports as a `storm.query` Micrometer Observation via the shared `StormObservationAutoConfiguration`. The starters ship storm-micrometer; override the convention with an `ObservationConvention` bean, replace the binding with a `QueryObserver` bean, or disable via `management.observations.enable.storm.query=false`. +- `@EnableStormRepositories(basePackages = ...)` (storm-spring, usable from both language stacks): switches on repository scanning in plain Spring applications, mirroring `@EnableJpaRepositories`; without packages, the annotated class's package is scanned. In Spring Boot the annotation doubles as the explicit override: the auto-configured scanning backs off. The `RepositoryBeanFactoryPostProcessor` adapters also gain configuring constructors (base packages, template bean name, prefix), so multi-template applications define one bean per repository set instead of one subclass. ### Changed diff --git a/docs/spring-integration.md b/docs/spring-integration.md index a5ebd44e6..06f459b11 100644 --- a/docs/spring-integration.md +++ b/docs/spring-integration.md @@ -145,14 +145,24 @@ fun processUsers() { ### Repository Injection -Storm repositories are interfaces with default method implementations. Spring cannot discover them automatically because they are not annotated with `@Component` or `@Repository`. The `RepositoryBeanFactoryPostProcessor` (in `st.orm.spring.kotlin` since 1.13) scans specified packages for interfaces that extend `EntityRepository` or `ProjectionRepository` and registers them as Spring beans. This makes them available for constructor injection like any other Spring-managed dependency. +Storm repositories are interfaces with default method implementations. Spring cannot discover them automatically because they are not annotated with `@Component` or `@Repository`. Enable scanning with `@EnableStormRepositories`, mirroring annotations like `@EnableJpaRepositories`: the given packages are scanned for interfaces that extend `EntityRepository` or `ProjectionRepository`, and each is registered as a Spring bean, available for constructor injection like any other Spring-managed dependency. ```kotlin @Configuration -class AcmeRepositoryBeanFactoryPostProcessor : RepositoryBeanFactoryPostProcessor() { +@EnableStormRepositories(basePackages = ["com.acme.repository"]) +class AcmeConfiguration +``` + +Without explicit packages, the package of the annotated class is scanned. For full control, or for multiple repository sets bound to different templates, define `RepositoryBeanFactoryPostProcessor` beans (from `st.orm.spring.kotlin`) instead: + +```kotlin +@Configuration +class AcmeRepositoryConfiguration { - override fun getRepositoryBasePackages(): Array = - arrayOf("com.acme.repository") + @Bean + fun acmeRepositories() = RepositoryBeanFactoryPostProcessor( + basePackages = arrayOf("com.acme.repository"), + ) } ``` @@ -220,15 +230,25 @@ A template created with `ORMTemplate.of(dataSource)` works too, but manages its ### Repository Injection -Register a `RepositoryBeanFactoryPostProcessor` that scans your repository packages. This works identically to the Kotlin version: Storm discovers interfaces extending `EntityRepository` or `ProjectionRepository` and registers them as Spring beans. +Enable repository scanning with `@EnableStormRepositories`. This works identically to the Kotlin version: Storm discovers interfaces extending `EntityRepository` or `ProjectionRepository` and registers them as Spring beans. + +```java +@Configuration +@EnableStormRepositories(basePackages = "com.acme.repository") +public class AcmeConfiguration { +} +``` + +For full control, or for multiple repository sets bound to different templates, define `RepositoryBeanFactoryPostProcessor` beans instead: ```java @Configuration -public class AcmeRepositoryBeanFactoryPostProcessor extends RepositoryBeanFactoryPostProcessor { +public class AcmeRepositoryConfiguration { - @Override - public String[] getRepositoryBasePackages() { - return new String[] { "com.acme.repository" }; + @Bean + public RepositoryBeanFactoryPostProcessor acmeRepositories() { + return new RepositoryBeanFactoryPostProcessor( + new String[] { "com.acme.repository" }, null, ""); } } ``` @@ -537,11 +557,8 @@ class StormConfig(private val dataSource: DataSource) { ```kotlin @Configuration -class MyRepositoryPostProcessor : RepositoryBeanFactoryPostProcessor() { - - override fun getRepositoryBasePackages(): Array = - arrayOf("com.myapp.repository", "com.myapp.other") -} +@EnableStormRepositories(basePackages = ["com.myapp.repository", "com.myapp.other"]) +class RepositoryConfiguration ``` ### Minimal Spring Boot Setup (without Starter) @@ -564,11 +581,8 @@ class StormConfig { } @Configuration -class MyRepositoryBeanFactoryPostProcessor : RepositoryBeanFactoryPostProcessor() { - - override fun getRepositoryBasePackages(): Array = - arrayOf("com.myapp.repository") -} +@EnableStormRepositories(basePackages = ["com.myapp.repository"]) +class RepositoryConfiguration ``` This gives you: @@ -637,14 +651,14 @@ Repositories bind to a specific template through a per-domain post-processor. Th ```kotlin @Configuration -class BillingRepositoryPostProcessor : RepositoryBeanFactoryPostProcessor() { - - override fun getOrmTemplateBeanName(): String = "billingTemplate" +class BillingRepositoryConfiguration { - override fun getRepositoryPrefix(): String = "billing" - - override fun getRepositoryBasePackages(): Array = - arrayOf("com.myapp.billing.repository") + @Bean + fun billingRepositories() = RepositoryBeanFactoryPostProcessor( + basePackages = arrayOf("com.myapp.billing.repository"), + ormTemplateBeanName = "billingTemplate", + repositoryPrefix = "billing", + ) } ``` diff --git a/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt b/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt index 330751f9c..f40e7298f 100644 --- a/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt +++ b/storm-kotlin-spring/src/main/kotlin/st/orm/spring/kotlin/RepositoryBeanFactoryPostProcessor.kt @@ -27,11 +27,22 @@ import st.orm.template.ORMTemplate * BeanFactoryPostProcessor that scans base packages for Repository interfaces and registers them as beans. * * The scanning and registration engine lives in [AbstractRepositoryBeanFactoryPostProcessor]; this class binds - * it to the Kotlin API's repository types. Subclasses override the engine's methods, e.g. - * `override fun getRepositoryBasePackages(): Array`. + * it to the Kotlin API's repository types. Configure through the constructor (one bean per domain in + * multi-template applications), by subclassing (`override fun getRepositoryBasePackages(): Array`), + * or with `@EnableStormRepositories`. */ @Component -open class RepositoryBeanFactoryPostProcessor : AbstractRepositoryBeanFactoryPostProcessor() { +open class RepositoryBeanFactoryPostProcessor( + private val basePackages: Array = emptyArray(), + private val ormTemplateBeanName: String? = null, + private val repositoryPrefix: String = "", +) : AbstractRepositoryBeanFactoryPostProcessor() { + + override fun getRepositoryBasePackages(): Array = if (basePackages.isNotEmpty()) basePackages.copyOf() else super.getRepositoryBasePackages() + + override fun getOrmTemplateBeanName(): String? = ormTemplateBeanName ?: super.getOrmTemplateBeanName() + + override fun getRepositoryPrefix(): String = repositoryPrefix.ifEmpty { super.getRepositoryPrefix() } override fun getRepositoryType(): Class<*> = Repository::class.java diff --git a/storm-kotlin-spring/src/test/kotlin/st/orm/spring/EnableStormRepositoriesTest.kt b/storm-kotlin-spring/src/test/kotlin/st/orm/spring/EnableStormRepositoriesTest.kt new file mode 100644 index 000000000..d9e34de99 --- /dev/null +++ b/storm-kotlin-spring/src/test/kotlin/st/orm/spring/EnableStormRepositoriesTest.kt @@ -0,0 +1,57 @@ +package st.orm.spring + +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import org.junit.jupiter.api.Test +import org.springframework.boot.jdbc.DataSourceBuilder +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.io.ClassPathResource +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator +import st.orm.spring.repository.VisitRepository +import st.orm.template.ORMTemplate +import javax.sql.DataSource + +/** + * Verifies that [EnableStormRepositories] binds repositories through the Kotlin adapter when + * storm-kotlin-spring is on the classpath. + */ +class EnableStormRepositoriesTest { + + @Configuration + open class DatabaseConfiguration { + @Bean + open fun dataSource(): DataSource { + val dataSource = DataSourceBuilder.create() + .url("jdbc:h2:mem:enablerepositoriestestkt;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false") + .username("sa") + .password("") + .driverClassName("org.h2.Driver") + .build() + ResourceDatabasePopulator(ClassPathResource("data.sql")).execute(dataSource) + return dataSource + } + + @Bean + open fun ormTemplate(dataSource: DataSource): ORMTemplate = ORMTemplate.of(dataSource) + } + + @Configuration + @EnableStormRepositories(basePackages = ["st.orm.spring.repository"], ormTemplateBeanName = "ormTemplate") + open class RepositoriesConfiguration + + @Test + fun `annotation registers repositories through the Kotlin adapter`() { + AnnotationConfigApplicationContext( + DatabaseConfiguration::class.java, + RepositoriesConfiguration::class.java, + ).use { context -> + // The registrar must pick the Kotlin adapter: repositories bind through the KClass API. + context.getBean("stormRepositoriesPostProcessor") + .shouldBeInstanceOf() + val visitRepository = context.getBean(VisitRepository::class.java) + visitRepository.count() shouldBe 14 + } + } +} diff --git a/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java b/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java index e5f12f3c0..b66427b56 100644 --- a/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java +++ b/storm-spring-boot-starter/src/test/java/st/orm/spring/boot/autoconfigure/StormAutoConfigurationTest.java @@ -548,6 +548,27 @@ public io.micrometer.observation.tck.TestObservationRegistry observationRegistry } } + @Test + void enableStormRepositoriesBacksOffAutoConfiguredScanning() { + // @EnableStormRepositories doubles as the explicit override in Boot: its registrar-provided + // post-processor makes the auto-configured one back off. + contextRunner + .withPropertyValues( + "spring.datasource.url=jdbc:h2:mem:enableAnnotationTest;DB_CLOSE_DELAY=-1", + "spring.datasource.driver-class-name=org.h2.Driver" + ) + .withUserConfiguration(EnableStormRepositoriesConfig.class) + .run(context -> { + assertThat(context).doesNotHaveBean(AutoConfiguredRepositoryBeanFactoryPostProcessor.class); + assertThat(context).hasBean("stormRepositoriesPostProcessor"); + }); + } + + @Configuration + @st.orm.spring.EnableStormRepositories(basePackages = "com.example.repository") + static class EnableStormRepositoriesConfig { + } + @Configuration static class CustomExceptionMapperConfig { @Bean diff --git a/storm-spring/src/main/java/st/orm/spring/EnableStormRepositories.java b/storm-spring/src/main/java/st/orm/spring/EnableStormRepositories.java new file mode 100644 index 000000000..2375d352d --- /dev/null +++ b/storm-spring/src/main/java/st/orm/spring/EnableStormRepositories.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 - 2026 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 st.orm.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.context.annotation.Import; +import st.orm.spring.impl.StormRepositoriesRegistrar; + +/** + * Enables Storm repository beans, mirroring annotations like {@code @EnableJpaRepositories}. + * + *

Scans the configured base packages for repository interfaces and registers each as a Spring bean, + * available for constructor injection. Without explicit packages, the package of the annotated class is + * scanned. Works with both language stacks: the repositories are bound through the Kotlin API when + * storm-kotlin-spring is on the classpath, and through the Java API otherwise.

+ * + * {@snippet lang = java: + * @Configuration + * @EnableStormRepositories(basePackages = "com.acme.repository") + * public class AcmeConfiguration { + * } + * } + * + *

In Spring Boot, repositories are discovered automatically under the application's base package; this + * annotation doubles as the explicit override there — when present, the starter's auto-configured scanning + * backs off. For multiple repository sets bound to different templates, define one + * {@link RepositoryBeanFactoryPostProcessor} bean per set using its configuring constructor instead.

+ * + * @since 1.13 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(StormRepositoriesRegistrar.class) +public @interface EnableStormRepositories { + + /** + * Base packages to scan for repository interfaces. Empty means the package of the annotated class. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()}: the packages of the given classes are scanned. + */ + Class[] basePackageClasses() default {}; + + /** + * The {@code ORMTemplate} bean the repositories bind to. Empty means the primary template. + */ + String ormTemplateBeanName() default ""; + + /** + * Prefix for the registered repository bean names. Empty for none. + */ + String repositoryPrefix() default ""; +} diff --git a/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java b/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java index 3599904fd..748caa3b9 100644 --- a/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java +++ b/storm-spring/src/main/java/st/orm/spring/RepositoryBeanFactoryPostProcessor.java @@ -16,6 +16,7 @@ package st.orm.spring; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import java.util.Set; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.stereotype.Component; @@ -33,6 +34,49 @@ @Component public class RepositoryBeanFactoryPostProcessor extends AbstractRepositoryBeanFactoryPostProcessor { + private final String[] basePackages; + private final String ormTemplateBeanName; + private final String repositoryPrefix; + + /** + * Creates a post-processor configured through overrides; subclasses override + * {@link #getRepositoryBasePackages()} and friends. + */ + public RepositoryBeanFactoryPostProcessor() { + this(new String[0], null, ""); + } + + /** + * Creates a fully configured post-processor, without subclassing. + * + * @param basePackages the base packages to scan for repository interfaces. + * @param ormTemplateBeanName the ORMTemplate bean repositories bind to, or {@code null} for the primary. + * @param repositoryPrefix prefix for the registered repository bean names; empty for none. + * @since 1.13 + */ + public RepositoryBeanFactoryPostProcessor(@Nonnull String[] basePackages, + @Nullable String ormTemplateBeanName, + @Nonnull String repositoryPrefix) { + this.basePackages = basePackages.clone(); + this.ormTemplateBeanName = ormTemplateBeanName; + this.repositoryPrefix = repositoryPrefix; + } + + @Override + public String[] getRepositoryBasePackages() { + return basePackages.length > 0 ? basePackages.clone() : super.getRepositoryBasePackages(); + } + + @Override + public String getOrmTemplateBeanName() { + return ormTemplateBeanName != null ? ormTemplateBeanName : super.getOrmTemplateBeanName(); + } + + @Override + protected String getRepositoryPrefix() { + return !repositoryPrefix.isEmpty() ? repositoryPrefix : super.getRepositoryPrefix(); + } + @Override protected Class getRepositoryType() { return Repository.class; diff --git a/storm-spring/src/main/java/st/orm/spring/impl/StormRepositoriesRegistrar.java b/storm-spring/src/main/java/st/orm/spring/impl/StormRepositoriesRegistrar.java new file mode 100644 index 000000000..1abc04d58 --- /dev/null +++ b/storm-spring/src/main/java/st/orm/spring/impl/StormRepositoriesRegistrar.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 - 2026 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 st.orm.spring.impl; + +import jakarta.annotation.Nonnull; +import java.util.LinkedHashSet; +import java.util.Set; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.ClassUtils; +import st.orm.spring.AbstractRepositoryBeanFactoryPostProcessor; +import st.orm.spring.EnableStormRepositories; +import st.orm.spring.RepositoryBeanFactoryPostProcessor; + +/** + * Registers the repository scanning post-processor for {@link EnableStormRepositories}. + * + *

The repositories are bound through the language stack present on the classpath: the Kotlin adapter + * when storm-kotlin-spring is available, the Java adapter otherwise (exactly one language stack exists per + * application, as the stacks share class names).

+ * + * @since 1.13 + */ +public class StormRepositoriesRegistrar implements ImportBeanDefinitionRegistrar { + + static final String BEAN_NAME = "stormRepositoriesPostProcessor"; + + private static final String KOTLIN_ADAPTER = "st.orm.spring.kotlin.RepositoryBeanFactoryPostProcessor"; + + @Override + public void registerBeanDefinitions(@Nonnull AnnotationMetadata importingClassMetadata, + @Nonnull BeanDefinitionRegistry registry) { + var attributes = AnnotationAttributes.fromMap( + importingClassMetadata.getAnnotationAttributes(EnableStormRepositories.class.getName())); + if (attributes == null) { + return; + } + Set basePackages = new LinkedHashSet<>(); + for (String basePackage : attributes.getStringArray("basePackages")) { + if (!basePackage.isBlank()) { + basePackages.add(basePackage); + } + } + for (Class basePackageClass : attributes.getClassArray("basePackageClasses")) { + basePackages.add(ClassUtils.getPackageName(basePackageClass)); + } + if (basePackages.isEmpty()) { + basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); + } + String ormTemplateBeanName = attributes.getString("ormTemplateBeanName"); + String repositoryPrefix = attributes.getString("repositoryPrefix"); + var beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(resolvePostProcessorType()) + .addConstructorArgValue(basePackages.toArray(String[]::new)) + .addConstructorArgValue(ormTemplateBeanName.isEmpty() ? null : ormTemplateBeanName) + .addConstructorArgValue(repositoryPrefix) + .getBeanDefinition(); + beanDefinition.setRole(org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(BEAN_NAME, beanDefinition); + } + + @SuppressWarnings("unchecked") + private Class resolvePostProcessorType() { + ClassLoader classLoader = StormRepositoriesRegistrar.class.getClassLoader(); + if (ClassUtils.isPresent(KOTLIN_ADAPTER, classLoader)) { + try { + return (Class) + ClassUtils.forName(KOTLIN_ADAPTER, classLoader); + } catch (ClassNotFoundException ignored) { + // Fall through to the Java adapter. + } + } + return RepositoryBeanFactoryPostProcessor.class; + } +} diff --git a/storm-spring/src/test/java/st/orm/spring/EnableStormRepositoriesTest.java b/storm-spring/src/test/java/st/orm/spring/EnableStormRepositoriesTest.java new file mode 100644 index 000000000..0966aabcb --- /dev/null +++ b/storm-spring/src/test/java/st/orm/spring/EnableStormRepositoriesTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2024 - 2026 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 st.orm.spring; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.sql.DataSource; +import org.junit.jupiter.api.Test; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import st.orm.spring.repository.VisitRepository; +import st.orm.template.ORMTemplate; + +/** + * Verifies that {@link EnableStormRepositories} scans and registers repository beans, mirroring + * annotations like {@code @EnableJpaRepositories}. + */ +class EnableStormRepositoriesTest { + + @Configuration + static class DatabaseConfiguration { + @Bean + public DataSource dataSource() { + DataSource dataSource = DataSourceBuilder.create() + .url("jdbc:h2:mem:enablerepositoriestest;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false") + .username("sa") + .password("") + .driverClassName("org.h2.Driver") + .build(); + new ResourceDatabasePopulator(new ClassPathResource("data.sql")).execute(dataSource); + return dataSource; + } + + @Bean + public ORMTemplate ormTemplate(DataSource dataSource) { + return ORMTemplate.of(dataSource); + } + } + + @Configuration + @EnableStormRepositories(basePackages = "st.orm.spring.repository", ormTemplateBeanName = "ormTemplate") + static class ExplicitPackagesConfiguration { + } + + @Configuration + @EnableStormRepositories(basePackageClasses = VisitRepository.class, ormTemplateBeanName = "ormTemplate") + static class PackageClassesConfiguration { + } + + @Configuration + @EnableStormRepositories( + basePackages = "st.orm.spring.repository", + ormTemplateBeanName = "ormTemplate", + repositoryPrefix = "acme") + static class PrefixedConfiguration { + } + + @Test + void explicitBasePackagesRegisterRepositories() { + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, ExplicitPackagesConfiguration.class)) { + VisitRepository visitRepository = context.getBean(VisitRepository.class); + assertEquals(14, visitRepository.count()); + assertInstanceOf(RepositoryBeanFactoryPostProcessor.class, + context.getBean("stormRepositoriesPostProcessor")); + } + } + + @Test + void basePackageClassesRegisterRepositories() { + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, PackageClassesConfiguration.class)) { + assertTrue(context.containsBean("VisitRepository")); + } + } + + @Test + void repositoryPrefixNamesTheBeans() { + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, PrefixedConfiguration.class)) { + assertTrue(context.containsBean("acmeVisitRepository")); + VisitRepository visitRepository = context.getBean(VisitRepository.class); + assertEquals(14, visitRepository.count()); + } + } + + @Test + void withoutPackagesTheAnnotatedClassPackageIsScanned() { + // The test configuration lives in st.orm.spring, so the scan covers st.orm.spring.repository too. + try (var context = new AnnotationConfigApplicationContext( + DatabaseConfiguration.class, DefaultPackageConfiguration.class)) { + assertTrue(context.containsBean("VisitRepository")); + } + } + + @Configuration + @EnableStormRepositories(ormTemplateBeanName = "ormTemplate") + static class DefaultPackageConfiguration { + } +}