From d2e9291fe448a20c518c415bd36f9ff6a37899eb Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 20:35:00 +0200 Subject: [PATCH 01/11] feat(slack)!: extract spring-services-slack feature module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2 of spec 014. Moves services/slack (main + tests) into a new spring-services-slack module that depends on spring-services-core and owns the slack-api-client dependency (removed from core). Adds SlackAutoConfiguration (@ConditionalOnClass(Slack.class), @Import(SlackConfig)) registered via the module's own AutoConfiguration.imports, so the feature self-activates by classpath presence. SlackConfig is dropped from FullSpringServiceConfig (now one fewer core feature). The auto-config lives in com.openelements.spring.base.slack, outside SlackConfig's @ComponentScan of services.slack — co-locating it there caused a duplicate SlackConfig bean definition. Reactor green: core 388 + slack 12 = 400 tests. Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 1 + spring-services-core/pom.xml | 6 ---- .../spring/base/FullSpringServiceConfig.java | 3 -- spring-services-slack/pom.xml | 36 +++++++++++++++++++ .../base/services/slack/SlackConfig.java | 0 .../base/services/slack/SlackException.java | 0 .../base/services/slack/SlackProperties.java | 0 .../base/services/slack/SlackService.java | 0 .../base/services/slack/package-info.java | 0 .../base/slack/SlackAutoConfiguration.java | 28 +++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../base/services/slack/SlackConfigTest.java | 0 .../base/services/slack/SlackServiceTest.java | 0 13 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 spring-services-slack/pom.xml rename {spring-services-core => spring-services-slack}/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java (100%) rename {spring-services-core => spring-services-slack}/src/main/java/com/openelements/spring/base/services/slack/SlackException.java (100%) rename {spring-services-core => spring-services-slack}/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java (100%) rename {spring-services-core => spring-services-slack}/src/main/java/com/openelements/spring/base/services/slack/SlackService.java (100%) rename {spring-services-core => spring-services-slack}/src/main/java/com/openelements/spring/base/services/slack/package-info.java (100%) create mode 100644 spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java create mode 100644 spring-services-slack/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename {spring-services-core => spring-services-slack}/src/test/java/com/openelements/spring/base/services/slack/SlackConfigTest.java (100%) rename {spring-services-core => spring-services-slack}/src/test/java/com/openelements/spring/base/services/slack/SlackServiceTest.java (100%) diff --git a/pom.xml b/pom.xml index 656c891..9f3978e 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,7 @@ spring-services-core + spring-services-slack diff --git a/spring-services-core/pom.xml b/spring-services-core/pom.xml index dbb881c..f5a135e 100644 --- a/spring-services-core/pom.xml +++ b/spring-services-core/pom.xml @@ -49,12 +49,6 @@ ${jspecify.version} compile - - com.slack.api - slack-api-client - ${slack-api-client.version} - - + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java rename to spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackConfig.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackException.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackException.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackException.java rename to spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackException.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java rename to spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackProperties.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackService.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackService.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/slack/SlackService.java rename to spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/SlackService.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/slack/package-info.java b/spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/package-info.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/slack/package-info.java rename to spring-services-slack/src/main/java/com/openelements/spring/base/services/slack/package-info.java diff --git a/spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java b/spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java new file mode 100644 index 0000000..cac1c98 --- /dev/null +++ b/spring-services-slack/src/main/java/com/openelements/spring/base/slack/SlackAutoConfiguration.java @@ -0,0 +1,28 @@ +package com.openelements.spring.base.slack; + +import com.openelements.spring.base.services.slack.SlackConfig; +import com.slack.api.Slack; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Import; + +/** + * Auto-configuration for the optional Slack messaging feature. + * + *

Registered through this module's + * {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}, so the + * feature self-activates when {@code spring-services-slack} is on the classpath and stays absent + * otherwise. {@link ConditionalOnClass @ConditionalOnClass(Slack.class)} keeps it inert if the + * {@code slack-api-client} dependency is not present, so a bundle that ships this class without the + * SDK causes no {@code NoClassDefFoundError}. + * + *

Lives outside the {@code services.slack} package on purpose: {@link SlackConfig} declares a + * {@code @ComponentScan} over its own package, and an auto-configuration co-located there would be + * swept up by that scan and registered twice. Delegates the actual bean wiring to + * {@link SlackConfig}, which remains a plain {@code @Configuration} consumers can still + * {@code @Import} explicitly. + */ +@AutoConfiguration +@ConditionalOnClass(Slack.class) +@Import(SlackConfig.class) +public class SlackAutoConfiguration {} diff --git a/spring-services-slack/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-services-slack/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..80dee58 --- /dev/null +++ b/spring-services-slack/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.openelements.spring.base.slack.SlackAutoConfiguration diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/slack/SlackConfigTest.java b/spring-services-slack/src/test/java/com/openelements/spring/base/services/slack/SlackConfigTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/slack/SlackConfigTest.java rename to spring-services-slack/src/test/java/com/openelements/spring/base/services/slack/SlackConfigTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/slack/SlackServiceTest.java b/spring-services-slack/src/test/java/com/openelements/spring/base/services/slack/SlackServiceTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/slack/SlackServiceTest.java rename to spring-services-slack/src/test/java/com/openelements/spring/base/services/slack/SlackServiceTest.java From 1b7e076e52559ce2620e89f8371a9669582b9cba Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 20:53:53 +0200 Subject: [PATCH 02/11] feat(search)!: extract spring-services-search feature module Step 3 of spec 014. Moves services/search (main + tests, incl. WireMock-backed client tests) into spring-services-search, depending on core. No new third-party dependency (Meilisearch is reached over Spring's RestClient via core). Adds SearchAutoConfiguration (@Import(SearchConfig)) in com.openelements.spring.base.search (outside SearchConfig's @ComponentScan) registered via the module's AutoConfiguration.imports; runtime opt-in stays governed by SearchConfig's @ConditionalOnProperty(openelements.meilisearch.enabled). Drops SearchConfig from FullSpringServiceConfig and de-links a stale DbBackupConfig javadoc reference. Reactor test-compiles; search module tests green (27). Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 1 + .../spring/base/FullSpringServiceConfig.java | 4 -- .../services/dbbackup/DbBackupConfig.java | 6 +-- spring-services-search/pom.xml | 37 +++++++++++++++++++ .../base/search/SearchAutoConfiguration.java | 23 ++++++++++++ .../base/services/search/BatchWriter.java | 0 .../services/search/BootstrapInvoker.java | 0 .../base/services/search/Highlighter.java | 0 .../base/services/search/IndexSettings.java | 0 .../search/MeilisearchBootstrapRunner.java | 0 .../services/search/MeilisearchClient.java | 0 .../services/search/MeilisearchException.java | 0 .../MeilisearchIndexSettingsInitializer.java | 0 .../search/MeilisearchProperties.java | 0 .../MeilisearchScopedKeyInitializer.java | 0 .../base/services/search/ScopedKeySpec.java | 0 .../base/services/search/SearchConfig.java | 0 .../search/SearchIndexBootstrapStep.java | 0 .../services/search/SearchReadinessState.java | 0 .../base/services/search/TaskOutcome.java | 0 .../base/services/search/package-info.java | 0 ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../base/services/search/HighlighterTest.java | 0 .../MeilisearchBootstrapRunnerTest.java | 0 .../search/MeilisearchClientTest.java | 0 ...ilisearchIndexSettingsInitializerTest.java | 0 .../MeilisearchPropertiesBindingTest.java | 0 .../MeilisearchScopedKeyInitializerTest.java | 0 28 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 spring-services-search/pom.xml create mode 100644 spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/BatchWriter.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/Highlighter.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/IndexSettings.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/ScopedKeySpec.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java (100%) rename {spring-services-core => spring-services-search}/src/main/java/com/openelements/spring/base/services/search/package-info.java (100%) create mode 100644 spring-services-search/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename {spring-services-core => spring-services-search}/src/test/java/com/openelements/spring/base/services/search/HighlighterTest.java (100%) rename {spring-services-core => spring-services-search}/src/test/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunnerTest.java (100%) rename {spring-services-core => spring-services-search}/src/test/java/com/openelements/spring/base/services/search/MeilisearchClientTest.java (100%) rename {spring-services-core => spring-services-search}/src/test/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializerTest.java (100%) rename {spring-services-core => spring-services-search}/src/test/java/com/openelements/spring/base/services/search/MeilisearchPropertiesBindingTest.java (100%) rename {spring-services-core => spring-services-search}/src/test/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializerTest.java (100%) diff --git a/pom.xml b/pom.xml index 9f3978e..a00d621 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ spring-services-core spring-services-slack + spring-services-search diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java index 754cffc..8368763 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java @@ -6,7 +6,6 @@ import com.openelements.spring.base.services.comment.CommentConfig; import com.openelements.spring.base.services.dbbackup.DbBackupConfig; import com.openelements.spring.base.services.email.EmailConfig; -import com.openelements.spring.base.services.search.SearchConfig; import com.openelements.spring.base.services.settings.SettingsConfig; import com.openelements.spring.base.services.tag.TagConfig; import com.openelements.spring.base.services.translation.TranslationConfig; @@ -40,8 +39,6 @@ *

  • {@link WebhookConfig} — outbound webhook subscriptions and dispatching. *
  • {@link EmailConfig} — Email sending service for delivering plain-text emails via SMTP. *
  • {@link AuditConfig} — Audit log of every data lifecycle event with user attribution. - *
  • {@link SearchConfig} — Meilisearch-backed full-text search: startup reindex, scoped keys, - * and per-index settings. *
  • {@link DbBackupConfig} — Client for the db-backup-service sidecar: trigger backups, poll * job status, list and stream backup artefacts. * @@ -57,7 +54,6 @@ AuditConfig.class, CommentConfig.class, TranslationConfig.class, - SearchConfig.class, DbBackupConfig.class }) public class FullSpringServiceConfig { diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java index a64d5e9..6239702 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java @@ -13,9 +13,9 @@ * part of the full platform setup. * *

    This is a plain {@code @Configuration} (not {@code @AutoConfiguration}) for the same reason as - * {@link com.openelements.spring.base.services.search.SearchConfig}: a host application's - * {@code @ComponentScan} reliably picks up plain configurations but excludes auto-configurations, - * so the properties are always registered alongside the client. + * the {@code SearchConfig} of the search module: a host application's {@code @ComponentScan} + * reliably picks up plain configurations but excludes auto-configurations, so the properties are + * always registered alongside the client. * *

    The feature is opt-in and disabled by default: none of these beans are created * unless {@code openelements.db-backup.enabled=true}. This avoids forcing a db-backup configuration diff --git a/spring-services-search/pom.xml b/spring-services-search/pom.xml new file mode 100644 index 0000000..8a6bfb5 --- /dev/null +++ b/spring-services-search/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + + com.open-elements + spring-services + 1.3.0-SNAPSHOT + + + spring-services-search + + Spring Services Search + Optional Meilisearch-backed full-text search feature module for spring-services + + + + com.open-elements + spring-services-core + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.wiremock + wiremock-standalone + ${wiremock.version} + test + + + + diff --git a/spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java b/spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java new file mode 100644 index 0000000..92b98a7 --- /dev/null +++ b/spring-services-search/src/main/java/com/openelements/spring/base/search/SearchAutoConfiguration.java @@ -0,0 +1,23 @@ +package com.openelements.spring.base.search; + +import com.openelements.spring.base.services.search.SearchConfig; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Import; + +/** + * Auto-configuration for the optional Meilisearch-backed search feature. + * + *

    Registered through this module's + * {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}, so the + * feature self-activates when {@code spring-services-search} is on the classpath. The module carries + * no extra third-party dependency (it talks to Meilisearch over Spring's {@code RestClient}), so + * there is no heavy-dependency class to guard on; runtime opt-in is governed by + * {@link SearchConfig}'s existing {@code @ConditionalOnProperty(openelements.meilisearch.enabled)}. + * + *

    Lives outside the {@code services.search} package on purpose: {@link SearchConfig} declares a + * {@code @ComponentScan} over its own package, and an auto-configuration co-located there would be + * swept up by that scan and registered twice. + */ +@AutoConfiguration +@Import(SearchConfig.class) +public class SearchAutoConfiguration {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/BatchWriter.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/BatchWriter.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/BatchWriter.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/BatchWriter.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/BootstrapInvoker.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/Highlighter.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/Highlighter.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/Highlighter.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/Highlighter.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/IndexSettings.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/IndexSettings.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/IndexSettings.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/IndexSettings.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunner.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchClient.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchException.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializer.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializer.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/ScopedKeySpec.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/ScopedKeySpec.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/ScopedKeySpec.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/ScopedKeySpec.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchConfig.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchIndexBootstrapStep.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/SearchReadinessState.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/TaskOutcome.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/search/package-info.java b/spring-services-search/src/main/java/com/openelements/spring/base/services/search/package-info.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/search/package-info.java rename to spring-services-search/src/main/java/com/openelements/spring/base/services/search/package-info.java diff --git a/spring-services-search/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-services-search/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..d16f8df --- /dev/null +++ b/spring-services-search/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.openelements.spring.base.search.SearchAutoConfiguration diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/search/HighlighterTest.java b/spring-services-search/src/test/java/com/openelements/spring/base/services/search/HighlighterTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/search/HighlighterTest.java rename to spring-services-search/src/test/java/com/openelements/spring/base/services/search/HighlighterTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunnerTest.java b/spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunnerTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunnerTest.java rename to spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchBootstrapRunnerTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchClientTest.java b/spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchClientTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchClientTest.java rename to spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchClientTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializerTest.java b/spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializerTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializerTest.java rename to spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchIndexSettingsInitializerTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchPropertiesBindingTest.java b/spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchPropertiesBindingTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchPropertiesBindingTest.java rename to spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchPropertiesBindingTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializerTest.java b/spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializerTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializerTest.java rename to spring-services-search/src/test/java/com/openelements/spring/base/services/search/MeilisearchScopedKeyInitializerTest.java From 0172d6c3442a618deead998eee48c8f28a309fae Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 20:55:48 +0200 Subject: [PATCH 03/11] feat(dbbackup)!: extract spring-services-dbbackup feature module Step 4 of spec 014. Moves services/dbbackup (main + WireMock-backed client test) into spring-services-dbbackup, depending on core. No new third-party dependency (RestClient via core). Adds DbBackupAutoConfiguration (@Import(DbBackupConfig)) in com.openelements.spring.base.dbbackup (outside DbBackupConfig's @ComponentScan) via the module's AutoConfiguration.imports; runtime opt-in stays governed by @ConditionalOnProperty(openelements.db-backup.enabled). Drops DbBackupConfig from FullSpringServiceConfig. Reactor installs; dbbackup module tests green (13). Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 1 + .../spring/base/FullSpringServiceConfig.java | 6 +-- spring-services-dbbackup/pom.xml | 37 +++++++++++++++++++ .../dbbackup/DbBackupAutoConfiguration.java | 23 ++++++++++++ .../services/dbbackup/BackupDownload.java | 0 .../base/services/dbbackup/BackupJob.java | 0 .../services/dbbackup/BackupJobStatus.java | 0 .../services/dbbackup/BackupMetadata.java | 0 .../services/dbbackup/BackupServiceInfo.java | 0 .../base/services/dbbackup/BackupTrigger.java | 0 .../dbbackup/BackupTriggerResult.java | 0 .../services/dbbackup/DbBackupClient.java | 0 .../services/dbbackup/DbBackupConfig.java | 0 .../services/dbbackup/DbBackupException.java | 0 .../services/dbbackup/DbBackupProperties.java | 0 .../base/services/dbbackup/package-info.java | 0 ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../services/dbbackup/DbBackupClientTest.java | 0 18 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 spring-services-dbbackup/pom.xml create mode 100644 spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java (100%) rename {spring-services-core => spring-services-dbbackup}/src/main/java/com/openelements/spring/base/services/dbbackup/package-info.java (100%) create mode 100644 spring-services-dbbackup/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename {spring-services-core => spring-services-dbbackup}/src/test/java/com/openelements/spring/base/services/dbbackup/DbBackupClientTest.java (100%) diff --git a/pom.xml b/pom.xml index a00d621..be02160 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ spring-services-core spring-services-slack spring-services-search + spring-services-dbbackup diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java index 8368763..59367aa 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java @@ -4,7 +4,6 @@ import com.openelements.spring.base.services.apikey.ApiKeyConfig; import com.openelements.spring.base.services.audit.AuditConfig; import com.openelements.spring.base.services.comment.CommentConfig; -import com.openelements.spring.base.services.dbbackup.DbBackupConfig; import com.openelements.spring.base.services.email.EmailConfig; import com.openelements.spring.base.services.settings.SettingsConfig; import com.openelements.spring.base.services.tag.TagConfig; @@ -39,8 +38,6 @@ *

  • {@link WebhookConfig} — outbound webhook subscriptions and dispatching. *
  • {@link EmailConfig} — Email sending service for delivering plain-text emails via SMTP. *
  • {@link AuditConfig} — Audit log of every data lifecycle event with user attribution. - *
  • {@link DbBackupConfig} — Client for the db-backup-service sidecar: trigger backups, poll - * job status, list and stream backup artefacts. * */ @Import({ @@ -53,8 +50,7 @@ EmailConfig.class, AuditConfig.class, CommentConfig.class, - TranslationConfig.class, - DbBackupConfig.class + TranslationConfig.class }) public class FullSpringServiceConfig { } diff --git a/spring-services-dbbackup/pom.xml b/spring-services-dbbackup/pom.xml new file mode 100644 index 0000000..bf6771c --- /dev/null +++ b/spring-services-dbbackup/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + + com.open-elements + spring-services + 1.3.0-SNAPSHOT + + + spring-services-dbbackup + + Spring Services DB-Backup + Optional db-backup-service sidecar client feature module for spring-services + + + + com.open-elements + spring-services-core + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.wiremock + wiremock-standalone + ${wiremock.version} + test + + + + diff --git a/spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java new file mode 100644 index 0000000..ee8930c --- /dev/null +++ b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/dbbackup/DbBackupAutoConfiguration.java @@ -0,0 +1,23 @@ +package com.openelements.spring.base.dbbackup; + +import com.openelements.spring.base.services.dbbackup.DbBackupConfig; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Import; + +/** + * Auto-configuration for the optional db-backup-service sidecar client feature. + * + *

    Registered through this module's + * {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}, so the + * feature self-activates when {@code spring-services-dbbackup} is on the classpath. The module + * carries no extra third-party dependency (it talks to the sidecar over Spring's {@code RestClient} + * via core), so there is no heavy-dependency class to guard on; runtime opt-in stays governed by + * {@link DbBackupConfig}'s existing {@code @ConditionalOnProperty(openelements.db-backup.enabled)}. + * + *

    Lives outside the {@code services.dbbackup} package on purpose: {@link DbBackupConfig} declares + * a {@code @ComponentScan} over its own package, and an auto-configuration co-located there would be + * swept up by that scan and registered twice. + */ +@AutoConfiguration +@Import(DbBackupConfig.class) +public class DbBackupAutoConfiguration {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupDownload.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJob.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupJobStatus.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupMetadata.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupServiceInfo.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTrigger.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/BackupTriggerResult.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupClient.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupException.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/package-info.java b/spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/package-info.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/dbbackup/package-info.java rename to spring-services-dbbackup/src/main/java/com/openelements/spring/base/services/dbbackup/package-info.java diff --git a/spring-services-dbbackup/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-services-dbbackup/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..6db5f17 --- /dev/null +++ b/spring-services-dbbackup/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.openelements.spring.base.dbbackup.DbBackupAutoConfiguration diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/dbbackup/DbBackupClientTest.java b/spring-services-dbbackup/src/test/java/com/openelements/spring/base/services/dbbackup/DbBackupClientTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/dbbackup/DbBackupClientTest.java rename to spring-services-dbbackup/src/test/java/com/openelements/spring/base/services/dbbackup/DbBackupClientTest.java From 299c00f888765a5f133b0c4651d224d6d988faa3 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 20:58:44 +0200 Subject: [PATCH 04/11] feat(email)!: extract spring-services-email feature module Step 5 of spec 014. Moves services/email (main + tests) into spring-services-email, depending on core (uses core's UserEntity) and owning spring-boot-starter-mail (removed from core). Adds EmailAutoConfiguration (@ConditionalOnClass(MimeMessage.class), @Import(EmailConfig)) in com.openelements.spring.base.email (outside EmailConfig's @ComponentScan) via the module's AutoConfiguration.imports. Graceful degradation without a JavaMailSender is preserved. Drops EmailConfig from FullSpringServiceConfig and de-links a cross-module javadoc reference in EmailServiceTest. Reactor installs; email module tests green (17). Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 1 + spring-services-core/pom.xml | 4 --- .../spring/base/FullSpringServiceConfig.java | 3 -- spring-services-email/pom.xml | 35 +++++++++++++++++++ .../base/email/EmailAutoConfiguration.java | 27 ++++++++++++++ .../base/services/email/EmailConfig.java | 0 .../base/services/email/EmailException.java | 0 .../base/services/email/EmailProperties.java | 0 .../base/services/email/EmailService.java | 0 .../base/services/email/package-info.java | 0 ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../base/services/email/EmailConfigTest.java | 0 .../base/services/email/EmailServiceTest.java | 7 ++-- 13 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 spring-services-email/pom.xml create mode 100644 spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java rename {spring-services-core => spring-services-email}/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java (100%) rename {spring-services-core => spring-services-email}/src/main/java/com/openelements/spring/base/services/email/EmailException.java (100%) rename {spring-services-core => spring-services-email}/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java (100%) rename {spring-services-core => spring-services-email}/src/main/java/com/openelements/spring/base/services/email/EmailService.java (100%) rename {spring-services-core => spring-services-email}/src/main/java/com/openelements/spring/base/services/email/package-info.java (100%) create mode 100644 spring-services-email/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename {spring-services-core => spring-services-email}/src/test/java/com/openelements/spring/base/services/email/EmailConfigTest.java (100%) rename {spring-services-core => spring-services-email}/src/test/java/com/openelements/spring/base/services/email/EmailServiceTest.java (97%) diff --git a/pom.xml b/pom.xml index be02160..fc353e4 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ spring-services-slack spring-services-search spring-services-dbbackup + spring-services-email diff --git a/spring-services-core/pom.xml b/spring-services-core/pom.xml index f5a135e..d41dadd 100644 --- a/spring-services-core/pom.xml +++ b/spring-services-core/pom.xml @@ -34,10 +34,6 @@ org.springframework.boot spring-boot-starter-oauth2-resource-server - - org.springframework.boot - spring-boot-starter-mail - io.swagger.core.v3 swagger-annotations-jakarta diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java index 59367aa..5a7c3ce 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/FullSpringServiceConfig.java @@ -4,7 +4,6 @@ import com.openelements.spring.base.services.apikey.ApiKeyConfig; import com.openelements.spring.base.services.audit.AuditConfig; import com.openelements.spring.base.services.comment.CommentConfig; -import com.openelements.spring.base.services.email.EmailConfig; import com.openelements.spring.base.services.settings.SettingsConfig; import com.openelements.spring.base.services.tag.TagConfig; import com.openelements.spring.base.services.translation.TranslationConfig; @@ -36,7 +35,6 @@ *

  • {@link SettingsConfig} — generic key/value settings store. *
  • {@link TagConfig} — taggable-entity support. *
  • {@link WebhookConfig} — outbound webhook subscriptions and dispatching. - *
  • {@link EmailConfig} — Email sending service for delivering plain-text emails via SMTP. *
  • {@link AuditConfig} — Audit log of every data lifecycle event with user attribution. * */ @@ -47,7 +45,6 @@ SettingsConfig.class, TagConfig.class, WebhookConfig.class, - EmailConfig.class, AuditConfig.class, CommentConfig.class, TranslationConfig.class diff --git a/spring-services-email/pom.xml b/spring-services-email/pom.xml new file mode 100644 index 0000000..6760b3f --- /dev/null +++ b/spring-services-email/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + + com.open-elements + spring-services + 1.3.0-SNAPSHOT + + + spring-services-email + + Spring Services Email + Optional SMTP email sending feature module for spring-services + + + + com.open-elements + spring-services-core + ${project.version} + + + org.springframework.boot + spring-boot-starter-mail + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java b/spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java new file mode 100644 index 0000000..6140c71 --- /dev/null +++ b/spring-services-email/src/main/java/com/openelements/spring/base/email/EmailAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.openelements.spring.base.email; + +import com.openelements.spring.base.services.email.EmailConfig; +import jakarta.mail.internet.MimeMessage; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Import; + +/** + * Auto-configuration for the optional SMTP email feature. + * + *

    Registered through this module's + * {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}, so the + * feature self-activates when {@code spring-services-email} is on the classpath. + * {@link ConditionalOnClass @ConditionalOnClass(MimeMessage.class)} keeps it inert if the + * {@code spring-boot-starter-mail} dependency is not present, so a bundle that ships this class + * without the mail library causes no {@code NoClassDefFoundError}. + * + *

    Lives outside the {@code services.email} package on purpose: {@link EmailConfig} declares a + * {@code @ComponentScan} over its own package, and an auto-configuration co-located there would be + * swept up by that scan and registered twice. {@code EmailService} still degrades gracefully when no + * {@code JavaMailSender} is configured (no {@code spring.mail.host}). + */ +@AutoConfiguration +@ConditionalOnClass(MimeMessage.class) +@Import(EmailConfig.class) +public class EmailAutoConfiguration {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java rename to spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailConfig.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailException.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailException.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailException.java rename to spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailException.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java rename to spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailProperties.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailService.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailService.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/email/EmailService.java rename to spring-services-email/src/main/java/com/openelements/spring/base/services/email/EmailService.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/services/email/package-info.java b/spring-services-email/src/main/java/com/openelements/spring/base/services/email/package-info.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/services/email/package-info.java rename to spring-services-email/src/main/java/com/openelements/spring/base/services/email/package-info.java diff --git a/spring-services-email/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-services-email/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..c3b99d8 --- /dev/null +++ b/spring-services-email/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.openelements.spring.base.email.EmailAutoConfiguration diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/email/EmailConfigTest.java b/spring-services-email/src/test/java/com/openelements/spring/base/services/email/EmailConfigTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/email/EmailConfigTest.java rename to spring-services-email/src/test/java/com/openelements/spring/base/services/email/EmailConfigTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/services/email/EmailServiceTest.java b/spring-services-email/src/test/java/com/openelements/spring/base/services/email/EmailServiceTest.java similarity index 97% rename from spring-services-core/src/test/java/com/openelements/spring/base/services/email/EmailServiceTest.java rename to spring-services-email/src/test/java/com/openelements/spring/base/services/email/EmailServiceTest.java index 680b21a..f7e6edc 100644 --- a/spring-services-core/src/test/java/com/openelements/spring/base/services/email/EmailServiceTest.java +++ b/spring-services-email/src/test/java/com/openelements/spring/base/services/email/EmailServiceTest.java @@ -133,10 +133,9 @@ void plainAddressWhenNoDisplayName() { } /** - * Symmetric to the WARN-on-blank-token check in {@link - * com.openelements.spring.base.services.slack.SlackConfigTest}: an environment that exports - * {@code OPEN_ELEMENTS_EMAIL_FROM_NAME=" "} must not emit an RFC header containing only - * whitespace as the display name. + * Symmetric to the WARN-on-blank-token check in {@code SlackConfigTest} (spring-services-slack): + * an environment that exports {@code OPEN_ELEMENTS_EMAIL_FROM_NAME=" "} must not emit an RFC + * header containing only whitespace as the display name. */ @Test @DisplayName("A whitespace-only from-name is treated as missing — the From header is the bare address.") From 38a6db03b52e7f12a4e9ad89dcf8f5382dddb116 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 21:02:31 +0200 Subject: [PATCH 05/11] feat(mcp)!: extract spring-services-mcp feature module Step 6 of spec 014. Moves the com.openelements.spring.base.mcp package (main + tests) into spring-services-mcp, depending on core (uses core's ApiKeyDataService) and owning the MCP SDK dependencies mcp-spring-webmvc + mcp-json-jackson2 (removed from core). Adds McpAutoConfiguration (@ConditionalOnClass(McpServer.class), @Import(McpConfiguration)) via the module's AutoConfiguration.imports, so the MCP endpoint and its dedicated security chain are only present when the module is on the classpath (a blast-radius improvement) and stay gated by @ConditionalOnProperty(openelements.mcp.enabled). McpConfiguration uses @Import (not @ComponentScan), so the auto-config co-locates safely. Reactor installs; mcp module tests green (37). Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 1 + spring-services-core/pom.xml | 19 ------- spring-services-mcp/pom.xml | 50 +++++++++++++++++++ .../spring/base/mcp/McpActorLabel.java | 0 .../spring/base/mcp/McpAutoConfiguration.java | 27 ++++++++++ .../spring/base/mcp/McpConfiguration.java | 0 .../openelements/spring/base/mcp/McpPage.java | 0 .../spring/base/mcp/McpPaging.java | 0 .../spring/base/mcp/McpProperties.java | 0 .../spring/base/mcp/McpSecurityConfig.java | 0 .../spring/base/mcp/McpServerConfig.java | 0 .../spring/base/mcp/McpToolLogic.java | 0 .../spring/base/mcp/McpToolProvider.java | 0 .../spring/base/mcp/McpToolSupport.java | 0 .../spring/base/mcp/McpTools.java | 0 .../base/mcp/McpUnavailableException.java | 0 ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../spring/base/mcp/McpActorLabelTest.java | 0 .../spring/base/mcp/McpPagingTest.java | 0 .../spring/base/mcp/McpPropertiesTest.java | 0 .../spring/base/mcp/McpToolSupportTest.java | 0 .../spring/base/mcp/McpToolsTest.java | 0 22 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 spring-services-mcp/pom.xml rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpActorLabel.java (100%) create mode 100644 spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpPage.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpPaging.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpProperties.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpToolLogic.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpTools.java (100%) rename {spring-services-core => spring-services-mcp}/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java (100%) create mode 100644 spring-services-mcp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename {spring-services-core => spring-services-mcp}/src/test/java/com/openelements/spring/base/mcp/McpActorLabelTest.java (100%) rename {spring-services-core => spring-services-mcp}/src/test/java/com/openelements/spring/base/mcp/McpPagingTest.java (100%) rename {spring-services-core => spring-services-mcp}/src/test/java/com/openelements/spring/base/mcp/McpPropertiesTest.java (100%) rename {spring-services-core => spring-services-mcp}/src/test/java/com/openelements/spring/base/mcp/McpToolSupportTest.java (100%) rename {spring-services-core => spring-services-mcp}/src/test/java/com/openelements/spring/base/mcp/McpToolsTest.java (100%) diff --git a/pom.xml b/pom.xml index fc353e4..a782e20 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ spring-services-search spring-services-dbbackup spring-services-email + spring-services-mcp diff --git a/spring-services-core/pom.xml b/spring-services-core/pom.xml index d41dadd..98172d8 100644 --- a/spring-services-core/pom.xml +++ b/spring-services-core/pom.xml @@ -45,25 +45,6 @@ ${jspecify.version} compile - - - io.modelcontextprotocol.sdk - mcp-spring-webmvc - ${mcp-sdk.version} - - - - - io.modelcontextprotocol.sdk - mcp-json-jackson2 - ${mcp-sdk.version} - - org.springframework.boot diff --git a/spring-services-mcp/pom.xml b/spring-services-mcp/pom.xml new file mode 100644 index 0000000..25ffd06 --- /dev/null +++ b/spring-services-mcp/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + + com.open-elements + spring-services + 1.3.0-SNAPSHOT + + + spring-services-mcp + + Spring Services MCP + Optional Model Context Protocol (MCP) server feature module for spring-services + + + + com.open-elements + spring-services-core + ${project.version} + + + + + io.modelcontextprotocol.sdk + mcp-spring-webmvc + ${mcp-sdk.version} + + + + + io.modelcontextprotocol.sdk + mcp-json-jackson2 + ${mcp-sdk.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpActorLabel.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpActorLabel.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpActorLabel.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpActorLabel.java diff --git a/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java new file mode 100644 index 0000000..f825e4f --- /dev/null +++ b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.openelements.spring.base.mcp; + +import io.modelcontextprotocol.server.McpServer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Import; + +/** + * Auto-configuration for the optional Model Context Protocol (MCP) server feature. + * + *

    Registered through this module's + * {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports}, so the + * feature self-activates when {@code spring-services-mcp} is on the classpath — the endpoint and its + * dedicated security filter chain can no longer be accidentally present in an application that never + * added the module. {@link ConditionalOnClass @ConditionalOnClass(McpServer.class)} keeps it inert + * if the MCP SDK is absent, so a bundle shipping this class without the SDK causes no + * {@code NoClassDefFoundError}. + * + *

    Imports {@link McpConfiguration}, the feature entry point, which binds {@link McpProperties} + * unconditionally and wires the server/security beans only when {@code openelements.mcp.enabled=true} + * (unchanged {@code @ConditionalOnProperty} behaviour). {@code @Import(McpConfiguration.class)} + * therefore remains valid for consumers who prefer explicit wiring. + */ +@AutoConfiguration +@ConditionalOnClass(McpServer.class) +@Import(McpConfiguration.class) +public class McpAutoConfiguration {} diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpConfiguration.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpPage.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPage.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpPage.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPage.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpPaging.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPaging.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpPaging.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpPaging.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpProperties.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpProperties.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpProperties.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpProperties.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpSecurityConfig.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpServerConfig.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpToolLogic.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolLogic.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpToolLogic.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolLogic.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolProvider.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpToolSupport.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpTools.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpTools.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpTools.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpTools.java diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java b/spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java similarity index 100% rename from spring-services-core/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java rename to spring-services-mcp/src/main/java/com/openelements/spring/base/mcp/McpUnavailableException.java diff --git a/spring-services-mcp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-services-mcp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3fc34fe --- /dev/null +++ b/spring-services-mcp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.openelements.spring.base.mcp.McpAutoConfiguration diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpActorLabelTest.java b/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpActorLabelTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpActorLabelTest.java rename to spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpActorLabelTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpPagingTest.java b/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpPagingTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpPagingTest.java rename to spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpPagingTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpPropertiesTest.java b/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpPropertiesTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpPropertiesTest.java rename to spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpPropertiesTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpToolSupportTest.java b/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpToolSupportTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpToolSupportTest.java rename to spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpToolSupportTest.java diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpToolsTest.java b/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpToolsTest.java similarity index 100% rename from spring-services-core/src/test/java/com/openelements/spring/base/mcp/McpToolsTest.java rename to spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpToolsTest.java From a4e3eca425a9dc95d337b1fdac1a358eaf1c268f Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 21:18:20 +0200 Subject: [PATCH 06/11] refactor(core)!: rename core auto-config to SpringServicesCoreAutoConfiguration Step 7 of spec 014. Renames SpringServicesAutoConfiguration -> SpringServicesCoreAutoConfiguration to reflect that it configures only the always-present core features, distinct from the per-module feature auto-configs. FullSpringServiceConfig is now core-only (security, tenant, apikey, settings, tag, webhook, audit, comment, translation). Updates the core AutoConfiguration.imports, the activation-guard test, and the test apps/exclusions that referenced the old name. This is a breaking rename of the class shipped in 1.3.0 (documented in the upgrade guide). Full reactor suite green: core 294 + slack 12 + search 27 + dbbackup 13 + email 17 + mcp 37 = 400 tests. Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- ...n.java => SpringServicesCoreAutoConfiguration.java} | 10 +++++++--- ...mework.boot.autoconfigure.AutoConfiguration.imports | 2 +- .../src/test/java/com/example/app/StarterTestApp.java | 2 +- .../com/example/explicitapp/ExplicitImportTestApp.java | 6 +++--- ...va => SpringServicesCoreAutoConfigurationTest.java} | 10 +++++----- .../java/com/openelements/spring/base/data/Tests.java | 4 ++-- 6 files changed, 19 insertions(+), 15 deletions(-) rename spring-services-core/src/main/java/com/openelements/spring/base/{SpringServicesAutoConfiguration.java => SpringServicesCoreAutoConfiguration.java} (87%) rename spring-services-core/src/test/java/com/openelements/spring/base/{SpringServicesAutoConfigurationTest.java => SpringServicesCoreAutoConfigurationTest.java} (83%) diff --git a/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesAutoConfiguration.java b/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesCoreAutoConfiguration.java similarity index 87% rename from spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesAutoConfiguration.java rename to spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesCoreAutoConfiguration.java index a017c88..4717cf7 100644 --- a/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesAutoConfiguration.java +++ b/spring-services-core/src/main/java/com/openelements/spring/base/SpringServicesCoreAutoConfiguration.java @@ -13,7 +13,11 @@ import org.springframework.core.type.AnnotationMetadata; /** - * Auto-configuration that turns spring-services into a real Spring Boot starter. + * Core auto-configuration that turns {@code spring-services-core} into a real Spring Boot starter. + * + *

    Each optional feature module ({@code -slack}, {@code -search}, {@code -dbbackup}, {@code -email}, + * {@code -mcp}) ships its own {@code @AutoConfiguration} guarded by {@code @ConditionalOnClass}; this + * class configures only the always-present core features. * *

    Registered through {@code META-INF/spring/ * org.springframework.boot.autoconfigure.AutoConfiguration.imports}, so a consuming application only @@ -43,8 +47,8 @@ @AutoConfiguration @ConditionalOnClass(EntityManagerFactory.class) @AutoConfigureBefore({HibernateJpaAutoConfiguration.class, JpaRepositoriesAutoConfiguration.class}) -@Import({FullSpringServiceConfig.class, SpringServicesAutoConfiguration.PackageRegistrar.class}) -public class SpringServicesAutoConfiguration { +@Import({FullSpringServiceConfig.class, SpringServicesCoreAutoConfiguration.PackageRegistrar.class}) +public class SpringServicesCoreAutoConfiguration { /** Root package of the library — scanned for both entities and Spring Data repositories. */ static final String LIBRARY_ROOT_PACKAGE = "com.openelements.spring.base"; diff --git a/spring-services-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-services-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6186b5b..4cb3c76 100644 --- a/spring-services-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-services-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1 @@ -com.openelements.spring.base.SpringServicesAutoConfiguration +com.openelements.spring.base.SpringServicesCoreAutoConfiguration diff --git a/spring-services-core/src/test/java/com/example/app/StarterTestApp.java b/spring-services-core/src/test/java/com/example/app/StarterTestApp.java index 87adb9b..741475e 100644 --- a/spring-services-core/src/test/java/com/example/app/StarterTestApp.java +++ b/spring-services-core/src/test/java/com/example/app/StarterTestApp.java @@ -9,7 +9,7 @@ * {@code @EntityScan}, and no {@code @EnableJpaRepositories}, and it does not point * {@code scanBasePackages} at the library. The default {@code @SpringBootApplication} behaviour * registers only this package ({@code com.example.app}) via {@code @AutoConfigurationPackage}. The - * library's {@code SpringServicesAutoConfiguration} must additively register + * library's {@code SpringServicesCoreAutoConfiguration} must additively register * {@code com.openelements.spring.base} so that both the application's and the library's entities and * repositories are discovered — which is exactly what {@link StarterAutoConfigurationIntegrationTest} * asserts. diff --git a/spring-services-core/src/test/java/com/example/explicitapp/ExplicitImportTestApp.java b/spring-services-core/src/test/java/com/example/explicitapp/ExplicitImportTestApp.java index 055a00b..51ff757 100644 --- a/spring-services-core/src/test/java/com/example/explicitapp/ExplicitImportTestApp.java +++ b/spring-services-core/src/test/java/com/example/explicitapp/ExplicitImportTestApp.java @@ -1,7 +1,7 @@ package com.example.explicitapp; import com.openelements.spring.base.FullSpringServiceConfig; -import com.openelements.spring.base.SpringServicesAutoConfiguration; +import com.openelements.spring.base.SpringServicesCoreAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; @@ -10,7 +10,7 @@ /** * A consumer application that wires the library the old, explicit way: the Spring Boot - * starter ({@link SpringServicesAutoConfiguration}) is excluded, and the library is brought in via + * starter ({@link SpringServicesCoreAutoConfiguration}) is excluded, and the library is brought in via * {@code @Import(FullSpringServiceConfig.class)} plus explicit {@code @EntityScan} / * {@code @EnableJpaRepositories} over the library root. This proves the pre-starter integration * contract still works and produces no duplicate-bean errors. @@ -18,7 +18,7 @@ @SpringBootApplication( exclude = { OAuth2ResourceServerAutoConfiguration.class, - SpringServicesAutoConfiguration.class + SpringServicesCoreAutoConfiguration.class }) @Import(FullSpringServiceConfig.class) @EntityScan("com.openelements.spring.base") diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/SpringServicesAutoConfigurationTest.java b/spring-services-core/src/test/java/com/openelements/spring/base/SpringServicesCoreAutoConfigurationTest.java similarity index 83% rename from spring-services-core/src/test/java/com/openelements/spring/base/SpringServicesAutoConfigurationTest.java rename to spring-services-core/src/test/java/com/openelements/spring/base/SpringServicesCoreAutoConfigurationTest.java index 5859ee5..a0ec620 100644 --- a/spring-services-core/src/test/java/com/openelements/spring/base/SpringServicesAutoConfigurationTest.java +++ b/spring-services-core/src/test/java/com/openelements/spring/base/SpringServicesCoreAutoConfigurationTest.java @@ -10,7 +10,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; /** - * Activation-guard test for {@link SpringServicesAutoConfiguration}. + * Activation-guard test for {@link SpringServicesCoreAutoConfiguration}. * *

    Verifies the {@code @ConditionalOnClass(EntityManagerFactory.class)} guard: in an application * without JPA on the classpath the starter must stay inert rather than fail trying to configure @@ -20,12 +20,12 @@ *

    The positive case (the auto-configuration activating and wiring the full platform when JPA is * present) is covered end-to-end by {@code com.example.app.StarterAutoConfigurationIntegrationTest}. */ -@DisplayName("SpringServicesAutoConfiguration — activation guard") -class SpringServicesAutoConfigurationTest { +@DisplayName("SpringServicesCoreAutoConfiguration — activation guard") +class SpringServicesCoreAutoConfigurationTest { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SpringServicesAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(SpringServicesCoreAutoConfiguration.class)); @Test @DisplayName("Backs off (does not activate) when EntityManagerFactory is absent from the classpath") @@ -36,6 +36,6 @@ void backsOffWithoutJpa() { context -> assertThat(context) .hasNotFailed() - .doesNotHaveBean(SpringServicesAutoConfiguration.class)); + .doesNotHaveBean(SpringServicesCoreAutoConfiguration.class)); } } diff --git a/spring-services-core/src/test/java/com/openelements/spring/base/data/Tests.java b/spring-services-core/src/test/java/com/openelements/spring/base/data/Tests.java index 38db89c..39fbc7a 100644 --- a/spring-services-core/src/test/java/com/openelements/spring/base/data/Tests.java +++ b/spring-services-core/src/test/java/com/openelements/spring/base/data/Tests.java @@ -13,7 +13,7 @@ import org.springframework.data.domain.Pageable; // This is a focused slice test of AbstractDbBackedDataService using the in-package ForTestConfig -// fixture. The library's real starter (SpringServicesAutoConfiguration) is excluded: because +// fixture. The library's real starter (SpringServicesCoreAutoConfiguration) is excluded: because // ForTestConfig lives inside the library root package, the starter's additive // AutoConfigurationPackages.register("com.openelements.spring.base") would overlap ForTestConfig's // own (.data) package registration — registering ForTestRepository twice — and would also pull the @@ -22,7 +22,7 @@ @SpringBootTest( classes = ForTestConfig.class, properties = - "spring.autoconfigure.exclude=com.openelements.spring.base.SpringServicesAutoConfiguration") + "spring.autoconfigure.exclude=com.openelements.spring.base.SpringServicesCoreAutoConfiguration") public class Tests { @Autowired private ForTestDataService forTestDataService; From 488ad8c8dc80c4726e77db0c5eaa2e3c6cc4e038 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 21:22:14 +0200 Subject: [PATCH 07/11] feat(all): add spring-services-all aggregator module + aggregate test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 8 of spec 014. spring-services-all depends on core plus every feature module (slack, search, dbbackup, email, mcp) and ships no configuration of its own — a pure dependency bundle and drop-in replacement for the pre-split coordinate. Hosts the relocated aggregate/zero-config starter integration test: a zero-config consumer app (com.example.aggregate) with the full reactor on the classpath, asserting the app's own and the core library's persistence resolve and that representative beans from the slack, email and mcp modules are all present (self-activation by classpath presence, no @Import). Aggregate test green (2). Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 1 + spring-services-all/pom.xml | 83 +++++++++++++++++++ .../com/example/aggregate/AggregateApp.java | 13 +++ .../AggregateStarterIntegrationTest.java | 72 ++++++++++++++++ .../aggregate/AggregateTestConfig.java | 29 +++++++ .../java/com/example/aggregate/Order.java | 42 ++++++++++ .../example/aggregate/OrderRepository.java | 7 ++ .../application-testcontainers.properties | 4 + 8 files changed, 251 insertions(+) create mode 100644 spring-services-all/pom.xml create mode 100644 spring-services-all/src/test/java/com/example/aggregate/AggregateApp.java create mode 100644 spring-services-all/src/test/java/com/example/aggregate/AggregateStarterIntegrationTest.java create mode 100644 spring-services-all/src/test/java/com/example/aggregate/AggregateTestConfig.java create mode 100644 spring-services-all/src/test/java/com/example/aggregate/Order.java create mode 100644 spring-services-all/src/test/java/com/example/aggregate/OrderRepository.java create mode 100644 spring-services-all/src/test/resources/application-testcontainers.properties diff --git a/pom.xml b/pom.xml index a782e20..aff2eab 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,7 @@ spring-services-dbbackup spring-services-email spring-services-mcp + spring-services-all diff --git a/spring-services-all/pom.xml b/spring-services-all/pom.xml new file mode 100644 index 0000000..ebbc833 --- /dev/null +++ b/spring-services-all/pom.xml @@ -0,0 +1,83 @@ + + 4.0.0 + + + com.open-elements + spring-services + 1.3.0-SNAPSHOT + + + spring-services-all + + Spring Services All + Everything bundle: depends on core plus every optional feature module. Drop-in + replacement for the pre-split spring-services artifact. Ships no configuration of its own — + each module self-activates via its own auto-configuration. + + + + com.open-elements + spring-services-core + ${project.version} + + + com.open-elements + spring-services-slack + ${project.version} + + + com.open-elements + spring-services-search + ${project.version} + + + com.open-elements + spring-services-dbbackup + ${project.version} + + + com.open-elements + spring-services-email + ${project.version} + + + com.open-elements + spring-services-mcp + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-testcontainers + test + + + org.testcontainers + testcontainers + test + + + org.testcontainers + testcontainers-postgresql + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + org.postgresql + postgresql + test + + + + diff --git a/spring-services-all/src/test/java/com/example/aggregate/AggregateApp.java b/spring-services-all/src/test/java/com/example/aggregate/AggregateApp.java new file mode 100644 index 0000000..3b1f337 --- /dev/null +++ b/spring-services-all/src/test/java/com/example/aggregate/AggregateApp.java @@ -0,0 +1,13 @@ +package com.example.aggregate; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; + +/** + * A zero-config consumer application with the full reactor on the classpath + * ({@code spring-services-all}). It declares its own entity/repository ({@link Order}) and + * no {@code @Import}, {@code @EntityScan}, or {@code @EnableJpaRepositories} — every + * library module self-activates through its own auto-configuration. + */ +@SpringBootApplication(exclude = OAuth2ResourceServerAutoConfiguration.class) +public class AggregateApp {} diff --git a/spring-services-all/src/test/java/com/example/aggregate/AggregateStarterIntegrationTest.java b/spring-services-all/src/test/java/com/example/aggregate/AggregateStarterIntegrationTest.java new file mode 100644 index 0000000..f615d17 --- /dev/null +++ b/spring-services-all/src/test/java/com/example/aggregate/AggregateStarterIntegrationTest.java @@ -0,0 +1,72 @@ +package com.example.aggregate; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.openelements.spring.base.mcp.McpProperties; +import com.openelements.spring.base.services.email.EmailService; +import com.openelements.spring.base.services.slack.SlackService; +import com.openelements.spring.base.services.user.SystemUser; +import com.openelements.spring.base.services.user.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * Aggregate / zero-config starter integration test (spec 013 gate 1, relocated here per spec 014). + * + *

    Boots {@link AggregateApp} — a zero-config consumer with the full reactor + * ({@code spring-services-all}) on the classpath — and asserts that: + * + *

      + *
    • the application's own {@link Order}/{@link OrderRepository} resolve (default entity scan + * intact); + *
    • the core library persistence resolves ({@link UserRepository}, System User bootstrapped); + *
    • representative beans from multiple optional feature modules are present — {@link SlackService} + * (slack), {@link EmailService} (email), and {@link McpProperties} (mcp) — proving every module + * self-activated by classpath presence without any {@code @Import}. + *
    + */ +@SpringBootTest(classes = AggregateApp.class) +@Import(AggregateTestConfig.class) +@Testcontainers +@ActiveProfiles("testcontainers") +class AggregateStarterIntegrationTest { + + @Autowired private ApplicationContext context; + + @Autowired private OrderRepository orderRepository; + + @Autowired private UserRepository userRepository; + + @Test + @DisplayName("Application and core-library persistence both resolve under the full classpath") + void applicationAndCorePersistenceResolve() { + final Order order = new Order(); + order.setDescription("aggregate order"); + final Order saved = orderRepository.save(order); + + assertThat(orderRepository.findById(saved.getId())).isPresent(); + assertThat(userRepository.findById(SystemUser.ID)) + .as("System User must be bootstrapped in the dedicated schema") + .isPresent(); + } + + @Test + @DisplayName("Representative beans from the slack, email and mcp feature modules are all present") + void optionalFeatureModulesSelfActivate() { + assertThat(context.getBeanNamesForType(SlackService.class)) + .as("slack module must self-activate") + .isNotEmpty(); + assertThat(context.getBeanNamesForType(EmailService.class)) + .as("email module must self-activate") + .isNotEmpty(); + assertThat(context.getBeanNamesForType(McpProperties.class)) + .as("mcp module must self-activate (properties bound unconditionally)") + .isNotEmpty(); + } +} diff --git a/spring-services-all/src/test/java/com/example/aggregate/AggregateTestConfig.java b/spring-services-all/src/test/java/com/example/aggregate/AggregateTestConfig.java new file mode 100644 index 0000000..a5c8358 --- /dev/null +++ b/spring-services-all/src/test/java/com/example/aggregate/AggregateTestConfig.java @@ -0,0 +1,29 @@ +package com.example.aggregate; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.testcontainers.containers.PostgreSQLContainer; + +/** + * Test infrastructure for the aggregate context: a PostgreSQL Testcontainer and a stub + * {@link JwtDecoder} required by the core JWT security chain (the aggregate test does not exercise + * real JWT validation). + */ +@TestConfiguration(proxyBeanMethods = false) +public class AggregateTestConfig { + + @Bean + @ServiceConnection + public PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer<>("postgres:16-alpine"); + } + + @Bean + public JwtDecoder testJwtDecoder() { + return token -> { + throw new UnsupportedOperationException("JWT validation is disabled in the aggregate test"); + }; + } +} diff --git a/spring-services-all/src/test/java/com/example/aggregate/Order.java b/spring-services-all/src/test/java/com/example/aggregate/Order.java new file mode 100644 index 0000000..2588415 --- /dev/null +++ b/spring-services-all/src/test/java/com/example/aggregate/Order.java @@ -0,0 +1,42 @@ +package com.example.aggregate; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; + +/** + * A consumer application's own entity (default schema, not {@code oe_spring_services}), used by the + * aggregate test to prove the application's own persistence resolves alongside the full library. + */ +@Entity +@Table(name = "orders") +public class Order { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id", updatable = false, nullable = false) + private UUID id; + + @Column(name = "description") + private String description; + + public UUID getId() { + return id; + } + + public void setId(final UUID id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(final String description) { + this.description = description; + } +} diff --git a/spring-services-all/src/test/java/com/example/aggregate/OrderRepository.java b/spring-services-all/src/test/java/com/example/aggregate/OrderRepository.java new file mode 100644 index 0000000..75e6b95 --- /dev/null +++ b/spring-services-all/src/test/java/com/example/aggregate/OrderRepository.java @@ -0,0 +1,7 @@ +package com.example.aggregate; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +/** The aggregate test application's own repository. */ +public interface OrderRepository extends JpaRepository {} diff --git a/spring-services-all/src/test/resources/application-testcontainers.properties b/spring-services-all/src/test/resources/application-testcontainers.properties new file mode 100644 index 0000000..31464e6 --- /dev/null +++ b/spring-services-all/src/test/resources/application-testcontainers.properties @@ -0,0 +1,4 @@ +spring.jpa.hibernate.ddl-auto=create-drop +# Provision the dedicated oe_spring_services schema for the schema-qualified library entities. +spring.jpa.properties.hibernate.hbm2ddl.create_namespaces=true +spring.main.allow-bean-definition-overriding=true From b5e87e2adc37ac022b63fea48757cccfc3f9b2db Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 21:25:09 +0200 Subject: [PATCH 08/11] feat(bom): add spring-services-bom and core dependency-isolation guards Step 9 + 10 of spec 014. spring-services-bom (packaging=pom) manages every module at the lockstep ${project.version}, so a-la-carte consumers import it once and declare modules without versions. Isolation guarantees: - maven-enforcer bannedDependencies in core fails the build if slack-api-client, the MCP SDK, or spring-boot-starter-mail ever leak into core's dependency tree ("core does not pull optional heavy dependencies"). - McpAutoConfigurationTest proves the feature stays inert (no MCP beans) when the MCP SDK type is filtered off the classpath. Reactor builds all 9 modules; enforcer passes; mcp module tests green (38). Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- pom.xml | 1 + spring-services-bom/pom.xml | 58 +++++++++++++++++++ spring-services-core/pom.xml | 34 +++++++++++ .../base/mcp/McpAutoConfigurationTest.java | 36 ++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 spring-services-bom/pom.xml create mode 100644 spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpAutoConfigurationTest.java diff --git a/pom.xml b/pom.xml index aff2eab..277ec95 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,7 @@ spring-services-email spring-services-mcp spring-services-all + spring-services-bom diff --git a/spring-services-bom/pom.xml b/spring-services-bom/pom.xml new file mode 100644 index 0000000..6c3295d --- /dev/null +++ b/spring-services-bom/pom.xml @@ -0,0 +1,58 @@ + + 4.0.0 + + + com.open-elements + spring-services + 1.3.0-SNAPSHOT + + + spring-services-bom + pom + + Spring Services BOM + Bill of materials: import once to manage every spring-services module at a single + lockstep version, then declare individual modules without versions. + + + + + com.open-elements + spring-services-core + ${project.version} + + + com.open-elements + spring-services-slack + ${project.version} + + + com.open-elements + spring-services-search + ${project.version} + + + com.open-elements + spring-services-dbbackup + ${project.version} + + + com.open-elements + spring-services-email + ${project.version} + + + com.open-elements + spring-services-mcp + ${project.version} + + + com.open-elements + spring-services-all + ${project.version} + + + + + diff --git a/spring-services-core/pom.xml b/spring-services-core/pom.xml index 98172d8..ad01b21 100644 --- a/spring-services-core/pom.xml +++ b/spring-services-core/pom.xml @@ -94,4 +94,38 @@ + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + ban-optional-feature-dependencies + + enforce + + + + + + com.slack.api:slack-api-client + io.modelcontextprotocol.sdk:* + org.springframework.boot:spring-boot-starter-mail + + spring-services-core must not depend on optional feature + libraries (slack-api-client, the MCP SDK, or + spring-boot-starter-mail). Those belong to the feature + modules. + + + + + + + + + diff --git a/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpAutoConfigurationTest.java b/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpAutoConfigurationTest.java new file mode 100644 index 0000000..b39150c --- /dev/null +++ b/spring-services-mcp/src/test/java/com/openelements/spring/base/mcp/McpAutoConfigurationTest.java @@ -0,0 +1,36 @@ +package com.openelements.spring.base.mcp; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.modelcontextprotocol.server.McpServer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** + * Activation-guard test for {@link McpAutoConfiguration}: proves the {@code @ConditionalOnClass} + * guard keeps the feature inert when the MCP SDK is not on the classpath, so a bundle that ships the + * auto-configuration class without the SDK causes no failure. + */ +@DisplayName("McpAutoConfiguration — activation guard") +class McpAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(McpAutoConfiguration.class)); + + @Test + @DisplayName("Stays inert (no MCP beans) when the MCP SDK type McpServer is absent") + void inertWhenSdkAbsent() { + contextRunner + .withClassLoader(new FilteredClassLoader(McpServer.class)) + .run( + context -> + assertThat(context) + .hasNotFailed() + .doesNotHaveBean(McpAutoConfiguration.class) + .doesNotHaveBean(McpProperties.class)); + } +} From 325033ddd927fa52208fddba31e7e304a32169a9 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 21:28:08 +0200 Subject: [PATCH 09/11] build(release): wire release tooling for the multi-artifact reactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 11 of spec 014. - release.sh: versions:set now uses -DprocessAllModules so a global version bump stays lockstep across the parent and every module (each pins the parent version explicitly). - release.yml: stage artifacts into an ABSOLUTE github.workspace/target/staging-deploy so all module artifacts (core, feature modules, -all, -bom) land in the single root staging directory JReleaser reads — a relative path would scatter them per-module and JReleaser (run once at root) would miss them. build.yml (clean verify) and snapshot.yml (reactor deploy) already build/deploy the whole reactor and need no change. The deploy/JReleaser path only runs on a tag, so it is validated at release time (per the spec's open question). Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 9 ++++++++- release.sh | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25541a8..af99780 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,14 @@ jobs: # distributionManagement points at GitHub Packages, so we override # the deploy target to the local staging directory configured in the # JReleaser block (target/staging-deploy). - run: ./mvnw -B -Pfull-build clean deploy -DaltDeploymentRepository=local::file:./target/staging-deploy + # + # The path is ABSOLUTE (github.workspace) on purpose: in the multi-module + # reactor a relative ./target/staging-deploy would resolve per-module and + # scatter each module's artifacts under its own target/, so JReleaser + # (running once at the root) would not find them. An absolute path stages + # every module — core, the feature modules, spring-services-all and the BOM — + # into the single root staging directory JReleaser reads. + run: ./mvnw -B -Pfull-build clean deploy -DaltDeploymentRepository=local::file:${{ github.workspace }}/target/staging-deploy - name: Publish with JReleaser run: ./mvnw -B -Pdeploy-release jreleaser:full-release -N diff --git a/release.sh b/release.sh index 6d9780c..f075b39 100755 --- a/release.sh +++ b/release.sh @@ -53,7 +53,9 @@ generate_release_doc() { } echo "Releasing version $NEW_VERSION" -./mvnw versions:set -DnewVersion=$NEW_VERSION +# -DprocessAllModules keeps the reactor lockstep: it bumps the parent and every +# module (each references the parent version explicitly, not ${revision}). +./mvnw versions:set -DnewVersion=$NEW_VERSION -DprocessAllModules # Build and test locally so we never push a tag that fails CI. # @@ -96,6 +98,6 @@ git tag "v$NEW_VERSION" git push origin "v$NEW_VERSION" echo "Setting version to $NEXT_VERSION" -./mvnw versions:set -DnewVersion=$NEXT_VERSION +./mvnw versions:set -DnewVersion=$NEXT_VERSION -DprocessAllModules git commit -am "Version $NEXT_VERSION" git push From 028bee6e42a440080726b0cd823e6f46458f2c75 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 21:30:35 +0200 Subject: [PATCH 10/11] docs(spec-014): add upgrade-to-1.4 guide and update README for the reactor Step 12 of spec 014. docs/releases/upgrade-to-1.4.md documents the breaking coordinate change (spring-services parent pom -> spring-services-all drop-in, or a-la-carte via the BOM), the self-activation model, and the SpringServicesAutoConfiguration -> SpringServicesCoreAutoConfiguration rename, with an ordering note relative to the 1.3.0 schema migration. README Installation and Project Layout now describe the module coordinates and reactor structure. Spec: docs/specs/014-multi-module-restructuring (#27) Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 78 ++++++++++++++---- docs/releases/upgrade-to-1.4.md | 138 ++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 docs/releases/upgrade-to-1.4.md diff --git a/README.md b/README.md index 6e8ad9b..7ec65ab 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,52 @@ into the full stack with a single import or pick the parts they need. Releases are published to Maven Central; SNAPSHOTs are published to the Sonatype Central Portal snapshot repository on every push to `main`. +`spring-services` is a **Maven multi-module reactor**. The bare `com.open-elements:spring-services` +coordinate is the reactor parent (a `pom`, no classes) — depend on one of the modules instead. + +**Everything (drop-in):** `spring-services-all` bundles the core plus every optional feature module. + ```xml com.open-elements - spring-services + spring-services-all ``` +**À la carte:** import the BOM once, then declare `spring-services-core` plus only the feature +modules you need (`spring-services-slack`, `spring-services-mcp`, `spring-services-email`, +`spring-services-search`, `spring-services-dbbackup`) without versions: + +```xml + + + + com.open-elements + spring-services-bom + + pom + import + + + + + + + com.open-elements + spring-services-core + + + com.open-elements + spring-services-slack + + +``` + +Each feature module self-activates when present on the classpath, so consumers pull only the +dependencies they use. Migrating from the pre-split `spring-services` artifact? See +[`docs/releases/upgrade-to-1.4.md`](docs/releases/upgrade-to-1.4.md). + To consume a `-SNAPSHOT` build, also declare the Central Portal snapshot repository (releases still come from Maven Central): @@ -272,23 +310,33 @@ later release). Consumers must run one Flyway migration when upgrading. ## Project Layout +A Maven reactor: `spring-services` (parent, `pom`) aggregates the modules below. All classes stay +under `com.openelements.spring.base.*` regardless of the module that ships them. + ``` -src/main/java/com/openelements/spring/base/ -├── FullSpringServiceConfig.java — Aggregate @Import of every feature config -├── data/ — Generic CRUD abstractions -├── events/ — Lifecycle events -├── security/ — Filter chain, JWT converter, AuthService -│ ├── apikey/ — X-API-Key authentication filter -│ └── user/ — Local user mirror -├── services/ -│ ├── apikey/ — API key data layer -│ ├── search/ — Meilisearch client, startup reindex, highlighter -│ ├── settings/ — Key/value settings -│ ├── tag/ — Tag CRUD + PreTagDeleteEvent -│ └── webhook/ — Outbound webhook delivery -└── tenant/ — Multi-tenancy primitives +spring-services/ — reactor parent (packaging=pom) +├── spring-services-core — all 7 @Entity + single persistence unit, the core +│ auto-configuration (SpringServicesCoreAutoConfiguration) and +│ the always-present features: +│ data/ — generic CRUD abstractions +│ events/ — lifecycle events +│ security/ — filter chains, JWT converter, AuthService +│ apikey/, user/ +│ services/ — apikey, settings, tag, comment, audit, +│ webhook, translation, system +│ tenant/, apitoken/ +├── spring-services-slack — Slack messaging (→ slack-api-client) +├── spring-services-mcp — MCP server (→ MCP SDK) +├── spring-services-email — SMTP email (→ spring-boot-starter-mail) +├── spring-services-search — Meilisearch full-text search (RestClient, no extra dep) +├── spring-services-dbbackup — db-backup sidecar client (RestClient, no extra dep) +├── spring-services-all — everything bundle (depends on all modules; no config of its own) +└── spring-services-bom — bill of materials for lockstep versioning ``` +Each optional feature module ships its own `@AutoConfiguration` guarded by `@ConditionalOnClass`, so +it self-activates when present and never pulls its heavy dependency into a consumer that skips it. + ## Release Process ### SNAPSHOT Publishing (automatic) diff --git a/docs/releases/upgrade-to-1.4.md b/docs/releases/upgrade-to-1.4.md new file mode 100644 index 0000000..d7d54a4 --- /dev/null +++ b/docs/releases/upgrade-to-1.4.md @@ -0,0 +1,138 @@ +# Upgrade prompt: `com.open-elements:spring-services` 1.3.0 → 1.4.0 + +`spring-services` 1.4.0 restructures the library into a **Maven multi-module reactor**. This is a +**breaking coordinate change** — but **not** a Java-API or database change: + +- The `com.open-elements:spring-services` coordinate is now the **reactor parent** (`packaging=pom`). + It no longer resolves to a jar with classes, so a consumer that declares it as a dependency will + fail to compile. +- The library is split into `spring-services-core` (all seven entities, single persistence unit) plus + optional feature modules — `spring-services-slack`, `spring-services-mcp`, `spring-services-email`, + `spring-services-search`, `spring-services-dbbackup` — an **everything** bundle + `spring-services-all`, and a BOM `spring-services-bom`. +- Consumers now depend only on the features they use; a consumer that used everything switches to + `spring-services-all` (drop-in). Heavy/rare libraries (`slack-api-client`, the MCP SDK, + `spring-boot-starter-mail`) are no longer pulled unless the matching module is present. + +There are **no public Java API changes** (method signatures, DTOs) and **no schema change** — the +`oe_spring_services` schema introduced in 1.3.0 is unchanged. The one internal rename: the starter +auto-configuration `com.openelements.spring.base.SpringServicesAutoConfiguration` is now +`SpringServicesCoreAutoConfiguration` (only relevant if you referenced it in an +`spring.autoconfigure.exclude` list). + +> If you are upgrading from a version **older than 1.3.0**, apply the 1.3.0 database migration first +> (see `upgrade-to-1.3.md`: move the tables into `oe_spring_services`), then this coordinate swap. + +This file is a self-contained prompt for an agent (Claude Code, etc.) to run inside a consumer repo. +Paste it verbatim. + +--- + +## Prompt + +You are working inside a Spring Boot service that depends on `com.open-elements:spring-services`. +Goal: upgrade from `1.3.0` to `1.4.0`. The library became a Maven reactor; you must change the +dependency coordinate. No application code, schema, or configuration changes are required. + +### What changed in 1.4.0 + +#### Dependency coordinate (required change) + +The old single coordinate no longer ships classes. Choose one of two migration paths. + +**Path A — everything (drop-in).** Replace `spring-services` with `spring-services-all`: + +```xml + + + com.open-elements + spring-services + 1.3.0 + + + + + com.open-elements + spring-services-all + 1.4.0 + +``` + +`spring-services-all` bundles core plus every feature module, reproducing the pre-split behaviour. + +**Path B — à-la-carte (only what you use).** Import the BOM once, then declare `spring-services-core` +plus only the feature modules you need, without versions: + +```xml + + + + com.open-elements + spring-services-bom + 1.4.0 + pom + import + + + + + + + com.open-elements + spring-services-core + + + + com.open-elements + spring-services-slack + + +``` + +Feature module artifact ids: `spring-services-slack`, `spring-services-mcp`, +`spring-services-email`, `spring-services-search`, `spring-services-dbbackup`. + +#### Self-activation + +Each feature module self-activates when it is on the classpath (via its own auto-configuration), +gated by the same enable properties as before (e.g. `openelements.mcp.enabled`, +`openelements.meilisearch.enabled`, `openelements.db-backup.enabled`). Slack and email activate when +their module is present. You do not need `@Import` to wire a feature — adding the module is enough. + +#### Auto-configuration rename (only if you excluded it) + +If your application excluded the starter auto-configuration by name, rename it: + +``` +com.openelements.spring.base.SpringServicesAutoConfiguration + -> +com.openelements.spring.base.SpringServicesCoreAutoConfiguration +``` + +Most applications never reference this class and have nothing to do here. + +#### No API, schema, or configuration changes + +No public Java types or signatures changed. The database schema (`oe_spring_services`, from 1.3.0) is +unchanged — no migration. Property names are unchanged. + +### Steps + +1. Find the consumer's build file that declares `com.open-elements:spring-services`. +2. Replace it per **Path A** (switch to `spring-services-all`) or **Path B** (BOM + `-core` + chosen + feature modules). Bump the version to `1.4.0`. +3. If you kept a hand-written `spring.autoconfigure.exclude` for the starter, rename it to + `SpringServicesCoreAutoConfiguration`. +4. Resolve dependencies (`mvn -U dependency:resolve` or `./gradlew --refresh-dependencies`). +5. Compile and run the consumer's test suite to verify the build is green. +6. Commit the coordinate change. + +### Guard rails / Don't do this + +- Do **not** keep depending on the bare `com.open-elements:spring-services` coordinate — it is now a + parent pom and carries no classes. +- Do **not** re-add `slack-api-client`, the MCP SDK, or `spring-boot-starter-mail` by hand — pull the + corresponding feature module instead, which brings the dependency transitively. +- Do **not** change the database or any `@Table(schema = ...)` — there is no schema change in 1.4.0. +- Do **not** mix explicit module versions with the BOM import; let the BOM manage them. +- Do **not** bundle unrelated upgrades into this coordinate change. From af8c15236ae1354128d97b0d91c0b63d40205506 Mon Sep 17 00:00:00 2001 From: Hendrik Ebbers Date: Mon, 13 Jul 2026 21:46:08 +0200 Subject: [PATCH 11/11] chore(spec-014): mark spec status as done Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/specs/INDEX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/specs/INDEX.md b/docs/specs/INDEX.md index 8695d4f..c10fdb2 100644 --- a/docs/specs/INDEX.md +++ b/docs/specs/INDEX.md @@ -15,6 +15,6 @@ | 011 | 011-security-config-hygiene | Security config hygiene | backend, security, architecture | Tighten Spring Boot auto-configuration annotations, drop synchronized from UserService hot path, centralise JSON AuthenticationEntryPoint, use SecurityContextHolderStrategy, replace UserInformation sentinel with Optional, lock Roles utility class | — | done | | 012 | 012-scim-foundation-user-model | SCIM foundation user model | backend, security, database | Relax UserEntity.sub, add externalId / userName / active, JIT-login correlation and backfill, AccessDeniedException gate for deactivated users — prepares the data model for the SCIM-Provider spec without introducing SCIM endpoints | — | done | | 013 | 013-dedicated-schema-and-starter | Dedicated schema + starter | backend, database, architecture, build | Move all 7 library tables into a fixed `oe_spring_services` schema (single persistence unit) and ship a real Spring Boot starter that registers the library package additively via AutoConfigurationPackages — zero-config consumption. Migrations stay consumer-managed; the library ships ready-to-apply per-release SQL in the upgrade doc. Includes the app→lib cross-entity reference contract | #25 | done | -| 014 | 014-multi-module-restructuring | Multi-module restructuring | build, architecture, infrastructure | Restructure the single artifact into a Maven reactor: `spring-services-core` (all 7 entities, single persistence unit) + optional feature modules (`slack`, `mcp`, `email`, `search`, `dbbackup`) + `spring-services-all` aggregator jar + `spring-services-bom`. Lockstep versioning; per-module `@AutoConfiguration` guarded by `@ConditionalOnClass`; builds on spec 013 (released first). Breaking change: `spring-services` coordinate becomes the reactor parent, consumers migrate to `spring-services-all` | #27 | in progress | +| 014 | 014-multi-module-restructuring | Multi-module restructuring | build, architecture, infrastructure | Restructure the single artifact into a Maven reactor: `spring-services-core` (all 7 entities, single persistence unit) + optional feature modules (`slack`, `mcp`, `email`, `search`, `dbbackup`) + `spring-services-all` aggregator jar + `spring-services-bom`. Lockstep versioning; per-module `@AutoConfiguration` guarded by `@ConditionalOnClass`; builds on spec 013 (released first). Breaking change: `spring-services` coordinate becomes the reactor parent, consumers migrate to `spring-services-all` | #27 | done | | 015 | 015-scim-users-provider | SCIM Users provider | backend, security, api, database | SCIM 2.0 service-provider first slice: an isolated `/scim/v2/**` bearer-token filter chain (static `openelements.scim.token`), discovery endpoints (ServiceProviderConfig/ResourceTypes/Schemas), and the Users resource (list/filter, POST, GET, PUT full-replace, soft DELETE) mapped onto spec 012's `UserEntity`. `POST` collision → `409 uniqueness`; DELETE is soft (`deleted`/`deleted_at`); writes audited under a reserved SCIM service principal. Groups+membership and Group→Role mapping deferred to follow-up issues; grounded in real Authentik 2026.5.4 traffic (#21) with a pre-merge verification test plan. | #21 | open |