diff --git a/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/CloudAuditStoreImpl.java b/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/CloudAuditStoreImpl.java index 57bb7180f..19ab1f30b 100644 --- a/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/CloudAuditStoreImpl.java +++ b/cloud/flamingock-cloud/src/main/java/io/flamingock/cloud/CloudAuditStoreImpl.java @@ -49,11 +49,32 @@ import java.io.IOException; import java.util.List; import java.util.stream.Collectors; +import io.flamingock.api.external.ExternalSystem; +import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; public class CloudAuditStoreImpl implements CloudAuditStore { private static final Logger logger = FlamingockLoggerFactory.getLogger("CloudAuditStore"); private CloudAuditPersistenceImpl persistence; + /** Lazily created stand-in; see {@link #getExternalSystem()}. */ + private NonTransactionalTargetSystem externalSystem; + + /** + * {@inheritDoc} + *

+ * This store has no external system of its own - it keeps everything in Flamingock Cloud. A + * non-transactional stand-in is returned rather than {@code null} so that the reserved + * {@code AUDIT_STORE_TARGET_SYSTEM} id still resolves, letting system changes run against this store. + * Cached because the identity of the instance matters once it is registered. + */ + @Override + public synchronized ExternalSystem getExternalSystem() { + if (externalSystem == null) { + externalSystem = new NonTransactionalTargetSystem(getId()); + } + return externalSystem; + } + @Override public String getId() { return Constants.DEFAULT_CLOUD_AUDIT_STORE; diff --git a/community/flamingock-couchbase-auditstore/src/main/java/io/flamingock/store/couchbase/CouchbaseAuditStore.java b/community/flamingock-couchbase-auditstore/src/main/java/io/flamingock/store/couchbase/CouchbaseAuditStore.java index a4c0765a8..ccf7530b8 100644 --- a/community/flamingock-couchbase-auditstore/src/main/java/io/flamingock/store/couchbase/CouchbaseAuditStore.java +++ b/community/flamingock-couchbase-auditstore/src/main/java/io/flamingock/store/couchbase/CouchbaseAuditStore.java @@ -89,6 +89,17 @@ public static CouchbaseAuditStore from(CouchbaseExternalSystem targetSystem) { return new CouchbaseAuditStore(targetSystem); } + /** + * {@inheritDoc} + *

+ * The system this store was handed by {@code from(...)} - not to be confused with + * {@link #getId()}, which names the store itself. + */ + @Override + public CouchbaseExternalSystem getExternalSystem() { + return targetSystem; + } + @Override public String getId() { return Constants.DEFAULT_COUCHBASE_AUDIT_STORE; diff --git a/community/flamingock-dynamodb-auditstore/src/main/java/io/flamingock/store/dynamodb/DynamoDBAuditStore.java b/community/flamingock-dynamodb-auditstore/src/main/java/io/flamingock/store/dynamodb/DynamoDBAuditStore.java index 7ec12203d..313022199 100644 --- a/community/flamingock-dynamodb-auditstore/src/main/java/io/flamingock/store/dynamodb/DynamoDBAuditStore.java +++ b/community/flamingock-dynamodb-auditstore/src/main/java/io/flamingock/store/dynamodb/DynamoDBAuditStore.java @@ -77,6 +77,17 @@ public static DynamoDBAuditStore from(DynamoDBExternalSystem targetSystem) { return new DynamoDBAuditStore(targetSystem); } + /** + * {@inheritDoc} + *

+ * The system this store was handed by {@code from(...)} - not to be confused with + * {@link #getId()}, which names the store itself. + */ + @Override + public DynamoDBExternalSystem getExternalSystem() { + return targetSystem; + } + @Override public String getId() { return Constants.DEFAULT_DYNAMODB_AUDIT_STORE; diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java index 22f986fc6..bf4b722ee 100644 --- a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java +++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java @@ -90,6 +90,17 @@ public static MongoDBReactiveAuditStore from(MongoDBReactiveExternalSystem targe return new MongoDBReactiveAuditStore(targetSystem); } + /** + * {@inheritDoc} + *

+ * The system this store was handed by {@code from(...)} - not to be confused with + * {@link #getId()}, which names the store itself. + */ + @Override + public MongoDBReactiveExternalSystem getExternalSystem() { + return mongoDBTargetSystem; + } + @Override public String getId() { return Constants.DEFAULT_MONGODB_AUDIT_STORE; diff --git a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/MongoDBSyncAuditStore.java b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/MongoDBSyncAuditStore.java index 080d6392f..bb65ff274 100644 --- a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/MongoDBSyncAuditStore.java +++ b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/MongoDBSyncAuditStore.java @@ -95,6 +95,17 @@ public static MongoDBSyncAuditStore from(MongoDBExternalSystem targetSystem) { return new MongoDBSyncAuditStore(targetSystem); } + /** + * {@inheritDoc} + *

+ * The system this store was handed by {@code from(...)} - not to be confused with + * {@link #getId()}, which names the store itself. + */ + @Override + public MongoDBExternalSystem getExternalSystem() { + return mongoDBTargetSystem; + } + @Override public String getId() { return Constants.DEFAULT_MONGODB_AUDIT_STORE; diff --git a/community/flamingock-sql-auditstore/src/main/java/io/flamingock/store/sql/SqlAuditStore.java b/community/flamingock-sql-auditstore/src/main/java/io/flamingock/store/sql/SqlAuditStore.java index 13083872c..ce40a1ebb 100644 --- a/community/flamingock-sql-auditstore/src/main/java/io/flamingock/store/sql/SqlAuditStore.java +++ b/community/flamingock-sql-auditstore/src/main/java/io/flamingock/store/sql/SqlAuditStore.java @@ -76,6 +76,17 @@ public static SqlAuditStore from(SqlExternalSystem targetSystem) { return new SqlAuditStore(targetSystem); } + /** + * {@inheritDoc} + *

+ * The system this store was handed by {@code from(...)} - not to be confused with + * {@link #getId()}, which names the store itself. + */ + @Override + public SqlExternalSystem getExternalSystem() { + return targetSystem; + } + @Override public String getId() { return Constants.DEFAULT_SQL_AUDIT_STORE; diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java index d03a46d95..424ec70e4 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java @@ -24,6 +24,22 @@ public class PipelineHelper { public static final String SYSTEM_STAGE_ID = "flamingock-system-stage"; public static final String LEGACY_STAGE_ID = "flamingock-legacy-stage"; + /** + * Reserved target-system id resolving to whatever target system the audit store was built from. + *

+ * System changes that operate on the audit store itself - compaction, journal backfill - have no target + * system of their own to name, yet every change is resolved against one + * ({@code ChangeProcessStrategyFactory} asks {@code TargetSystemManager}, which throws on an unknown + * id). Declaring this id lets them reach the audit store's own target system without the user having to + * name it, and without the framework inventing a default. + *

+ * Lives here rather than in {@code io.flamingock.internal.util.Constants} because that class ships from + * the external {@code flamingock-general-util} artifact, so adding to it needs a cross-repo release. + * This module is reachable from both the annotation processor and the runtime, which is what a reserved + * id needs. + */ + public static final String AUDIT_STORE_TARGET_SYSTEM = "flamingock-audit-store-target-system"; + private final PipelineDescriptor pipelineDescriptor; public PipelineHelper(PipelineDescriptor pipelineDescriptor) { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractBuilder.java index 88b184090..ebe1258dd 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/builder/AbstractBuilder.java @@ -82,6 +82,12 @@ protected AbstractBuilder( protected abstract HOLDER getSelf(); protected void configureStoreAndTargetSystem(PriorityContext dependencyContext) { + // Before initialize(), and that order is load-bearing on two counts: the manager rejects + // registrations once initialized, and registering now is what gets the audit store's target system + // initialized along with the rest when the user never added it themselves. Safe this early because + // getExternalSystem() only hands back a constructor field - the audit store itself need not be + // initialized yet. + targetSystemManager.registerAuditStoreTargetSystem(auditStore.getExternalSystem()); //remove this, targetSystem should be mandatory targetSystemManager.initialize(dependencyContext); auditStore.initialize(dependencyContext); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/AuditStore.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/AuditStore.java index dfb2f2dda..3d82401fa 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/AuditStore.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/AuditStore.java @@ -29,6 +29,23 @@ public interface AuditStore extends ExternalSystem, ContextInitializable { + /** + * Returns the external system this audit store was built from. + *

+ * Not to be confused with {@link #getId()}, which this store inherits from {@code ExternalSystem} and + * which identifies the store (for example {@code mongodb-audit-store}). This returns the + * system the store's data actually lives in - the one a {@code from(...)} factory was handed. + *

+ * Used to register the reserved {@code PipelineHelper.AUDIT_STORE_TARGET_SYSTEM} id, so that system + * changes operating on the audit store can be resolved against a real target system. + *

+ * Abstract rather than defaulted: a store that genuinely has no external system must say so by + * returning a non-transactional stand-in, not by inheriting a silent {@code null}. + * + * @return the external system backing this store; never {@code null} + */ + ExternalSystem getExternalSystem(); + @Deprecated default PERSISTENCE getPersistence() { throw new IllegalStateException("Audit stores must provide persistence through getPersistenceFactory(stageId)"); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/targets/TargetSystemManager.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/targets/TargetSystemManager.java index a1e472cda..d2e74136d 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/targets/TargetSystemManager.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/targets/TargetSystemManager.java @@ -15,7 +15,9 @@ */ package io.flamingock.internal.core.external.targets; +import io.flamingock.api.external.ExternalSystem; import io.flamingock.api.external.TargetSystem; +import io.flamingock.internal.common.core.pipeline.PipelineHelper; import io.flamingock.internal.common.core.context.ContextInitializable; import io.flamingock.internal.common.core.context.ContextResolver; import io.flamingock.internal.common.core.error.FlamingockException; @@ -50,6 +52,12 @@ public class TargetSystemManager implements ContextInitializable { //TODO if we decide to remove the TX_SHARED private AbstractTargetSystem auditStoreTargetSystem; private final Map> targetSystemMap = new HashMap<>(); + /** + * Reserved id to real id. Deliberately an indirection rather than extra {@link #targetSystemMap} + * entries: {@link #initialize} and {@link #getTransactionalTargetSystems} both iterate that map's + * values, so a second entry pointing at the same instance would initialize it twice and count it twice. + */ + private final Map aliases = new HashMap<>(); @Override public void initialize(ContextResolver contextResolver) { @@ -67,6 +75,58 @@ public void initialize(ContextResolver contextResolver) { * @param targetSystem the target system to register (must be non-null and have a non-empty ID) * @throws IllegalArgumentException if the target system or its ID is null/blank */ + /** + * Registers the audit store's own target system under the reserved + * {@link PipelineHelper#AUDIT_STORE_TARGET_SYSTEM} id. + *

+ * System changes that act on the audit store - compaction, journal backfill - have no target system of + * their own to name, but every change is resolved against one. Declaring the reserved id lets them + * reach the store's system without the user naming it. + *

+ * Registers the instance, not a name to look up later. That matters: an audit store built with + * {@code from(targetSystem)} does not require that target system to have been added to the builder, so + * resolving by id alone could find nothing - or worse, find a different instance that happens to share + * the id. + *

+ * Must be called before {@link #initialize}: {@link #validateAndCast} rejects late registrations, and + * registering earlier is also what gets an otherwise-unregistered system initialized along with the + * rest. + * + * @param externalSystem the system the audit store was built from + * @throws IllegalArgumentException if it is not an {@link AbstractTargetSystem}, has a blank id, or + * this manager is already initialized + */ + public void registerAuditStoreTargetSystem(ExternalSystem externalSystem) { + if (externalSystem == null) { + // Checked here rather than left to validateAndCast, whose "Target system null not allowed" + // says nothing about where the null came from. Every audit store must return a system - one + // that keeps no data of its own returns a NonTransactionalTargetSystem stand-in. + throw new IllegalArgumentException("The audit store returned no external system. " + + "AuditStore.getExternalSystem() must never return null; an audit store with no " + + "external system of its own should return a NonTransactionalTargetSystem instead."); + } + AbstractTargetSystem targetSystem = validateAndCast(externalSystem); + String id = targetSystem.getId(); + + AbstractTargetSystem alreadyRegistered = targetSystemMap.get(id); + if (alreadyRegistered == null) { + // The user never registered it, so register it here - otherwise it would neither resolve nor + // be initialized. + targetSystemMap.put(id, targetSystem); + } else if (alreadyRegistered != targetSystem) { + // Identity, not equality: the user's instance wins, because overwriting it would silently + // change what their own changes run against. But the audit store then operates against a + // different instance than it was built with, which is worth saying out loud. + logger.warn("The audit store was built from a target system with id[{}], but a different " + + "instance is already registered under that id. The registered one will be used to " + + "resolve [{}]", id, PipelineHelper.AUDIT_STORE_TARGET_SYSTEM); + } + + aliases.put(PipelineHelper.AUDIT_STORE_TARGET_SYSTEM, id); + logger.debug("Registered [{}] as an alias of target system [{}]", + PipelineHelper.AUDIT_STORE_TARGET_SYSTEM, id); + } + public void add(TargetSystem targetSystem) { targetSystemMap.put(targetSystem.getId(), validateAndCast(targetSystem)); } @@ -93,7 +153,10 @@ public TargetSystemOps getTargetSystem(TargetSystemDescriptor tsd) { */ public TargetSystemOps getTargetSystem(String id) { logger.debug("Resolving target system with id: [{}]", id); - + + // A reserved alias resolves to the id it points at; anything else passes straight through. + id = aliases.getOrDefault(id, id); + if (id == null || !targetSystemMap.containsKey(id)) { String availableTargetSystems = String.join(", ", targetSystemMap.keySet()); String message = String.format( @@ -125,7 +188,7 @@ public List> getTransactionalTargetSystems() { * @param targetSystem the target system to validate * @throws IllegalArgumentException if validation fails */ - private AbstractTargetSystem validateAndCast(TargetSystem targetSystem) { + private AbstractTargetSystem validateAndCast(ExternalSystem targetSystem) { if (targetSystem == null) { throw new IllegalArgumentException("Target system null not allowed"); } diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/targets/TargetSystemManagerTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/targets/TargetSystemManagerTest.java index c6dccfea4..d7043fdc9 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/targets/TargetSystemManagerTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/targets/TargetSystemManagerTest.java @@ -18,6 +18,7 @@ import io.flamingock.internal.common.core.context.RuntimeContext; import io.flamingock.internal.common.core.targets.OperationType; import io.flamingock.internal.common.core.external.ExecutionWrapper; +import io.flamingock.internal.common.core.pipeline.PipelineHelper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -86,6 +87,111 @@ void shouldPreserveTransactionalOperationsByDefault() { assertEquals(OperationType.TX_NON_SYNC, manager.getTargetSystem("tx-default").getOperationType()); } + // ----------------------------------------------------------------------------------------------- + // The reserved audit-store alias + // ----------------------------------------------------------------------------------------------- + + @Test + @DisplayName("The reserved alias resolves to the audit store's target system") + void aliasResolvesToTheAuditStoreTargetSystem() { + TargetSystemManager manager = new TargetSystemManager(); + StubTransactionalTargetSystem auditStoreSystem = new StubTransactionalTargetSystem("audit-db"); + manager.add(auditStoreSystem); + manager.registerAuditStoreTargetSystem(auditStoreSystem); + + assertEquals("audit-db", + manager.getTargetSystem(PipelineHelper.AUDIT_STORE_TARGET_SYSTEM).getId()); + } + + @Test + @DisplayName("The alias does not duplicate the target system") + void aliasDoesNotDuplicateTheTargetSystem() { + TargetSystemManager manager = new TargetSystemManager(); + StubTransactionalTargetSystem auditStoreSystem = new StubTransactionalTargetSystem("audit-db"); + manager.add(auditStoreSystem); + manager.registerAuditStoreTargetSystem(auditStoreSystem); + + // The reason the alias is an indirection rather than a second map entry: initialize() and this + // method both iterate the map's values, so a duplicate would be initialized twice and counted + // twice - and the duplicate count reaches CloudAuditStoreImpl as duplicate audit markers. + assertEquals(1, manager.getTransactionalTargetSystems().size()); + } + + @Test + @DisplayName("The alias registers the target system when the user did not") + void aliasRegistersTheTargetSystemWhenTheUserDidNot() { + TargetSystemManager manager = new TargetSystemManager(); + // An audit store built with from(targetSystem) does not require that system to be added to the + // builder, so this is the ordinary case, not an edge one. + manager.registerAuditStoreTargetSystem(new StubTransactionalTargetSystem("audit-db")); + + assertEquals("audit-db", + manager.getTargetSystem(PipelineHelper.AUDIT_STORE_TARGET_SYSTEM).getId()); + assertEquals(1, manager.getTransactionalTargetSystems().size()); + } + + @Test + @DisplayName("The alias does not override an instance the user already registered") + void aliasDoesNotOverrideAUserRegisteredInstance() { + TargetSystemManager manager = new TargetSystemManager(); + StubTransactionalTargetSystem userRegistered = new StubTransactionalTargetSystem("shared-id"); + StubTransactionalTargetSystem auditStoreBuiltFrom = new StubTransactionalTargetSystem("shared-id"); + manager.add(userRegistered); + manager.registerAuditStoreTargetSystem(auditStoreBuiltFrom); + + // Overwriting would silently change what the user's own changes execute against, so theirs wins + // and the mismatch is logged instead. + assertSame(userRegistered, manager.getTransactionalTargetSystems().get(0)); + assertEquals(1, manager.getTransactionalTargetSystems().size()); + } + + @Test + @DisplayName("A null external system fails with a message naming the audit store") + void aliasRejectsANullExternalSystem() { + TargetSystemManager manager = new TargetSystemManager(); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> manager.registerAuditStoreTargetSystem(null)); + + // A bare "Target system null not allowed" says nothing about where the null came from. Test + // doubles of an audit store return null here, so this message is the one someone will actually + // read when they hit it. + assertTrue(exception.getMessage().contains("audit store"), exception.getMessage()); + assertTrue(exception.getMessage().contains("getExternalSystem"), exception.getMessage()); + } + + @Test + @DisplayName("The alias rejects an external system that is not a target system") + void aliasRejectsANonTargetSystem() { + TargetSystemManager manager = new TargetSystemManager(); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> manager.registerAuditStoreTargetSystem(() -> "not-a-target-system")); + + assertTrue(exception.getMessage().contains("AbstractTargetSystem"), exception.getMessage()); + } + + @Test + @DisplayName("The alias cannot be registered once the manager is initialized") + void aliasCannotBeRegisteredAfterInitialize() { + TargetSystemManager manager = new TargetSystemManager(); + manager.initialize(new io.flamingock.internal.core.context.SimpleContext()); + + // Why the builder registers the alias before initialize(): registering later would also leave an + // otherwise-unregistered system uninitialized. + assertThrows(IllegalArgumentException.class, + () -> manager.registerAuditStoreTargetSystem(new StubTransactionalTargetSystem("audit-db"))); + } + + @Test + @DisplayName("An unknown id still fails, alias or not") + void unknownIdStillThrows() { + TargetSystemManager manager = new TargetSystemManager(); + manager.registerAuditStoreTargetSystem(new StubTransactionalTargetSystem("audit-db")); + + assertThrows(RuntimeException.class, () -> manager.getTargetSystem("nope")); + } + private static class StubNonTransactionalTargetSystem extends AbstractTargetSystem { StubNonTransactionalTargetSystem(String id) { super(id); } @Override protected StubNonTransactionalTargetSystem getSelf() { return this; } diff --git a/core/flamingock-test-support/build.gradle.kts b/core/flamingock-test-support/build.gradle.kts index 81f91ba5f..c5019e657 100644 --- a/core/flamingock-test-support/build.gradle.kts +++ b/core/flamingock-test-support/build.gradle.kts @@ -8,7 +8,7 @@ dependencies { // Add test utilities from the repository so tests can use InMemoryTestKit and pipeline helpers testImplementation(project(":utils:test-util")) - testImplementation(project(":core:target-systems:flamingock-nontransactional-targetsystem")) + api(project(":core:target-systems:flamingock-nontransactional-targetsystem")) implementation("org.mockito:mockito-inline:4.11.0") } diff --git a/core/flamingock-test-support/src/main/java/io/flamingock/support/inmemory/InMemoryAuditStore.java b/core/flamingock-test-support/src/main/java/io/flamingock/support/inmemory/InMemoryAuditStore.java index 148bb299e..eacc690bc 100644 --- a/core/flamingock-test-support/src/main/java/io/flamingock/support/inmemory/InMemoryAuditStore.java +++ b/core/flamingock-test-support/src/main/java/io/flamingock/support/inmemory/InMemoryAuditStore.java @@ -22,6 +22,8 @@ import io.flamingock.internal.util.Constants; import io.flamingock.internal.util.Result; import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.api.external.ExternalSystem; +import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; public class InMemoryAuditStore implements CommunityAuditStore { @@ -39,6 +41,25 @@ private InMemoryAuditStore() { this.lockStorage = new InMemoryLockStorage(); } + /** Lazily created stand-in; see {@link #getExternalSystem()}. */ + private NonTransactionalTargetSystem externalSystem; + + /** + * {@inheritDoc} + *

+ * This store has no external system of its own - it keeps everything in memory. A + * non-transactional stand-in is returned rather than {@code null} so that the reserved + * {@code AUDIT_STORE_TARGET_SYSTEM} id still resolves, letting system changes run against this store. + * Cached because the identity of the instance matters once it is registered. + */ + @Override + public synchronized ExternalSystem getExternalSystem() { + if (externalSystem == null) { + externalSystem = new NonTransactionalTargetSystem(getId()); + } + return externalSystem; + } + @Override public String getId() { return Constants.DEFAULT_IN_MEMORY_AUDIT_STORE; diff --git a/platform-plugins/flamingock-springboot-integration/build.gradle.kts b/platform-plugins/flamingock-springboot-integration/build.gradle.kts index 70cf0bb56..bad207d9d 100644 --- a/platform-plugins/flamingock-springboot-integration/build.gradle.kts +++ b/platform-plugins/flamingock-springboot-integration/build.gradle.kts @@ -3,6 +3,7 @@ val springBootVersion = "2.0.0.RELEASE" val springFrameworkVersion = "5.0.4.RELEASE" dependencies { api(project(":core:flamingock-core")) + testImplementation(project(":core:target-systems:flamingock-nontransactional-targetsystem")) compileOnlyApi("org.springframework:spring-context:${springFrameworkVersion}") compileOnlyApi("org.springframework.boot:spring-boot:${springBootVersion}") compileOnlyApi("org.springframework.boot:spring-boot-autoconfigure:${springBootVersion}") diff --git a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/FlamingockAutoConfigurationTests.java b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/FlamingockAutoConfigurationTests.java index d1ee778b0..ac491c0a5 100644 --- a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/FlamingockAutoConfigurationTests.java +++ b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/FlamingockAutoConfigurationTests.java @@ -28,6 +28,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; class FlamingockAutoConfigurationTests { @@ -115,7 +117,12 @@ public List targetSystems() { @Bean public CommunityAuditStore auditStore() { - return mock(CommunityAuditStore.class); + CommunityAuditStore auditStore = mock(CommunityAuditStore.class); + // The builder registers the audit store's own target system under a reserved id, so a store + // that supplies none fails the build. A bare mock returns null, hence the stub. + when(auditStore.getExternalSystem()) + .thenReturn(new NonTransactionalTargetSystem("test-audit-store")); + return auditStore; } } } diff --git a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java index 9d5a36fa4..a82f552cf 100644 --- a/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java +++ b/platform-plugins/flamingock-springboot-integration/src/test/java/io/flamingock/springboot/ValidationOnlyIntegrationTest.java @@ -41,6 +41,7 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; /** * Spring Boot end-to-end integration tests for the {@code validationOnly} mode. @@ -111,6 +112,11 @@ private static CommunityAuditStore buildAuditStoreMock(List auditHis CommunityAuditStore auditStore = mock(CommunityAuditStore.class); CommunityAuditPersistence persistence = mock(CommunityAuditPersistence.class); + + // The builder registers the audit store's own target system under a reserved id, so a store that + // supplies none fails the build. A bare mock returns null, hence the stub. + when(auditStore.getExternalSystem()) + .thenReturn(new NonTransactionalTargetSystem("test-audit-store")); CommunityLockService lockService = mock(CommunityLockService.class); // Stub audit history — determines whether changes are "pending" diff --git a/utils/test-util/build.gradle.kts b/utils/test-util/build.gradle.kts index 484e27af4..b11f70d7e 100644 --- a/utils/test-util/build.gradle.kts +++ b/utils/test-util/build.gradle.kts @@ -2,6 +2,7 @@ dependencies { api(project(":cloud:flamingock-cloud-api")) api(project(":core:flamingock-core")) + api(project(":core:target-systems:flamingock-nontransactional-targetsystem")) implementation(project(":core:flamingock-processor")) implementation("com.github.tomakehurst:wiremock-jre8:2.35.2") diff --git a/utils/test-util/src/main/java/io/flamingock/core/kit/inmemory/InternalInMemoryTestAuditStore.java b/utils/test-util/src/main/java/io/flamingock/core/kit/inmemory/InternalInMemoryTestAuditStore.java index ccf24d6f9..2f4483025 100644 --- a/utils/test-util/src/main/java/io/flamingock/core/kit/inmemory/InternalInMemoryTestAuditStore.java +++ b/utils/test-util/src/main/java/io/flamingock/core/kit/inmemory/InternalInMemoryTestAuditStore.java @@ -27,6 +27,8 @@ import io.flamingock.internal.util.Constants; import io.flamingock.internal.util.Result; import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.api.external.ExternalSystem; +import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; public class InternalInMemoryTestAuditStore implements CommunityAuditStore { @@ -50,6 +52,25 @@ public InternalInMemoryTestAuditStore(InternalInMemoryAuditStorage auditStorage, this.journalEventSequencerFactory = new JournalEventSequencerFactory(journalEventStore); } + /** Lazily created stand-in; see {@link #getExternalSystem()}. */ + private NonTransactionalTargetSystem externalSystem; + + /** + * {@inheritDoc} + *

+ * This store has no external system of its own - it keeps everything in memory. A + * non-transactional stand-in is returned rather than {@code null} so that the reserved + * {@code AUDIT_STORE_TARGET_SYSTEM} id still resolves, letting system changes run against this store. + * Cached because the identity of the instance matters once it is registered. + */ + @Override + public synchronized ExternalSystem getExternalSystem() { + if (externalSystem == null) { + externalSystem = new NonTransactionalTargetSystem(getId()); + } + return externalSystem; + } + @Override public String getId() { return Constants.DEFAULT_IN_MEMORY_AUDIT_STORE;