diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml index 4771ee89a..2423a86cf 100644 --- a/.github/workflows/build-master.yml +++ b/.github/workflows/build-master.yml @@ -1,7 +1,7 @@ name: Build Master on: push: - branches: [ master ] + branches: [ master, release/* ] jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91e9fb4d8..3fa7a32f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: steps: - name: Checkout project - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Java uses: graalvm/setup-graalvm@v1 @@ -19,3 +19,12 @@ jobs: run: | ./gradlew clean build \ -Psql.test.dialects=mysql + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports + path: | + **/build/reports/tests/** + **/build/test-results/** diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml index 097a543a9..574cc2c98 100644 --- a/.github/workflows/license-check.yml +++ b/.github/workflows/license-check.yml @@ -2,9 +2,9 @@ name: License Header Check on: pull_request: - branches: [ master, develop ] + branches: [ master, release/* ] push: - branches: [ master, develop ] + branches: [ master, release/* ] jobs: license-check: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f5ef6607..a283d79f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,19 +44,6 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} - flamingock-core-api: - needs: [ build ] - uses: ./.github/workflows/module-release-graalvm.yml - with: - module: flamingock-core-api - secrets: - FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} - FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} - FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} - FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} - FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} - FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} - flamingock-core-commons: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -383,19 +370,6 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} - general-util: - needs: [ build ] - uses: ./.github/workflows/module-release-graalvm.yml - with: - module: general-util - secrets: - FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} - FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} - FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} - FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} - FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} - FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} - test-util: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -474,6 +448,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + couchbase-test-kit: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: couchbase-test-kit + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + sql-util: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -487,6 +474,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + sql-test-kit: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: sql-test-kit + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + mongock-support: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -543,7 +543,6 @@ jobs: needs: [ flamingock-core, flamingock-core-commons, - flamingock-core-api, flamingock-processor, flamingock-graalvm, flamingock-cloud, @@ -565,14 +564,15 @@ jobs: dynamodb-target-system, couchbase-external-system-api, couchbase-target-system, - general-util, test-util, mongodb-util, mongodb-test-kit, dynamodb-util, dynamodb-test-kit, couchbase-util, + couchbase-test-kit, sql-util, + sql-test-kit, mongock-support, mongock-importer-mongodb, mongock-importer-dynamodb, diff --git a/.github/workflows/validate-commits.yml b/.github/workflows/validate-commits.yml index d66bad7cb..bc5659616 100644 --- a/.github/workflows/validate-commits.yml +++ b/.github/workflows/validate-commits.yml @@ -2,9 +2,9 @@ name: Validate Conventional Commits on: push: - branches: [develop, master] + branches: [master, release/*] pull_request: - branches: [develop, master] + branches: [master, release/*] jobs: commitlint: diff --git a/build.gradle.kts b/build.gradle.kts index 1ba14e12b..8606ac4da 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,9 +16,14 @@ plugins { allprojects { group = "io.flamingock" - version = "1.2.0-SNAPSHOT" + version = "1.2.0-beta.3" + + extra["templateApiVersion"] = "1.3.1" + extra["generalUtilVersion"] = "1.5.0" + extra["coreApiVersion"] = "1.3.0" repositories { + mavenLocal() mavenCentral() } } diff --git a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts index 3bb00f701..4d54bae90 100644 --- a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts +++ b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts @@ -68,7 +68,9 @@ val legacyProjects = setOf( val testKitsProjects = setOf( "mongodb-test-kit", - "dynamodb-test-kit" + "dynamodb-test-kit", + "sql-test-kit", + "couchbase-test-kit" ) val allProjects = coreProjects + cloudProjects + communityProjects + pluginProjects + targetSystemProjects + externalSystemProjects + utilProjects + legacyProjects + testKitsProjects diff --git a/cloud/flamingock-cloud/build.gradle.kts b/cloud/flamingock-cloud/build.gradle.kts index 53bf844e3..5b8c7ed6c 100644 --- a/cloud/flamingock-cloud/build.gradle.kts +++ b/cloud/flamingock-cloud/build.gradle.kts @@ -1,7 +1,8 @@ +val coreApiVersion: String by extra dependencies { // Core implementation(project(":core:flamingock-core")) - api(project(":core:flamingock-core-api")) + api("io.flamingock:flamingock-core-api:${coreApiVersion}") // target systems api(project(":core:target-systems:nontransactional-target-system")) api(project(":core:target-systems:couchbase-target-system")) diff --git a/community/flamingock-auditstore-couchbase/build.gradle.kts b/community/flamingock-auditstore-couchbase/build.gradle.kts index bcefcdd69..5d79e59d6 100644 --- a/community/flamingock-auditstore-couchbase/build.gradle.kts +++ b/community/flamingock-auditstore-couchbase/build.gradle.kts @@ -2,10 +2,12 @@ dependencies { api(project(":core:flamingock-core")) api(project(":core:target-systems:couchbase-external-system-api")) implementation(project(":utils:couchbase-util")) - + compileOnly("com.couchbase.client:java-client:3.6.0") testImplementation(project(":core:target-systems:couchbase-target-system")) + testImplementation(project(":utils:test-util")) + testImplementation(project(":utils:couchbase-test-kit")) testImplementation("org.testcontainers:testcontainers-couchbase:2.0.2") testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2") } @@ -20,4 +22,4 @@ java { configurations.testImplementation { extendsFrom(configurations.compileOnly.get()) -} \ No newline at end of file +} diff --git a/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/CouchbaseAuditStoreTest.java b/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/CouchbaseAuditStoreTest.java index 3736a90f9..c844a7084 100644 --- a/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/CouchbaseAuditStoreTest.java +++ b/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/CouchbaseAuditStoreTest.java @@ -20,21 +20,13 @@ import com.couchbase.client.java.Cluster; import com.couchbase.client.java.Collection; import com.couchbase.client.java.json.JsonObject; -import io.flamingock.store.couchbase.changes.failedWithoutRollback._001__create_index; -import io.flamingock.store.couchbase.changes.failedWithoutRollback._002__insert_document; -import io.flamingock.store.couchbase.changes.failedWithoutRollback._003__execution_with_exception; -import io.flamingock.store.couchbase.changes.happyPath._003__insert_another_document; +import io.flamingock.common.test.pipeline.CodeChangeTestDefinition; +import io.flamingock.core.kit.audit.AuditTestSupport; +import io.flamingock.couchbase.kit.CouchbaseTestKit; import io.flamingock.targetsystem.couchbase.CouchbaseTargetSystem; -import io.flamingock.internal.common.core.util.Deserializer; -import io.flamingock.internal.common.core.audit.AuditEntry; import io.flamingock.internal.common.couchbase.CouchbaseCollectionHelper; -import io.flamingock.internal.core.builder.FlamingockFactory; -import io.flamingock.internal.util.constants.CommunityPersistenceConstants; import io.flamingock.internal.core.operation.OperationException; -import io.flamingock.internal.util.Trio; import org.junit.jupiter.api.*; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import org.testcontainers.couchbase.BucketDefinition; import org.testcontainers.couchbase.CouchbaseContainer; import org.testcontainers.junit.jupiter.Container; @@ -42,8 +34,8 @@ import java.time.Duration; import java.util.Collections; -import java.util.List; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.*; import static org.junit.jupiter.api.Assertions.*; @Testcontainers @@ -53,6 +45,9 @@ class CouchbaseAuditStoreTest { private static Cluster cluster; private static CouchbaseTestHelper couchbaseTestHelper; + private CouchbaseTargetSystem couchbaseTargetSystem; + private CouchbaseAuditStore couchbaseAuditStore; + private CouchbaseTestKit testKit; @Container public static final CouchbaseContainer couchbaseContainer = new CouchbaseContainer("couchbase/server:7.2.4") @@ -71,56 +66,46 @@ static void beforeAll() { @BeforeEach void setupEach() { + couchbaseTargetSystem = new CouchbaseTargetSystem("couchbase", cluster, BUCKET_NAME); + couchbaseAuditStore = CouchbaseAuditStore.from(couchbaseTargetSystem); + testKit = CouchbaseTestKit.create(couchbaseAuditStore, cluster, BUCKET_NAME, CollectionIdentifier.DEFAULT_SCOPE); } @AfterEach void tearDownEach() { - CouchbaseCollectionHelper.deleteAllDocuments(cluster, BUCKET_NAME, CollectionIdentifier.DEFAULT_SCOPE, CollectionIdentifier.DEFAULT_COLLECTION); - CouchbaseCollectionHelper.deleteAllDocuments(cluster, BUCKET_NAME, CollectionIdentifier.DEFAULT_SCOPE, CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME); - CouchbaseCollectionHelper.dropIndexIfExists(cluster, BUCKET_NAME, CollectionIdentifier.DEFAULT_SCOPE, CollectionIdentifier.DEFAULT_COLLECTION, "idx_standalone_index"); + testKit.cleanUp(); } @Test @DisplayName("When standalone runs the AuditStore should persist the audit logs and the test data") void happyPath() { - //Given-When Bucket bucket = cluster.bucket(BUCKET_NAME); Collection testCollection = bucket.defaultCollection(); - CouchbaseTargetSystem couchbaseTargetSystem = new CouchbaseTargetSystem("couchbase", cluster, BUCKET_NAME); - - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - mocked.when(Deserializer::readMetadataFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( - new Trio<>(io.flamingock.store.couchbase.changes.happyPath._001__create_index.class, Collections.singletonList(Collection.class)), - new Trio<>(io.flamingock.store.couchbase.changes.happyPath._002__insert_document.class, Collections.singletonList(Collection.class)), - new Trio<>(_003__insert_another_document.class, Collections.singletonList(Collection.class))) - ); - - FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(couchbaseTargetSystem)) - .addTargetSystem(couchbaseTargetSystem) - .addDependency(testCollection) // for test purpose only - .build() - .run(); - } - - //Then - //Checking auditLog - Collection auditLogCollection = bucket.collection(CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME); - List auditLog = couchbaseTestHelper.getAuditEntriesSorted(auditLogCollection); - assertEquals(6, auditLog.size()); - assertEquals("create-index", auditLog.get(0).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(0).getState()); - assertEquals("create-index", auditLog.get(1).getTaskId()); - assertEquals(AuditEntry.Status.APPLIED, auditLog.get(1).getState()); - assertEquals("insert-document", auditLog.get(2).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(2).getState()); - assertEquals("insert-document", auditLog.get(3).getTaskId()); - assertEquals(AuditEntry.Status.APPLIED, auditLog.get(3).getState()); - assertEquals("insert-another-document", auditLog.get(4).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(4).getState()); - assertEquals("insert-another-document", auditLog.get(5).getTaskId()); - assertEquals(AuditEntry.Status.APPLIED, auditLog.get(5).getState()); + + String[] expectedTaskIds = {"create-index", "insert-document", "insert-another-document"}; + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.happyPath._001__create_index.class, Collections.singletonList(Collection.class)), + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.happyPath._002__insert_document.class, Collections.singletonList(Collection.class)), + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.happyPath._003__insert_another_document.class, Collections.singletonList(Collection.class)) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(couchbaseAuditStore) + .addTargetSystem(couchbaseTargetSystem) + .addDependency(testCollection) // for test purpose only + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED(expectedTaskIds[0]), + APPLIED(expectedTaskIds[0]), + STARTED(expectedTaskIds[1]), + APPLIED(expectedTaskIds[1]), + STARTED(expectedTaskIds[2]), + APPLIED(expectedTaskIds[2]) + ) + .run(); //Checking created index and documents assertTrue(CouchbaseCollectionHelper.indexExists(cluster, testCollection.bucketName(), testCollection.scopeName(), testCollection.name(), "idx_standalone_index")); @@ -137,47 +122,37 @@ void happyPath() { @Test @DisplayName("When standalone runs the AuditStore and execution fails (with rollback method) should persist all the audit logs up to the failed one (ROLLED_BACK)") void failedWithRollback() { - //Given-When Bucket bucket = cluster.bucket(BUCKET_NAME); Collection testCollection = bucket.defaultCollection(); - CouchbaseTargetSystem couchbaseTargetSystem = new CouchbaseTargetSystem("couchbase", cluster, BUCKET_NAME); - - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - mocked.when(Deserializer::readMetadataFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( - new Trio<>(io.flamingock.store.couchbase.changes.failedWithRollback._001__create_index.class, Collections.singletonList(Collection.class)), - new Trio<>(io.flamingock.store.couchbase.changes.failedWithRollback._002__insert_document.class, Collections.singletonList(Collection.class)), - new Trio<>(io.flamingock.store.couchbase.changes.failedWithRollback._003__execution_with_exception.class, Collections.singletonList(Collection.class), Collections.singletonList(Collection.class))) - ); - - assertThrows(OperationException.class, () -> { - FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(couchbaseTargetSystem)) + + String[] expectedTaskIds = {"create-index", "insert-document", "execution-with-exception"}; + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.failedWithRollback._001__create_index.class, Collections.singletonList(Collection.class)), + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.failedWithRollback._002__insert_document.class, Collections.singletonList(Collection.class)), + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.failedWithRollback._003__execution_with_exception.class, Collections.singletonList(Collection.class), Collections.singletonList(Collection.class)) + ) + .WHEN(() -> { + assertThrows(OperationException.class, () -> { + testKit.createBuilder() + .setAuditStore(couchbaseAuditStore) .addTargetSystem(couchbaseTargetSystem) .addDependency(testCollection) // for test purpose only .build() .run(); - }); - } - - //Then - //Checking auditLog - Collection auditLogCollection = bucket.collection(CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME); - List auditLog = couchbaseTestHelper.getAuditEntriesSorted(auditLogCollection); - assertEquals(7, auditLog.size()); - assertEquals("create-index", auditLog.get(0).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(0).getState()); - assertEquals("create-index", auditLog.get(1).getTaskId()); - assertEquals(AuditEntry.Status.APPLIED, auditLog.get(1).getState()); - assertEquals("insert-document", auditLog.get(2).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(2).getState()); - assertEquals("insert-document", auditLog.get(3).getTaskId()); - assertEquals(AuditEntry.Status.APPLIED, auditLog.get(3).getState()); - assertEquals("execution-with-exception", auditLog.get(4).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(4).getState()); - assertEquals("execution-with-exception", auditLog.get(5).getTaskId()); - assertEquals(AuditEntry.Status.FAILED, auditLog.get(5).getState()); - assertEquals("execution-with-exception", auditLog.get(6).getTaskId()); - assertEquals(AuditEntry.Status.ROLLED_BACK, auditLog.get(6).getState()); + }); + }) + .THEN_VerifyAuditSequenceStrict( + STARTED(expectedTaskIds[0]), + APPLIED(expectedTaskIds[0]), + STARTED(expectedTaskIds[1]), + APPLIED(expectedTaskIds[1]), + STARTED(expectedTaskIds[2]), + FAILED(expectedTaskIds[2]), + ROLLED_BACK(expectedTaskIds[2]) + ) + .run(); //Checking created index and documents assertTrue(CouchbaseCollectionHelper.indexExists(cluster, testCollection.bucketName(), testCollection.scopeName(), testCollection.name(), "idx_standalone_index")); @@ -191,47 +166,37 @@ void failedWithRollback() { @Test @DisplayName("When standalone runs the AuditStore and execution fails (without rollback method) should persist all the audit logs up to the failed one (FAILED)") void failedWithoutRollback() { - //Given-When Bucket bucket = cluster.bucket(BUCKET_NAME); Collection testCollection = bucket.defaultCollection(); - CouchbaseTargetSystem couchbaseTargetSystem = new CouchbaseTargetSystem("couchbase", cluster, BUCKET_NAME); - - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - mocked.when(Deserializer::readMetadataFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( - new Trio<>(_001__create_index.class, Collections.singletonList(Collection.class)), - new Trio<>(_002__insert_document.class, Collections.singletonList(Collection.class)), - new Trio<>(_003__execution_with_exception.class, Collections.singletonList(Collection.class))) - ); - - assertThrows(OperationException.class, () -> { - FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(couchbaseTargetSystem)) + + String[] expectedTaskIds = {"create-index", "insert-document", "execution-with-exception"}; + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.failedWithoutRollback._001__create_index.class, Collections.singletonList(Collection.class)), + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.failedWithoutRollback._002__insert_document.class, Collections.singletonList(Collection.class)), + new CodeChangeTestDefinition(io.flamingock.store.couchbase.changes.failedWithoutRollback._003__execution_with_exception.class, Collections.singletonList(Collection.class)) + ) + .WHEN(() -> { + assertThrows(OperationException.class, () -> { + testKit.createBuilder() + .setAuditStore(couchbaseAuditStore) .addTargetSystem(couchbaseTargetSystem) .addDependency(testCollection) // for test purpose only .build() .run(); - }); - } - - //Then - //Checking auditLog - Collection auditLogCollection = bucket.collection(CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME); - List auditLog = couchbaseTestHelper.getAuditEntriesSorted(auditLogCollection); - assertEquals(7, auditLog.size()); - assertEquals("create-index", auditLog.get(0).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(0).getState()); - assertEquals("create-index", auditLog.get(1).getTaskId()); - assertEquals(AuditEntry.Status.APPLIED, auditLog.get(1).getState()); - assertEquals("insert-document", auditLog.get(2).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(2).getState()); - assertEquals("insert-document", auditLog.get(3).getTaskId()); - assertEquals(AuditEntry.Status.APPLIED, auditLog.get(3).getState()); - assertEquals("execution-with-exception", auditLog.get(4).getTaskId()); - assertEquals(AuditEntry.Status.STARTED, auditLog.get(4).getState()); - assertEquals("execution-with-exception", auditLog.get(5).getTaskId()); - assertEquals(AuditEntry.Status.FAILED, auditLog.get(5).getState()); - assertEquals("execution-with-exception", auditLog.get(6).getTaskId()); - assertEquals(AuditEntry.Status.ROLLED_BACK, auditLog.get(6).getState()); + }); + }) + .THEN_VerifyAuditSequenceStrict( + STARTED(expectedTaskIds[0]), + APPLIED(expectedTaskIds[0]), + STARTED(expectedTaskIds[1]), + APPLIED(expectedTaskIds[1]), + STARTED(expectedTaskIds[2]), + FAILED(expectedTaskIds[2]), + ROLLED_BACK(expectedTaskIds[2]) + ) + .run(); //Checking created index and documents assertTrue(CouchbaseCollectionHelper.indexExists(cluster, testCollection.bucketName(), testCollection.scopeName(), testCollection.name(), "idx_standalone_index")); diff --git a/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java b/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java index cea55e751..7b3a4c76a 100644 --- a/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java +++ b/community/flamingock-auditstore-couchbase/src/test/java/io/flamingock/store/couchbase/PipelineTestHelper.java @@ -87,6 +87,7 @@ public static FlamingockMetadata getPreviewPipeline(String stageName, Trio changeInfo.getOrder(), changeInfo.getAuthor(), trio.getFirst().getName(), + null, PreviewConstructor.getDefault(), new PreviewMethod("apply", getParameterTypes(trio.getSecond())), rollback, diff --git a/community/flamingock-auditstore-dynamodb/src/test/java/io/flamingock/store/dynamodb/internal/entities/AuditEntryEntityTest.java b/community/flamingock-auditstore-dynamodb/src/test/java/io/flamingock/store/dynamodb/internal/entities/AuditEntryEntityTest.java index 9a08f349f..fce2c2520 100644 --- a/community/flamingock-auditstore-dynamodb/src/test/java/io/flamingock/store/dynamodb/internal/entities/AuditEntryEntityTest.java +++ b/community/flamingock-auditstore-dynamodb/src/test/java/io/flamingock/store/dynamodb/internal/entities/AuditEntryEntityTest.java @@ -58,6 +58,7 @@ void shouldConvertToAndFromAuditEntryWithTxType() { assertEquals(original.getTaskId(), converted.getTaskId()); assertEquals(original.getAuthor(), converted.getAuthor()); assertEquals(original.getState(), converted.getState()); + assertEquals(original.getSourceFile(), converted.getSourceFile()); } @Test @@ -129,6 +130,7 @@ void shouldConvertToAndFromAuditEntryWithTargetSystemId() { assertEquals(original.getTaskId(), converted.getTaskId()); assertEquals(original.getAuthor(), converted.getAuthor()); assertEquals(original.getState(), converted.getState()); + assertEquals(original.getSourceFile(), converted.getSourceFile()); } @Test diff --git a/community/flamingock-auditstore-sql/build.gradle.kts b/community/flamingock-auditstore-sql/build.gradle.kts index 4b1c51ae4..e0d0ff827 100644 --- a/community/flamingock-auditstore-sql/build.gradle.kts +++ b/community/flamingock-auditstore-sql/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { testImplementation("org.testcontainers:testcontainers-postgresql:2.0.2") testImplementation("org.testcontainers:testcontainers-mariadb:2.0.2") testImplementation(project(":utils:test-util")) + testImplementation(project(":utils:sql-test-kit")) testImplementation("com.zaxxer:HikariCP:3.4.5") testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2") testImplementation("com.h2database:h2:2.2.224") diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlAuditor.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlAuditor.java index ed716c199..e709b2d0b 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlAuditor.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlAuditor.java @@ -19,6 +19,7 @@ import io.flamingock.internal.common.core.audit.AuditReader; import io.flamingock.internal.common.core.audit.AuditTxType; import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.dialectHelpers.SqlAuditorDialectHelper; import io.flamingock.internal.core.external.store.audit.LifecycleAuditWriter; import io.flamingock.internal.util.Result; diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlLockService.java b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlLockService.java index 52ce8a36c..ce50c266d 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlLockService.java +++ b/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlLockService.java @@ -16,6 +16,7 @@ package io.flamingock.store.sql.internal; import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.dialectHelpers.SqlLockDialectHelper; import io.flamingock.internal.core.external.store.lock.LockAcquisition; import io.flamingock.internal.core.external.store.lock.LockKey; import io.flamingock.internal.core.external.store.lock.LockServiceException; @@ -285,6 +286,6 @@ private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLE private void upsertLockEntry(Connection conn, String key, String owner, LocalDateTime expiresAt) throws SQLException { - dialectHelper.upsertLockEntry(conn, lockRepositoryName, key, owner, expiresAt); + dialectHelper.upsertLockEntry(conn, lockRepositoryName, key, owner, LockStatus.LOCK_HELD.name(), expiresAt); } } diff --git a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/store/sql/PipelineTestHelper.java b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/store/sql/PipelineTestHelper.java index 6ea16f64c..8a7f058ab 100644 --- a/community/flamingock-auditstore-sql/src/test/java/io/flamingock/store/sql/PipelineTestHelper.java +++ b/community/flamingock-auditstore-sql/src/test/java/io/flamingock/store/sql/PipelineTestHelper.java @@ -87,6 +87,7 @@ public static FlamingockMetadata getPreviewPipeline(String stageName, Trio[] getChangeClasses(String dialectName, String scenario) { @DisplayName("When standalone runs the AuditStore should persist the audit logs and the test data") void happyPathWithMockedPipeline(SqlDialect sqlDialect, String dialectName) throws Exception { context = setupTest(sqlDialect, dialectName); - - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - Class[] changeClasses = getChangeClasses(dialectName, "happyPath"); - - mocked.when(Deserializer::readMetadataFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( - new Trio<>(changeClasses[0], Collections.singletonList(Connection.class), null), - new Trio<>(changeClasses[1], Collections.singletonList(Connection.class), null), - new Trio<>(changeClasses[2], Collections.singletonList(Connection.class), null) - )); - - SqlTargetSystem targetSystem = new SqlTargetSystem("sql", context.dataSource); - SqlAuditStore auditStore = SqlAuditStore.from(targetSystem); - - FlamingockFactory.getCommunityBuilder() - .setAuditStore(auditStore) - .addTargetSystem(targetSystem) - .build() - .run(); - } - - // Verify audit logs - try (Connection conn = context.dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement( - "SELECT change_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); - ResultSet rs = ps.executeQuery()) { - - String[] expectedTaskIds = {"create-index", "insert-document", "insert-another-document"}; - int recordCount = 0; - int startedCount = 0; - int appliedCount = 0; - - while (rs.next()) { - String taskId = rs.getString("change_id"); - String state = rs.getString("state"); - assertTrue( - java.util.Arrays.asList(expectedTaskIds).contains(taskId), - "Unexpected change_id: " + taskId - ); - assertTrue( - state.equals("STARTED") || state.equals("APPLIED"), - "Unexpected state: " + state - ); - if (state.equals("STARTED")) startedCount++; - if (state.equals("APPLIED")) appliedCount++; - recordCount++; - } - - assertEquals(6, recordCount, "Audit log should have 6 records"); - assertEquals(3, startedCount, "Should have 3 STARTED records"); - assertEquals(3, appliedCount, "Should have 3 APPLIED records"); - } - - // Verify test data - try (Connection conn = context.dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { - ps.setString(1, "test-client-Federico"); - try (ResultSet rs = ps.executeQuery()) { - assertTrue(rs.next()); - assertEquals("Federico", rs.getString("name")); - } - ps.setString(1, "test-client-Jorge"); - try (ResultSet rs = ps.executeQuery()) { - assertTrue(rs.next()); - assertEquals("Jorge", rs.getString("name")); - } - } - + SqlTargetSystem sqlTargetSystem = new SqlTargetSystem("sql", context.dataSource); + SqlAuditStore sqlAuditStore = SqlAuditStore.from(sqlTargetSystem); + TestKit testKit = SqlTestKit.create(sqlAuditStore, context.dataSource); + + Class[] changeClasses = getChangeClasses(dialectName, "happyPath"); + String[] expectedTaskIds = {"create-index", "insert-document", "insert-another-document"}; + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(changeClasses[0], Collections.singletonList(Connection.class)), + new CodeChangeTestDefinition(changeClasses[1], Collections.singletonList(Connection.class)), + new CodeChangeTestDefinition(changeClasses[2], Collections.singletonList(Connection.class)) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(sqlAuditStore) + .addTargetSystem(sqlTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED(expectedTaskIds[0]), + APPLIED(expectedTaskIds[0]), + STARTED(expectedTaskIds[1]), + APPLIED(expectedTaskIds[1]), + STARTED(expectedTaskIds[2]), + APPLIED(expectedTaskIds[2]) + ) + .run(); + + // Verify index exists and data state + SqlAuditTestHelper.verifyIndexExists(context); + verifyDataState(context, false); } @ParameterizedTest @@ -356,86 +319,40 @@ void happyPathWithMockedPipeline(SqlDialect sqlDialect, String dialectName) thro @DisplayName("When standalone runs the AuditStore and execution fails (with rollback method) should persist all the audit logs up to the failed one (ROLLED_BACK)") void failedWithRollback(SqlDialect sqlDialect, String dialectName) throws Exception { context = setupTest(sqlDialect, dialectName); - - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - Class[] changeClasses = getChangeClasses(dialectName, "failedWithRollback"); - - mocked.when(Deserializer::readMetadataFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( - new Trio<>(changeClasses[0], Collections.singletonList(Connection.class), null), - new Trio<>(changeClasses[1], Collections.singletonList(Connection.class), Collections.singletonList(Connection.class)), - new Trio<>(changeClasses[2], Collections.singletonList(Connection.class), Collections.singletonList(Connection.class)) - )); - - SqlTargetSystem targetSystem = new SqlTargetSystem("sql", context.dataSource); - SqlAuditStore auditStore = SqlAuditStore.from(targetSystem); - - assertThrows(OperationException.class, () -> { - FlamingockFactory.getCommunityBuilder() - .setAuditStore(auditStore) - .addTargetSystem(targetSystem) - .build() - .run(); - }); - - // Verify audit sequence - try (Connection conn = context.dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement( - "SELECT change_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); - ResultSet rs = ps.executeQuery()) { - - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("change_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("change_id")); - assertEquals("APPLIED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("change_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("change_id")); - assertEquals("APPLIED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("change_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("change_id")); - assertEquals("FAILED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("change_id")); - assertEquals("ROLLED_BACK", rs.getString("state")); - - assertFalse(rs.next()); - } - - // Verify index exists - SqlAuditTestHelper.verifyIndexExists(context); - - // Verify partial data - try (Connection conn = context.dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { - ps.setString(1, "test-client-Federico"); - try (ResultSet rs = ps.executeQuery()) { - assertTrue(rs.next()); - assertEquals("Federico", rs.getString("name")); - } - } - - try (Connection conn = context.dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { - ps.setString(1, "test-client-Jorge"); - try (ResultSet rs = ps.executeQuery()) { - assertFalse(rs.next()); - } - } - } - + SqlTargetSystem sqlTargetSystem = new SqlTargetSystem("sql", context.dataSource); + SqlAuditStore sqlAuditStore = SqlAuditStore.from(sqlTargetSystem); + TestKit testKit = SqlTestKit.create(sqlAuditStore, context.dataSource); + + Class[] changeClasses = getChangeClasses(dialectName, "failedWithRollback"); + String[] expectedTaskIds = {"create-index", "insert-document", "execution-with-exception"}; + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(changeClasses[0], Collections.singletonList(Connection.class), null), + new CodeChangeTestDefinition(changeClasses[1], Collections.singletonList(Connection.class), Collections.singletonList(Connection.class)), + new CodeChangeTestDefinition(changeClasses[2], Collections.singletonList(Connection.class), Collections.singletonList(Connection.class)) + ) + .WHEN(() -> assertThrows(OperationException.class, () -> { + testKit.createBuilder() + .setAuditStore(sqlAuditStore) + .addTargetSystem(sqlTargetSystem) + .build() + .run(); + })) + .THEN_VerifyAuditSequenceStrict( + STARTED(expectedTaskIds[0]), + APPLIED(expectedTaskIds[0]), + STARTED(expectedTaskIds[1]), + APPLIED(expectedTaskIds[1]), + STARTED(expectedTaskIds[2]), + FAILED(expectedTaskIds[2]), + ROLLED_BACK(expectedTaskIds[2]) + ) + .run(); + + // Verify index exists and data state + SqlAuditTestHelper.verifyIndexExists(context); + verifyDataState(context, true); } @ParameterizedTest @@ -443,72 +360,43 @@ void failedWithRollback(SqlDialect sqlDialect, String dialectName) throws Except @DisplayName("When standalone runs the AuditStore and execution fails (without rollback method) should persist all the audit logs up to the failed one (FAILED)") void failedWithoutRollback(SqlDialect sqlDialect, String dialectName) throws Exception { context = setupTest(sqlDialect, dialectName); - - try (MockedStatic mocked = Mockito.mockStatic(Deserializer.class)) { - Class[] changeClasses = getChangeClasses(dialectName, "failedWithoutRollback"); - - mocked.when(Deserializer::readMetadataFromFile).thenReturn(PipelineTestHelper.getPreviewPipeline( - new Trio<>(changeClasses[0], Collections.singletonList(Connection.class), null), - new Trio<>(changeClasses[1], Collections.singletonList(Connection.class), null), - new Trio<>(changeClasses[2], Collections.singletonList(Connection.class), null) - )); - - SqlTargetSystem targetSystem = new SqlTargetSystem("sql", context.dataSource); - SqlAuditStore auditStore = SqlAuditStore.from(targetSystem); - - assertThrows(OperationException.class, () -> { - FlamingockFactory.getCommunityBuilder() - .setAuditStore(auditStore) - .addTargetSystem(targetSystem) - .build() - .run(); - }); - - // Verify audit sequence - try (Connection conn = context.dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement( - "SELECT change_id, state FROM " + CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME + " ORDER BY id ASC"); - ResultSet rs = ps.executeQuery()) { - - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("change_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("create-index", rs.getString("change_id")); - assertEquals("APPLIED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("change_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("insert-document", rs.getString("change_id")); - assertEquals("APPLIED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("change_id")); - assertEquals("STARTED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("change_id")); - assertEquals("FAILED", rs.getString("state")); - - assertTrue(rs.next()); - assertEquals("execution-with-exception", rs.getString("change_id")); - assertEquals("ROLLED_BACK", rs.getString("state")); - - assertFalse(rs.next()); - } - - // Verify index exists and data state - SqlAuditTestHelper.verifyIndexExists(context); - verifyPartialDataState(context); - } - + SqlTargetSystem sqlTargetSystem = new SqlTargetSystem("sql", context.dataSource); + SqlAuditStore sqlAuditStore = SqlAuditStore.from(sqlTargetSystem); + TestKit testKit = SqlTestKit.create(sqlAuditStore, context.dataSource); + + Class[] changeClasses = getChangeClasses(dialectName, "failedWithoutRollback"); + String[] expectedTaskIds = {"create-index", "insert-document", "execution-with-exception"}; + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(changeClasses[0], Collections.singletonList(Connection.class), null), + new CodeChangeTestDefinition(changeClasses[1], Collections.singletonList(Connection.class), null), + new CodeChangeTestDefinition(changeClasses[2], Collections.singletonList(Connection.class), null) + ) + .WHEN(() -> assertThrows(OperationException.class, () -> { + testKit.createBuilder() + .setAuditStore(sqlAuditStore) + .addTargetSystem(sqlTargetSystem) + .build() + .run(); + })) + .THEN_VerifyAuditSequenceStrict( + STARTED(expectedTaskIds[0]), + APPLIED(expectedTaskIds[0]), + STARTED(expectedTaskIds[1]), + APPLIED(expectedTaskIds[1]), + STARTED(expectedTaskIds[2]), + FAILED(expectedTaskIds[2]), + ROLLED_BACK(expectedTaskIds[2]) + ) + .run(); + + // Verify index exists and data state + SqlAuditTestHelper.verifyIndexExists(context); + verifyDataState(context, true); } - private void verifyPartialDataState(TestContext context) throws SQLException { + private void verifyDataState(TestContext context, Boolean partial) throws SQLException { try (Connection conn = context.dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { ps.setString(1, "test-client-Federico"); @@ -522,7 +410,12 @@ private void verifyPartialDataState(TestContext context) throws SQLException { PreparedStatement ps = conn.prepareStatement("SELECT name FROM test_table WHERE id = ?")) { ps.setString(1, "test-client-Jorge"); try (ResultSet rs = ps.executeQuery()) { - assertFalse(rs.next()); + if (partial) { + assertFalse(rs.next()); + } else { + assertTrue(rs.next()); + assertEquals("Jorge", rs.getString("name")); + } } } } diff --git a/community/flamingock-community/build.gradle.kts b/community/flamingock-community/build.gradle.kts index de7b1aa50..8bf75ed16 100644 --- a/community/flamingock-community/build.gradle.kts +++ b/community/flamingock-community/build.gradle.kts @@ -1,7 +1,8 @@ +val coreApiVersion: String by extra dependencies { // Core api(project(":core:flamingock-core")) - api(project(":core:flamingock-core-api")) + api("io.flamingock:flamingock-core-api:${coreApiVersion}") // target systems api(project(":core:target-systems:nontransactional-target-system")) api(project(":core:target-systems:couchbase-target-system")) diff --git a/core/flamingock-core-api/build.gradle.kts b/core/flamingock-core-api/build.gradle.kts deleted file mode 100644 index d4fc576a2..000000000 --- a/core/flamingock-core-api/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -val jacksonVersion = "2.16.0" -dependencies { - api(project(":core:flamingock-template-api")) - implementation(project(":utils:general-util")) - api("jakarta.annotation:jakarta.annotation-api:2.1.1")//todo can this be implementation? - - implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") -} - -description = "Public API annotations and interfaces for defining changes, stages, and templates" - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } -} \ No newline at end of file diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/NonLockGuardedType.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/NonLockGuardedType.java deleted file mode 100644 index d630174fe..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/NonLockGuardedType.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api; - -public enum NonLockGuardedType { - /** - * Indicates the returned object shouldn't be decorated for lock guard. So clean instance is returned. - * But still the method needs to bbe lock-guarded - */ - RETURN, - - /** - * Indicates the method shouldn't be lock-guarded, but still should decorate the returned object(if applies) - */ - METHOD, - - /** - * Indicates the method shouldn't be lock-guarded neither the returned object should be decorated for lock guard. - */ - NONE -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/RecoveryStrategy.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/RecoveryStrategy.java deleted file mode 100644 index e2569c8b3..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/RecoveryStrategy.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api; - -/** - * Defines how Flamingock handles change execution failures. - * Determines whether failed changes should be automatically retried - * or require manual intervention before proceeding. - * - * @see io.flamingock.api.annotations.Recovery - * @see io.flamingock.api.annotations.Change - */ -public enum RecoveryStrategy { - /** - * Automatically retry the change on subsequent runs until successful. - * Use for idempotent operations with transient failure modes. - */ - ALWAYS_RETRY, - - /** - * Require manual intervention before retrying. - * Use for critical operations where failures need investigation. - */ - MANUAL_INTERVENTION; - - /** - * Checks if this strategy allows automatic retries. - * - * @return {@code true} if automatic retry is enabled - */ - public boolean isAlwaysRetry() { - return this == ALWAYS_RETRY; - } -} \ No newline at end of file diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/StageType.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/StageType.java deleted file mode 100644 index bf64b6039..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/StageType.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api; - -public enum StageType { - DEFAULT, - LEGACY("legacy"), - SYSTEM("importer"); - - private final String alias; - - StageType(String alias) { - this.alias = alias; - } - - StageType() { - this.alias = null; - } - - public static StageType from(String name) { - if (name == null || name.isEmpty()) { - return DEFAULT; - } - for (StageType stageType : StageType.values()) { - if (name.equals(stageType.alias)) { - return stageType; - } - } - throw new IllegalArgumentException("No such stage type: " + name); - } -} \ No newline at end of file diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Apply.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Apply.java deleted file mode 100644 index 07d15fd55..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Apply.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks the method that applies a change to the target system. - * This method contains the forward migration logic that evolves your system state. - * - *

The method can accept dependency-injected parameters from the Flamingock context, - * including database connections, repositories, and custom dependencies. - * - *

Example usage: - *

{@code
- * @Change(id = "add-user-email-index", order = "2024-11-15-002", author = "team@example.com")
- * public class AddUserEmailIndex {
- *
- *     @Apply
- *     public void addIndex(MongoDatabase database) {
- *         database.getCollection("users")
- *                 .createIndex(Indexes.ascending("email"));
- *     }
- *
- *     @Rollback
- *     public void removeIndex(MongoDatabase database) {
- *         database.getCollection("users")
- *                 .dropIndex("email_1");
- *     }
- * }
- * }
- * - * @see Change - * @see Rollback - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Apply { - -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Change.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Change.java deleted file mode 100644 index 7c511ea1e..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Change.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -/** - * Marks a class as a change that encapsulates a system evolution operation. - * Each change represents an atomic, versioned modification to your distributed system. - * - *

Example usage: - *

{@code
- * @Change(id = "create-user-index", order = "2024-11-15-001", author = "john.doe")
- * public class CreateUserIndexChange {
- *     @Apply
- *     public void createIndex(MongoDatabase db) {
- *         // Implementation
- *     }
- * }
- * }
- * - * @see Apply - * @see Rollback - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface Change { - - /** - * Unique identifier for this change. Must be globally unique across all changes. - * Typically, follows a kebab-case naming convention describing the operation. - * - * @return the unique change identifier - */ - String id(); - -// /** -// * Execution order for this change. Changes are applied in lexicographical order. -// * Minimum 4 characters required. Recommended format: date-based with index (e.g., "2024-05-19-001"). -// * This format provides optimal sorting, clarity, and sequential indexing within the same day. -// * Alternative formats like zero-padded numbers ("0001", "0002") are also supported. -// * -// * @return the execution order string -// */ -// String order() default "NULL_VALUE"; - - /** - * Author of this change. Required for audit trail and accountability. - * Typically, an email, username, or team identifier. - * - * @return the change author identifier - */ - String author(); - - /** - * Whether this change should run within a transaction if supported by the target system. - * Set to {@code false} for operations that cannot be transactional (e.g., DDL in some databases). - * - * @return {@code true} if transactional execution is required, {@code false} otherwise - */ - boolean transactional() default true; - -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java deleted file mode 100644 index f10058051..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/EnableFlamingock.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Core annotation for configuring Flamingock pipeline execution. - * This annotation must be placed on a class to enable Flamingock processing and define - * how the pipeline should be configured. - * - *

Pipeline Configuration

- * - * The annotation supports two mutually exclusive pipeline configuration modes: - * - *

1. File-based Configuration

- * Use {@link #configFile()} to reference a YAML pipeline definition: - *
- * @EnableFlamingock(configFile = "config/pipeline.yaml")
- * public class MyMigrationConfig {
- *     // Configuration class
- * }
- * 
- * - *

2. Annotation-based Configuration

- * Use {@link #stages()} to define the pipeline inline: - *
- * @EnableFlamingock(
- *     stages = {
- *         @Stage(type = StageType.SYSTEM, location = "com.example.system"),
- *         @Stage(type = StageType.LEGACY, location = "com.example.init"),
- *         @Stage(location = "com.example.migrations")
- *     }
- * )
- * public class MyMigrationConfig {
- *     // Configuration class
- * }
- * 
- * - *

Validation Rules

- *
    - *
  • Either {@link #configFile()} OR {@link #stages()} must be specified (mutually exclusive)
  • - *
  • At least one configuration mode must be provided
  • - *
  • Maximum of 1 stage with type {@code StageType.SYSTEM} is allowed
  • - *
  • Maximum of 1 stage with type {@code StageType.LEGACY} is allowed
  • - *
- * - * @since 1.0 - * @see Stage - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface EnableFlamingock { - - - /** - * Defines the pipeline stages. - * Each stage represents a logical grouping of changes that execute in sequence. - * - *

Mutually exclusive with {@link #configFile()}. When using stages, - * do not specify a pipeline file. - * - *

Stage type restrictions: - *

    - *
  • Maximum of 1 stage with type {@code StageType.SYSTEM} is allowed
  • - *
  • Maximum of 1 stage with type {@code StageType.LEGACY} is allowed
  • - *
  • Unlimited stages with type {@code StageType.DEFAULT} are allowed
  • - *
- * - *

Example: - *

-     * stages = {
-     *     @Stage(type = StageType.SYSTEM, location = "com.example.system"),
-     *     @Stage(type = StageType.LEGACY, location = "com.example.init"),
-     *     @Stage(type = StageType.DEFAULT, location = "com.example.changes")
-     * }
-     * 
- * - * @return array of stage configurations - * @see Stage - */ - Stage[] stages() default {}; - - /** - * Specifies the path to a YAML pipeline configuration file for file-based configuration. - * The file path supports both absolute paths and classpath resources. - * - *

Mutually exclusive with {@link #stages()}. When using a pipeline file, - * do not specify stages in the annotation. - * - *

File resolution order: - *

    - *
  1. Direct file path (absolute or relative to working directory)
  2. - *
  3. Classpath resource in {@code src/main/resources/}
  4. - *
  5. Classpath resource in {@code src/test/resources/}
  6. - *
- * - *

Example: - *

-     * configFile = "config/flamingock-pipeline.yaml"
-     * 
- * - * @return the pipeline file path, or empty string for annotation-based configuration - */ - String configFile() default ""; - - /** - * If true, the annotation processor will validate that all code-based changes - * (classes annotated with @Change) are mapped to some stage. When unmapped changes - * are found and this flag is true**(default)**, a RuntimeException is thrown at compilation time. - * When false, only a warning is emitted. - */ - boolean strictStageMapping() default true; - -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/FlamingockCliBuilder.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/FlamingockCliBuilder.java deleted file mode 100644 index 33012cdd5..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/FlamingockCliBuilder.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a static method that provides a configured Flamingock builder for CLI execution. - * - *

The annotated method must: - *

    - *
  • Be static
  • - *
  • Take no parameters, OR take a single {@code String[] args} parameter
  • - *
  • Return AbstractChangeRunnerBuilder (or a subtype)
  • - *
- * - *

Example without arguments: - *

- * @FlamingockCliBuilder
- * public static AbstractChangeRunnerBuilder flamingockBuilder() {
- *     return Flamingock.builder()
- *         .setAuditStore(auditStore)
- *         .addTargetSystem(targetSystem);
- * }
- * 
- * - *

Example with arguments (for configuration based on CLI args): - *

- * @FlamingockCliBuilder
- * public static AbstractChangeRunnerBuilder flamingockBuilder(String[] args) {
- *     // args can be used during builder configuration
- *     return Flamingock.builder()
- *         .setAuditStore(auditStore)
- *         .addTargetSystem(targetSystem);
- * }
- * 
- * - *

The CLI will invoke this method to get the builder, add CLI arguments - * via {@code setApplicationArguments(args)}, build, and run the Flamingock pipeline. - * - *

Note: When using the {@code String[] args} parameter, you can access - * the arguments during builder creation. The CLI will still call - * {@code setApplicationArguments(args)} after your method returns, ensuring - * Flamingock's internal argument parsing always occurs. - * - * @since 1.1.0 - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface FlamingockCliBuilder { -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/FlamingockConstructor.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/FlamingockConstructor.java deleted file mode 100644 index f96186220..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/FlamingockConstructor.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.CONSTRUCTOR) -@Retention(RetentionPolicy.RUNTIME) -public @interface FlamingockConstructor { -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/NonLockGuarded.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/NonLockGuarded.java deleted file mode 100644 index 1588a33a6..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/NonLockGuarded.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - - -import io.flamingock.api.NonLockGuardedType; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) -@Retention(RetentionPolicy.RUNTIME) -public @interface NonLockGuarded { - - - /** - *

Indicates the grade of non-lock-guard applied to a method. - * Does not have any effect at class level. - *

- * - * @return value - */ - NonLockGuardedType[] value() default {NonLockGuardedType.METHOD, NonLockGuardedType.RETURN}; - - -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Nullable.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Nullable.java deleted file mode 100644 index fd86d6b84..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Nullable.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -@Target({ElementType.PARAMETER}) -@Retention(RetentionPolicy.RUNTIME) -public @interface Nullable { - -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Recovery.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Recovery.java deleted file mode 100644 index 618cd1fc6..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Recovery.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import io.flamingock.api.RecoveryStrategy; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Configures the recovery behavior for a change when execution fails. - * Determines whether Flamingock should automatically retry or require manual intervention. - * - *

Recovery strategies help maintain system consistency during failures by defining - * clear policies for handling transient vs. permanent errors. - * - *

Example usage: - *

{@code
- * @Recovery(strategy = RecoveryStrategy.ALWAYS_RETRY)
- * @Change(id = "populate-cache", order = "2024-11-18-001", author = "cache-team")
- * public class PopulateCacheChange {
- *     @Apply
- *     public void populateCache(CacheManager cache, ExternalAPI api) {
- *         // Might fail due to transient network issues - safe to retry
- *         cache.put("data", api.fetchData());
- *     }
- * }
- *
- * @Recovery(strategy = RecoveryStrategy.MANUAL_INTERVENTION)
- * @Change(id = "critical-data-migration", order = "2024-11-18-002", author = "data-team")
- * public class CriticalDataMigration {
- *     @Apply
- *     public void migrateData(Database db) {
- *         // Complex migration requiring human verification on failure
- *         db.executeMigration();
- *     }
- * }
- * }
- * - * @see Change - * @see RecoveryStrategy - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface Recovery { - - /** - * The recovery strategy to apply when this change fails. - * Defaults to {@link RecoveryStrategy#MANUAL_INTERVENTION} for safety. - * - * @return the recovery strategy - */ - RecoveryStrategy strategy() default RecoveryStrategy.MANUAL_INTERVENTION; - -} \ No newline at end of file diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Rollback.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Rollback.java deleted file mode 100644 index 546f8cbb5..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Rollback.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks the method that rolls back a change in case of failure or manual reversion. - * This method should undo the operations performed by the corresponding {@link Apply} method. - * - *

Rollback methods are optional but recommended for production systems to ensure - * safe change reversibility. They receive the same dependency injection as Apply methods. - * - *

Example usage: - *

{@code
- * @Change(id = "migrate-user-schema", order = "2024-11-16-001", author = "ops-team")
- * public class MigrateUserSchema {
- *
- *     @Apply
- *     public void migrateSchema(MongoDatabase db) {
- *         // Add new field and migrate data
- *         db.getCollection("users").updateMany(
- *             new Document(),
- *             Updates.set("createdAt", new Date())
- *         );
- *     }
- *
- *     @Rollback
- *     public void revertSchema(MongoDatabase db) {
- *         // Remove the added field
- *         db.getCollection("users").updateMany(
- *             new Document(),
- *             Updates.unset("createdAt")
- *         );
- *     }
- * }
- * }
- * - * @see Change - * @see Apply - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Rollback { - -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Stage.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Stage.java deleted file mode 100644 index b2cdc47eb..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/Stage.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -public @interface Stage { - /** - * Specifies the location where changes are found. This field is mandatory. - * - *

The location format determines how it's interpreted: - *

    - *
  • Package name: Contains dots and no slashes (e.g., "com.example.migrations") - - * Used to scan for annotated changes in the specified package
  • - *
  • Resource directory (relative): Starts with "resources/" (e.g., "resources/db/migrations") - - * Used to scan for template-based changes in the specified resources directory
  • - *
  • Resource directory (absolute): Starts with "/" (e.g., "/absolute/path/to/templates") - - * Used to scan for template-based changes in the specified absolute path
  • - *
- * - * @return the location where changes are found (mandatory) - */ - String location(); - - /** - * The name of the stage. If not specified, the name will be automatically derived from the location. - * - *

Name derivation rules: - *

    - *
  • Package: "com.example.migrations" → "migrations" (last segment)
  • - *
  • Resource path: "resources/db/migrations" → "migrations" (last segment)
  • - *
  • Absolute path: "/path/to/migrations" → "migrations" (last segment)
  • - *
- * - * @return the stage name, or empty string for auto-derived name - */ - String name() default ""; - - String description() default ""; - -} \ No newline at end of file diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/TargetSystem.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/TargetSystem.java deleted file mode 100644 index fdf536daf..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/annotations/TargetSystem.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Associates a change class with a specific target system or subsystem. - * Use this when managing multiple systems (databases, message queues, etc.) within the same pipeline. - * - *

Target systems allow fine-grained control over which changes apply to which components - * of your distributed architecture, enabling selective execution and rollback. - * - *

Example usage: - *

{@code
- * @TargetSystem(id = "user-database")
- * @Change(id = "add-user-preferences", order = "2024-11-17-001", author = "backend-team")
- * public class AddUserPreferences {
- *     @Apply
- *     public void addPreferencesTable(Connection conn) {
- *         // Create preferences table in user database
- *     }
- * }
- *
- * @TargetSystem(id = "analytics-database")
- * @Change(id = "create-metrics-view", order = "2024-11-17-002", author = "analytics-team")
- * public class CreateMetricsView {
- *     @Apply
- *     public void createView(Connection conn) {
- *         // Create materialized view in analytics database
- *     }
- * }
- * }
- * - * @see Change - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface TargetSystem { - - /** - * Identifier for the target system this change applies to. - * Must match a system configured in your Flamingock setup. - * - * @return the target system identifier - */ - String id(); -} \ No newline at end of file diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/external/ExternalSystem.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/external/ExternalSystem.java deleted file mode 100644 index 0d588b560..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/external/ExternalSystem.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.external; - -public interface ExternalSystem { - String getId(); - -} diff --git a/core/flamingock-core-api/src/main/java/io/flamingock/api/external/TargetSystem.java b/core/flamingock-core-api/src/main/java/io/flamingock/api/external/TargetSystem.java deleted file mode 100644 index 85c9fa771..000000000 --- a/core/flamingock-core-api/src/main/java/io/flamingock/api/external/TargetSystem.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.external; - - -public interface TargetSystem extends ExternalSystem { -} diff --git a/core/flamingock-core-commons/build.gradle.kts b/core/flamingock-core-commons/build.gradle.kts index e40cd4947..355b3feb3 100644 --- a/core/flamingock-core-commons/build.gradle.kts +++ b/core/flamingock-core-commons/build.gradle.kts @@ -1,7 +1,9 @@ val jacksonVersion = "2.16.0" +val generalUtilVersion: String by extra +val coreApiVersion: String by extra dependencies { - api(project(":core:flamingock-core-api")) - api(project(":utils:general-util"))//todo implementation + api("io.flamingock:flamingock-core-api:${coreApiVersion}") + api("io.flamingock:flamingock-general-util:${generalUtilVersion}")//todo implementation api("jakarta.annotation:jakarta.annotation-api:2.1.1")//todo can this be implementation? implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java index 07e327669..019acd5a9 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java @@ -23,6 +23,7 @@ public final class Constants { public static final String DEFAULT_MONGOCK_ORIGIN = "mongockChangeLog"; + public static final String MONGOCK_IMPORT_SKIP_PROPERTY_KEY = "internal.mongock.import.skip"; public static final String MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY = "internal.mongock.import.origin"; public static final String MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY = "internal.mongock.import.emptyOriginAllowed"; diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/AbstractPreviewTask.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/AbstractPreviewTask.java index 5b52e1619..cd0c9c4e6 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/AbstractPreviewTask.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/AbstractPreviewTask.java @@ -36,13 +36,14 @@ public AbstractPreviewTask(String id, String order, String author, String source, + String sourceFile, boolean runAlways, Boolean transactional, boolean system, TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery, boolean legacy) { - super(id, order, author, source, runAlways, transactional, system, targetSystem, recovery, legacy); + super(id, order, author, source, sourceFile, runAlways, transactional, system, targetSystem, recovery, legacy); } @Override diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/CodePreviewChange.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/CodePreviewChange.java index 11db39d5f..a9d261f9f 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/CodePreviewChange.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/CodePreviewChange.java @@ -36,6 +36,7 @@ public CodePreviewChange(String id, String order, String author, String sourceClassPath, + String sourceFile, PreviewConstructor previewConstructor, PreviewMethod applyPreviewMethod, PreviewMethod rollbackPreviewMethod, @@ -45,7 +46,7 @@ public CodePreviewChange(String id, TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery, boolean legacy) { - super(id, order, author, sourceClassPath, runAlways, transactional, system, targetSystem, recovery, legacy); + super(id, order, author, sourceClassPath, sourceFile, runAlways, transactional, system, targetSystem, recovery, legacy); this.previewConstructor = previewConstructor; this.applyPreviewMethod = applyPreviewMethod; this.rollbackPreviewMethod = rollbackPreviewMethod; diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java index 45f0cfffb..f2b7e215b 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/TemplatePreviewChange.java @@ -48,7 +48,7 @@ public TemplatePreviewChange(String fileName, Object steps, TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery) { - super(id, order, author, templateName, runAlways, transactional, system, targetSystem, recovery, false); + super(id, order, author, templateName, fileName, runAlways, transactional, system, targetSystem, recovery, false); this.fileName = fileName; this.profiles = profiles; this.configuration = configuration; diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilder.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilder.java index fc5904001..a6f9a7f5d 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilder.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilder.java @@ -171,6 +171,7 @@ private CodePreviewChange getCodePreviewChange() { order, author, sourceClassPath, + null, constructor, applyMethod, rollbackMethod, diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptor.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptor.java index 6d7cbb588..ba5b5c683 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptor.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptor.java @@ -29,6 +29,8 @@ public abstract class AbstractTaskDescriptor implements TaskDescriptor { protected String source; + protected String sourceFile; + protected boolean runAlways; protected Boolean transactionalFlag; @@ -47,6 +49,7 @@ public AbstractTaskDescriptor(String id, String order, String author, String source, + String sourceFile, boolean runAlways, Boolean transactionalFlag, boolean system, @@ -57,6 +60,7 @@ public AbstractTaskDescriptor(String id, this.order = order; this.author = author; this.source = source; + this.sourceFile = sourceFile; this.runAlways = runAlways; this.transactionalFlag = transactionalFlag; this.system = system; @@ -85,6 +89,11 @@ public String getSource() { return source; } + @Override + public String getSourceFile() { + return sourceFile; + } + @Override public boolean isRunAlways() { return runAlways; @@ -131,6 +140,10 @@ public void setSource(String source) { this.source = source; } + public void setSourceFile(String sourceFile) { + this.sourceFile = sourceFile; + } + public void setRunAlways(boolean runAlways) { this.runAlways = runAlways; } @@ -174,6 +187,7 @@ public int hashCode() { public String toString() { return new StringJoiner(", ", AbstractTaskDescriptor.class.getSimpleName() + "[", "]") .add("source=" + source) + .add("sourceFile=" + sourceFile) .add("sourceClass=" + getSource()) .add("sourceName='" + getSource() + "'") .add("id='" + getId() + "'") diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/TaskDescriptor.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/TaskDescriptor.java index 601b0676f..966135710 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/TaskDescriptor.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/task/TaskDescriptor.java @@ -32,6 +32,8 @@ public interface TaskDescriptor extends Comparable { String getSource(); + String getSourceFile(); + Optional getOrder(); TargetSystemDescriptor getTargetSystem(); diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilderTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilderTest.java new file mode 100644 index 000000000..8b55ae683 --- /dev/null +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilderTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.common.core.preview.builder; + +import io.flamingock.internal.common.core.preview.CodePreviewChange; +import io.flamingock.internal.common.core.preview.PreviewConstructor; +import io.flamingock.internal.common.core.preview.PreviewMethod; +import io.flamingock.internal.common.core.task.RecoveryDescriptor; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertNull; + +class CodePreviewTaskBuilderTest { + + @Test + void shouldBuildNullSourceFileForCodeChanges() { + CodePreviewChange preview = CodePreviewTaskBuilder.instance() + .setId("test-id") + .setOrder("001") + .setAuthor("author") + .setSourceClassPath("io.flamingock.TestChange") + .setConstructor(PreviewConstructor.getDefault()) + .setApplyMethod(new PreviewMethod("apply", Collections.emptyList())) + .setRollbackMethod(null) + .setRunAlways(false) + .setTransactionalFlag(true) + .setSystem(false) + .setRecovery(RecoveryDescriptor.getDefault()) + .build(); + + assertNull(preview.getSourceFile()); + } +} diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilderTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilderTest.java index c63d713ba..757f3c87f 100644 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilderTest.java +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/TemplatePreviewTaskBuilderTest.java @@ -41,6 +41,7 @@ void shouldPreserveNullTransactionalFromYaml() { .build(); assertEquals(Optional.empty(), preview.getTransactionalFlag()); + assertEquals("_0001__test.yaml", preview.getSourceFile()); } @Test @@ -56,6 +57,7 @@ void shouldPreserveExplicitTrueTransactional() { .build(); assertEquals(Optional.of(true), preview.getTransactionalFlag()); + assertEquals("_0001__test.yaml", preview.getSourceFile()); } @Test @@ -71,5 +73,6 @@ void shouldPreserveExplicitFalseTransactional() { .build(); assertEquals(Optional.of(false), preview.getTransactionalFlag()); + assertEquals("_0001__test.yaml", preview.getSourceFile()); } } diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptorTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptorTest.java index 8e236604f..7f37e0915 100644 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptorTest.java +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/task/AbstractTaskDescriptorTest.java @@ -27,7 +27,7 @@ class AbstractTaskDescriptorTest { private static AbstractTaskDescriptor createDescriptor(Boolean transactional) { return new AbstractTaskDescriptor( - "test-id", "001", "author", "source", + "test-id", "001", "author", "source", "sourceFile", false, transactional, false, null, null, false ) {}; diff --git a/core/flamingock-core/build.gradle.kts b/core/flamingock-core/build.gradle.kts index e2a493cd8..e93aaf6e6 100644 --- a/core/flamingock-core/build.gradle.kts +++ b/core/flamingock-core/build.gradle.kts @@ -1,8 +1,9 @@ val jacksonVersion = "2.16.0" +val generalUtilVersion: String by extra dependencies { api(project(":core:flamingock-core-commons")) - api(project(":utils:general-util"))//todo implementation + api("io.flamingock:flamingock-general-util:${generalUtilVersion}")//todo implementation api("javax.inject:javax.inject:1") api("org.javassist:javassist:3.30.2-GA") diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundle.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundle.java index 9dc713ef7..fe73e9476 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundle.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundle.java @@ -21,10 +21,12 @@ import io.flamingock.internal.common.cloud.vo.TargetSystemAuditMarkType; import io.flamingock.internal.core.pipeline.execution.ExecutionContext; import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; +import io.flamingock.internal.core.task.loaded.AbstractTemplateLoadedChange; import static io.flamingock.internal.common.core.audit.AuditEntry.ChangeType.MONGOCK_BEFORE; import static io.flamingock.internal.common.core.audit.AuditEntry.ChangeType.MONGOCK_EXECUTION; import static io.flamingock.internal.common.core.audit.AuditEntry.ChangeType.STANDARD_CODE; +import static io.flamingock.internal.common.core.audit.AuditEntry.ChangeType.STANDARD_TEMPLATE; public abstract class AuditContextBundle { @@ -104,7 +106,7 @@ public AuditEntry toAuditEntry() { getChangeType(), loadedChange.getSource(), runtimeContext.getMethodExecutor(), - null, //TODO: set sourceFile + loadedChange.getSourceFile(), runtimeContext.getDuration(), stageExecutionContext.getHostname(), stageExecutionContext.getMetadata(), @@ -132,12 +134,12 @@ private AuditEntry.Status getAuditStatus() { private AuditEntry.ChangeType getChangeType() { if(changeDescriptor.isLegacy()) { - //TODO improve the way we retrieve mongock before return changeDescriptor.getId().endsWith("_before") ? MONGOCK_BEFORE : MONGOCK_EXECUTION; + } else if(changeDescriptor instanceof AbstractTemplateLoadedChange) { + return STANDARD_TEMPLATE; } else { - //TODO update this when template is released return STANDARD_CODE; } } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractExecutableTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractExecutableTask.java index 2f4581021..8ec7dd77a 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractExecutableTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/executable/AbstractExecutableTask.java @@ -84,6 +84,11 @@ public String getSource() { return loadedChange.getSource(); } + @Override + public String getSourceFile() { + return loadedChange.getSourceFile(); + } + @Override public Optional getOrder() { return loadedChange.getOrder(); diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractLoadedTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractLoadedTask.java index 50ad55112..737fc47a4 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractLoadedTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractLoadedTask.java @@ -35,6 +35,7 @@ public AbstractLoadedTask(String id, String order, String author, String implementationSourceName, + String sourceFile, boolean runAlways, Boolean transactionalFlag, boolean transactional, @@ -42,7 +43,7 @@ public AbstractLoadedTask(String id, TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery, boolean legacy) { - super(id, order, author, implementationSourceName, runAlways, transactionalFlag, system, targetSystem, recovery, legacy); + super(id, order, author, implementationSourceName, sourceFile, runAlways, transactionalFlag, system, targetSystem, recovery, legacy); this.transactional = transactional; } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractReflectionLoadedTask.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractReflectionLoadedTask.java index e02885671..59cdccd63 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractReflectionLoadedTask.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/AbstractReflectionLoadedTask.java @@ -60,10 +60,10 @@ public abstract class AbstractReflectionLoadedTask extends AbstractLoadedTask { /** * The source file name where this change is defined. * - *

This represents the original source of the change definition:

+ *

This represents the original source of the change definition when available:

*
    *
  • Template-based: The YAML/JSON template file name (e.g., "create-users.yaml")
  • - *
  • Code-based: The Java class name containing the {@code @Change} annotation
  • + *
  • Code-based: {@code null} for now
  • *
* *

Note: This may differ from the {@link #implementationClass} in template-based scenarios @@ -103,7 +103,7 @@ public AbstractReflectionLoadedTask(String fileName, TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery, boolean legacy) { - super(id, order, author, implementationClass.getName(), runAlways, transactionalFlag, transactional, system, targetSystem, recovery, legacy); + super(id, order, author, implementationClass.getName(), fileName, runAlways, transactionalFlag, transactional, system, targetSystem, recovery, legacy); this.fileName = fileName; this.implementationClass = implementationClass; } @@ -111,7 +111,7 @@ public AbstractReflectionLoadedTask(String fileName, /** * Returns the source file name where this change is defined. * - * @return the file name (template file for template-based, class name for code-based) + * @return the file name for template-based changes, or {@code null} when unavailable * @see #fileName */ public String getFileName() { diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedChange.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedChange.java index 3503b2fe5..35b724b27 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedChange.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedChange.java @@ -32,6 +32,7 @@ public class CodeLoadedChange extends AbstractLoadedChange { String order, String author, Class changeClass, + String sourceFile, Constructor constructor, Method applyMethod, Optional rollbackMethod, @@ -42,7 +43,7 @@ public class CodeLoadedChange extends AbstractLoadedChange { TargetSystemDescriptor targetSystem, RecoveryDescriptor recovery, boolean legacy) { - super(changeClass.getSimpleName(), id, order, author, changeClass, constructor, runAlways, transactionalFlag, transactional, systemTask, targetSystem, recovery, legacy); + super(sourceFile, id, order, author, changeClass, constructor, runAlways, transactionalFlag, transactional, systemTask, targetSystem, recovery, legacy); this.applyMethod = applyMethod; this.rollbackMethod = rollbackMethod; } diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilder.java index a5a5b5777..69121b6fb 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilder.java @@ -44,6 +44,7 @@ public class CodeLoadedTaskBuilder implements LoadedTaskBuilder rollbackMethod; private boolean isRunAlways; private Boolean transactionalFlag; + private String sourceFile; private boolean isSystem; private TargetSystemDescriptor targetSystem; private RecoveryDescriptor recovery; @@ -78,6 +79,7 @@ private CodeLoadedTaskBuilder setPreview(CodePreviewChange preview) { setOrder(preview.getOrder().orElse(null)); setAuthor(preview.getAuthor()); setChangeClassName(preview.getSource()); + setSourceFile(preview.getSourceFile()); setConstructor(getConstructorFromPreview(preview)); setApplyMethod(getApplyMethodFromPreview(preview)); setRollbackMethod(getRollbackMethodFromPreview(preview)); @@ -151,6 +153,11 @@ public CodeLoadedTaskBuilder setTransactionalFlag(Boolean transactionalFlag) { return this; } + public CodeLoadedTaskBuilder setSourceFile(String sourceFile) { + this.sourceFile = sourceFile; + return this; + } + public CodeLoadedTaskBuilder setSystem(boolean system) { this.isSystem = system; return this; @@ -183,6 +190,7 @@ public CodeLoadedChange build() { order, author, changeClass, + sourceFile, constructor, applyMethod, rollbackMethod, @@ -202,6 +210,7 @@ private void setFromFlamingockChangeAnnotation(Class sourceClass, Change anno setOrder(ChangeOrderExtractor.extractOrderFromClassName(changeId, sourceClass.getName())); setAuthor(annotation.author()); setChangeClassName(sourceClass.getName()); + setSourceFile(null); setConstructor(getConstructor(sourceClass)); setApplyMethod(getApplyMethodFromAnnotation(sourceClass)); setRollbackMethod(getRollbackMethodFromAnnotation(sourceClass)); diff --git a/core/flamingock-core/src/test/java/io/flamingock/core/pipeline/PipelineTest.java b/core/flamingock-core/src/test/java/io/flamingock/core/pipeline/PipelineTest.java index 3d2e4c307..209522811 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/core/pipeline/PipelineTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/core/pipeline/PipelineTest.java @@ -117,6 +117,7 @@ void shouldThrowExceptionOnlyForEmptyStageWhenMixedWithNonEmptyStages() { "001", "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -171,6 +172,7 @@ void shouldThrowExceptionWhenTaskHasInvalidOrderFormat() { "12", // Too short (only 2 alphanumeric characters) "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -186,6 +188,7 @@ void shouldThrowExceptionWhenTaskHasInvalidOrderFormat() { "a_", // Only 1 alphanumeric character "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -225,6 +228,7 @@ void shouldValidateSuccessfullyWhenTasksHaveValidOrderFormats() { "001", // Valid 3 alphanumeric characters "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -240,6 +244,7 @@ void shouldValidateSuccessfullyWhenTasksHaveValidOrderFormats() { "abc", // Valid 3 alphanumeric characters "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -255,6 +260,7 @@ void shouldValidateSuccessfullyWhenTasksHaveValidOrderFormats() { "V1_2_3", // Valid with underscores and alphanumeric chars "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -270,6 +276,7 @@ void shouldValidateSuccessfullyWhenTasksHaveValidOrderFormats() { "20250925_01_migration", // Valid complex format "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -307,6 +314,7 @@ void shouldThrowExceptionWhenDuplicateChangeIds() { "001", // Valid: 3 alphanumeric characters "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -322,6 +330,7 @@ void shouldThrowExceptionWhenDuplicateChangeIds() { "002", // Valid: 3 alphanumeric characters "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, @@ -337,6 +346,7 @@ void shouldThrowExceptionWhenDuplicateChangeIds() { "003", // Valid: 3 alphanumeric characters "test-author", PipelineTestChange.class.getName(), + null, PreviewConstructor.getDefault(), executionMethod, null, diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundleChangeTypeTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundleChangeTypeTest.java new file mode 100644 index 000000000..9c0b8c3a4 --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundleChangeTypeTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.core.external.store.audit.domain; + +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.audit.AuditTxType; +import io.flamingock.internal.common.core.task.RecoveryDescriptor; +import io.flamingock.internal.core.pipeline.execution.ExecutionContext; +import io.flamingock.internal.core.task.executable.ExecutableTask; +import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; +import io.flamingock.internal.core.task.loaded.CodeLoadedChange; +import io.flamingock.internal.core.task.loaded.SimpleTemplateLoadedChange; +import io.flamingock.internal.core.task.navigation.step.StartStep; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AuditContextBundleChangeTypeTest { + + @Test + void toAuditEntry_shouldReturnStandardCode_whenCodeLoadedChange() { + CodeLoadedChange loadedChange = mockCodeLoadedChange("change-001", false); + AuditEntry entry = buildAuditEntry(loadedChange); + assertEquals(AuditEntry.ChangeType.STANDARD_CODE, entry.getType()); + } + + @Test + void toAuditEntry_shouldReturnStandardTemplate_whenTemplateLoadedChange() { + SimpleTemplateLoadedChange loadedChange = mockTemplateLoadedChange("change-002"); + AuditEntry entry = buildAuditEntry(loadedChange); + assertEquals(AuditEntry.ChangeType.STANDARD_TEMPLATE, entry.getType()); + } + + @Test + void toAuditEntry_shouldReturnMongockExecution_whenLegacyChange() { + CodeLoadedChange loadedChange = mockCodeLoadedChange("legacy-change", true); + AuditEntry entry = buildAuditEntry(loadedChange); + assertEquals(AuditEntry.ChangeType.MONGOCK_EXECUTION, entry.getType()); + } + + private AuditEntry buildAuditEntry(AbstractLoadedTask loadedChange) { + ExecutionContext executionContext = new ExecutionContext("exec-1", "localhost", Collections.emptyMap()); + + ExecutableTask executableTask = mock(ExecutableTask.class); + when(executableTask.getApplyMethodName()).thenReturn("apply"); + when(executableTask.getStageName()).thenReturn("stage-1"); + + StartStep startStep = new StartStep(executableTask); + RuntimeContext runtimeContext = RuntimeContext.builder() + .setStartStep(startStep) + .setAppliedAt(LocalDateTime.now()) + .build(); + + ExecutionAuditContextBundle bundle = new ExecutionAuditContextBundle( + loadedChange, + executionContext, + runtimeContext, + AuditTxType.NON_TX, + "target-system-1" + ); + + return bundle.toAuditEntry(); + } + + @SuppressWarnings("unchecked") + private static CodeLoadedChange mockCodeLoadedChange(String id, boolean legacy) { + CodeLoadedChange change = mock(CodeLoadedChange.class); + when(change.getId()).thenReturn(id); + when(change.getAuthor()).thenReturn("test-author"); + when(change.getSource()).thenReturn("TestChangeClass"); + when(change.isSystem()).thenReturn(false); + when(change.isLegacy()).thenReturn(legacy); + when(change.getOrder()).thenReturn(Optional.of("001")); + when(change.getRecovery()).thenReturn(RecoveryDescriptor.getDefault()); + when(change.isTransactional()).thenReturn(true); + return change; + } + + @SuppressWarnings("unchecked") + private static SimpleTemplateLoadedChange mockTemplateLoadedChange(String id) { + SimpleTemplateLoadedChange change = mock(SimpleTemplateLoadedChange.class); + when(change.getId()).thenReturn(id); + when(change.getAuthor()).thenReturn("test-author"); + when(change.getSource()).thenReturn("template-change.yaml"); + when(change.isSystem()).thenReturn(false); + when(change.isLegacy()).thenReturn(false); + when(change.getOrder()).thenReturn(Optional.of("002")); + when(change.getRecovery()).thenReturn(RecoveryDescriptor.getDefault()); + when(change.isTransactional()).thenReturn(true); + return change; + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundleTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundleTest.java new file mode 100644 index 000000000..54158315a --- /dev/null +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/external/store/audit/domain/AuditContextBundleTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.core.external.store.audit.domain; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.audit.AuditTxType; +import io.flamingock.internal.common.core.recovery.action.ChangeAction; +import io.flamingock.internal.common.core.task.RecoveryDescriptor; +import io.flamingock.internal.core.pipeline.execution.ExecutionContext; +import io.flamingock.internal.core.task.executable.CodeExecutableTask; +import io.flamingock.internal.core.task.executable.ExecutableTask; +import io.flamingock.internal.core.task.loaded.AbstractLoadedTask; +import io.flamingock.internal.core.task.loaded.CodeLoadedChange; +import io.flamingock.internal.core.task.loaded.CodeLoadedTaskBuilder; +import io.flamingock.internal.core.task.navigation.step.StartStep; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class AuditContextBundleTest { + + @Change(id = "audit-source-file-test", author = "aperezdieppa") + public static class _001__AuditSourceFileChange { + @Apply + public void apply() { + // no-op + } + } + + @Test + void shouldPopulateAuditEntrySourceFileFromLoadedTask() { + CodeLoadedChange loadedChange = buildLoadedChange(_001__AuditSourceFileChange.class); + CodeExecutableTask executableTask = new CodeExecutableTask<>( + "test-stage", + loadedChange, + ChangeAction.APPLY, + loadedChange.getApplyMethod(), + loadedChange.getRollbackMethod().orElse(null) + ); + RuntimeContext runtimeContext = RuntimeContext.builder() + .setStartStep(new StartStep(executableTask)) + .setAppliedAt(LocalDateTime.now()) + .build(); + ExecutionContext executionContext = new ExecutionContext("execution-id", "test-host", Collections.emptyMap()); + + AuditEntry auditEntry = new ExecutionAuditContextBundle( + loadedChange, + executionContext, + runtimeContext, + AuditTxType.NON_TX, + "target-system") + .toAuditEntry(); + + assertNull(auditEntry.getSourceFile()); + } + + @Test + void shouldPopulateAuditEntrySourceFileForTemplateBasedChanges() { + AbstractLoadedTask loadedChange = Mockito.mock(AbstractLoadedTask.class); + Mockito.when(loadedChange.getId()).thenReturn("template-change"); + Mockito.when(loadedChange.getAuthor()).thenReturn("author"); + Mockito.when(loadedChange.getSource()).thenReturn("io.flamingock.TemplateChange"); + Mockito.when(loadedChange.getSourceFile()).thenReturn("_0001__template-change.yaml"); + Mockito.when(loadedChange.isSystem()).thenReturn(false); + Mockito.when(loadedChange.getOrder()).thenReturn(java.util.Optional.of("0001")); + Mockito.when(loadedChange.getRecovery()).thenReturn(RecoveryDescriptor.getDefault()); + Mockito.when(loadedChange.isTransactional()).thenReturn(true); + + ExecutableTask executableTask = Mockito.mock(ExecutableTask.class); + Mockito.when(executableTask.getApplyMethodName()).thenReturn("apply"); + Mockito.when(executableTask.getStageName()).thenReturn("test-stage"); + + RuntimeContext runtimeContext = RuntimeContext.builder() + .setStartStep(new StartStep(executableTask)) + .setAppliedAt(LocalDateTime.now()) + .build(); + ExecutionContext executionContext = new ExecutionContext("execution-id", "test-host", Collections.emptyMap()); + + AuditEntry auditEntry = new ExecutionAuditContextBundle( + loadedChange, + executionContext, + runtimeContext, + AuditTxType.NON_TX, + "target-system") + .toAuditEntry(); + + assertEquals("_0001__template-change.yaml", auditEntry.getSourceFile()); + } + + private CodeLoadedChange buildLoadedChange(Class changeClass) { + try { + Method factoryMethod = CodeLoadedTaskBuilder.class.getDeclaredMethod("getInstanceFromClass", Class.class); + factoryMethod.setAccessible(true); + Object builder = factoryMethod.invoke(null, changeClass); + return ((CodeLoadedTaskBuilder) builder).build(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/AuditListOperationTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/AuditListOperationTest.java index 484375014..cb99759eb 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/AuditListOperationTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/operation/AuditListOperationTest.java @@ -164,7 +164,7 @@ private AuditEntry createAuditEntryWithTime(String executionId, String taskId, L AuditEntry.ChangeType.STANDARD_CODE, "TestClass", "apply", - "TestClass.java", + null, 100L, "localhost", null, diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilderTest.java index 47bc64004..3096538ae 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/CodeLoadedTaskBuilderTest.java @@ -54,6 +54,7 @@ void shouldBuildWithOrderInContentWhenOrderInContentPresentAndNoOrderInSource() assertEquals("001", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals(WithoutOrderTestClass.class, result.getImplementationClass()); + assertNull(result.getSourceFile()); } @Test @@ -151,6 +152,7 @@ void shouldWorkWithRealClassWhenOrderValidationPasses() { assertFalse(result.isRunAlways()); assertTrue(result.isTransactional()); assertFalse(result.isSystem()); + assertNull(result.getSourceFile()); } @Test @@ -193,6 +195,7 @@ void shouldBuildFromAnnotatedClassCorrectly() { assertEquals("annotation-test", result.getId()); assertEquals("100", result.getOrder().orElse(null)); assertEquals(_100__TestChangeClass.class, result.getImplementationClass()); + assertNull(result.getSourceFile()); assertFalse(result.isRunAlways()); // Default is false since not specified in annotation assertFalse(result.isTransactional()); // Explicitly set to false in annotation assertFalse(result.isSystem()); @@ -227,6 +230,7 @@ void shouldBuildFromAnnotatedClassCorrectlyWhenOrderInAnnotationNull() { assertEquals("no-order-in_annotation", result.getId()); assertEquals("0001", result.getOrder().orElse(null)); assertEquals(_0001__anotherChange.class, result.getImplementationClass()); + assertNull(result.getSourceFile()); } } diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java index 42170b729..f02f33902 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/task/loaded/SimpleTemplateLoadedTaskBuilderTest.java @@ -171,6 +171,7 @@ void shouldBuildWithOrderInContentWhenOrderInContentPresentAndNoOrderInFileName( assertEquals("001", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("test-file.yml", result.getFileName()); + assertEquals("test-file.yml", result.getSourceFile()); // Verify typed payloads are stored SimpleTemplateLoadedChange simpleResult = (SimpleTemplateLoadedChange) result; assertNotNull(simpleResult.getApplyPayload()); @@ -207,6 +208,7 @@ void shouldBuildWithOrderFromFileNameWhenOrderInContentIsNullAndOrderInFileNameI assertEquals("0002", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("_0002__test-file.yml", result.getFileName()); + assertEquals("_0002__test-file.yml", result.getSourceFile()); } } @@ -238,6 +240,7 @@ void shouldBuildWithOrderInContentWhenOrderInContentMatchesOrderInFileName() { assertEquals("003", result.getOrder().orElse(null)); assertEquals("test-id", result.getId()); assertEquals("_003__test-file.yml", result.getFileName()); + assertEquals("_003__test-file.yml", result.getSourceFile()); } } diff --git a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java index db5df461d..d5267b1a0 100644 --- a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java +++ b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java @@ -17,7 +17,11 @@ import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.ChangeTemplate; +import io.flamingock.api.template.TemplateField; +import io.flamingock.api.template.TemplatePayload; import io.flamingock.api.template.TemplateStep; +import io.flamingock.api.template.wrappers.TemplateString; +import io.flamingock.api.template.wrappers.TemplateVoid; import io.flamingock.internal.common.core.metadata.FlamingockMetadata; import io.flamingock.internal.common.core.preview.*; import io.flamingock.internal.common.core.task.AbstractTaskDescriptor; @@ -150,6 +154,10 @@ private void registerTemplates() { registerClassForReflection(ChangeTemplate.class); registerClassForReflection(AbstractChangeTemplate.class); registerClassForReflection(TemplateStep.class); + registerClassForReflection(TemplateField.class); + registerClassForReflection(TemplatePayload.class); + registerClassForReflection(TemplateString.class); + registerClassForReflection(TemplateVoid.class); ChangeTemplateManager.getRawTemplates().forEach(template -> { registerClassForReflection(template.getClass()); template.getReflectiveClasses().forEach(RegistrationFeature::registerClassForReflection); diff --git a/core/flamingock-processor/build.gradle.kts b/core/flamingock-processor/build.gradle.kts index e79ba8fcd..d3ffa2eaf 100644 --- a/core/flamingock-processor/build.gradle.kts +++ b/core/flamingock-processor/build.gradle.kts @@ -1,7 +1,8 @@ val jacksonVersion = "2.16.0" +val generalUtilVersion: String by extra dependencies { api(project(":core:flamingock-core-commons")) - api(project(":utils:general-util"))//todo implementation + api("io.flamingock:flamingock-general-util:${generalUtilVersion}")//todo implementation api("org.yaml:snakeyaml:2.2")//todo implementation api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")//todo implementation } diff --git a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java index 7a2b0d8bd..1bce4e857 100644 --- a/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java +++ b/core/flamingock-processor/src/test/java/io/flamingock/core/processor/PipelinePreProcessorTest.java @@ -443,7 +443,7 @@ private void createPipelineYamlFile() throws IOException { private Map> createMockChangesMap() { Map> map = new HashMap<>(); // Create mock tasks for each package so stages can be built - AbstractPreviewTask mockTask = new AbstractPreviewTask("mock-task", "001", "test-author", "test-source", false, true, false, null, RecoveryDescriptor.getDefault(), false) {}; + AbstractPreviewTask mockTask = new AbstractPreviewTask("mock-task", "001", "test-author", "test-source", "test-source-file", false, true, false, null, RecoveryDescriptor.getDefault(), false) {}; map.put("com.example.system", Collections.singletonList(mockTask)); map.put("com.example.system1", Collections.singletonList(mockTask)); diff --git a/core/flamingock-template-api/build.gradle.kts b/core/flamingock-template-api/build.gradle.kts deleted file mode 100644 index 8f7d09c8c..000000000 --- a/core/flamingock-template-api/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -dependencies { - implementation(project(":utils:general-util")) -} - -description = "Public API for creating Flamingock change templates" - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/ApplyTemplate.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/ApplyTemplate.java deleted file mode 100644 index cf8912052..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/ApplyTemplate.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks the method that applies a template-based change to the target system. - * This method contains the forward change logic that evolves your system state. - * - *

The method can accept dependency-injected parameters from the Flamingock context, - * including database connections, repositories, and custom dependencies. - * - *

This annotation is specifically for use in {@link ChangeTemplate} classes. - * For code-based changes, use {@code @Apply} instead. - * - * @see ChangeTemplate - * @see RollbackTemplate - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface ApplyTemplate { - -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/ChangeTemplate.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/ChangeTemplate.java deleted file mode 100644 index b3c31316b..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/ChangeTemplate.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import io.flamingock.api.template.AbstractChangeTemplate; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a class as a Flamingock change template and configures its execution mode. - * - *

All template classes must extend {@link AbstractChangeTemplate} and be annotated with - * this annotation to specify whether they process single or multiple steps. - * - *

The {@code id} field is mandatory and must match the {@code template:} field in YAML - * pipeline definitions. This decouples template identity from Java class naming. - * - *

Simple templates (default, {@code multiStep = false}): - *

- * id: create-users-table
- * template: sql-template
- * apply: "CREATE TABLE users (id INT PRIMARY KEY)"
- * rollback: "DROP TABLE users"
- * 
- * - *

Steppable templates ({@code multiStep = true}) process multiple operations: - *

- * id: setup-orders
- * template: mongo-template
- * steps:
- *   - apply: { type: createCollection, collection: orders }
- *     rollback: { type: dropCollection, collection: orders }
- *   - apply: { type: insert, collection: orders, ... }
- *     rollback: { type: delete, collection: orders, ... }
- * 
- * - *

Steppable rollback behavior: - *

    - *
  • On failure, previously successful steps are rolled back in reverse order
  • - *
  • Steps without rollback are skipped during rollback
  • - *
- * - * @see AbstractChangeTemplate - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface ChangeTemplate { - - /** - * Unique identifier for this template. The YAML {@code template:} field must match this ID. - * - * @return the template identifier - */ - String name(); - - /** - * When {@code true}, the template expects a {@code steps} array in YAML. - * When {@code false} (default), it expects {@code apply} and optional {@code rollback} at root. - * - * @return {@code true} for steppable templates, {@code false} for simple templates - */ - boolean multiStep() default false; - - /** - * When {@code true} (default), change authors must provide rollback payloads in YAML. - * When {@code false}, rollback payloads are optional. - * - * @return {@code true} if rollback payloads are required, {@code false} otherwise - */ - boolean rollbackPayloadRequired() default true; -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/RollbackTemplate.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/RollbackTemplate.java deleted file mode 100644 index cb0ea1935..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/annotations/RollbackTemplate.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks the method that rolls back a template-based change in case of failure or manual reversion. - * This method should undo the operations performed by the corresponding {@link ApplyTemplate} method. - * - *

Rollback methods are optional but recommended for production systems to ensure - * safe change reversibility. They receive the same dependency injection as ApplyTemplate methods. - * - *

This annotation is specifically for use in {@link ChangeTemplate} classes. - * For code-based changes, use {@code @Rollback} instead. - * - * @see ChangeTemplate - * @see ApplyTemplate - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface RollbackTemplate { - -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java deleted file mode 100644 index 739178f16..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/AbstractChangeTemplate.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -import io.flamingock.internal.util.ReflectionUtil; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - - -/** - * Base class for creating Flamingock change templates. - * - *

Extend this class and annotate with {@link io.flamingock.api.annotations.ChangeTemplate} - * to create custom templates. The annotation's {@code steppable} attribute determines the YAML structure. - * - *

Use {@code @ChangeTemplate} (default {@code steppable = false}) for simple templates with - * single apply/rollback fields. - * - *

Use {@code @ChangeTemplate(steppable = true)} for steppable templates with multiple steps. - * - * @param shared configuration type (use {@code TemplateVoid} if none) - * @param apply payload type - * @param rollback payload type - * @see io.flamingock.api.annotations.ChangeTemplate - */ -public abstract class AbstractChangeTemplate implements ChangeTemplate { - - private final Class configurationClass; - private final Class applyPayloadClass; - private final Class rollbackPayloadClass; - protected String changeId; - protected boolean isTransactional; - - protected SHARED_CONFIGURATION_FIELD configuration; - protected APPLY_FIELD applyPayload; - protected ROLLBACK_FIELD rollbackPayload; - - private final Set> additionalReflectiveClasses; - - - @SuppressWarnings("unchecked") - public AbstractChangeTemplate(Class... additionalReflectiveClass) { - // Store additional classes - reflective classes set is built on-demand in getReflectiveClasses() - this.additionalReflectiveClasses = new HashSet<>(Arrays.asList(additionalReflectiveClass)); - - try { - Class[] typeArgs = ReflectionUtil.resolveTypeArgumentsAsClasses(this.getClass(), AbstractChangeTemplate.class); - - if (typeArgs.length < 3) { - throw new IllegalStateException("Expected 3 generic type arguments for a Template, but found " + typeArgs.length); - } - - this.configurationClass = (Class) typeArgs[0]; - this.applyPayloadClass = (Class) typeArgs[1]; - this.rollbackPayloadClass = (Class) typeArgs[2]; - } catch (ClassCastException e) { - throw new IllegalStateException("Generic type arguments for a Template must be concrete types (classes, interfaces, or primitive wrappers like String, Integer, etc.): " + e.getMessage(), e); - } catch (Exception e) { - throw new IllegalStateException("Failed to initialize template: " + e.getMessage(), e); - } - } - - /** - * Returns the collection of classes that need reflection registration for GraalVM native images. - *

- * This method builds the reflective classes set on-demand, including: - *

    - *
  • The configuration class (generic type argument 0)
  • - *
  • The apply payload class (generic type argument 1)
  • - *
  • The rollback payload class (generic type argument 2)
  • - *
  • {@link TemplateStep} class
  • - *
  • Any additional classes passed to the constructor
  • - *
- *

- * This method is only called by GraalVM's {@code RegistrationFeature} at build-time, - * so there is no performance concern from building the set on each call. - * - * @return collection of classes requiring reflection registration - */ - @Override - public final Collection> getReflectiveClasses() { - Set> reflectiveClasses = new HashSet<>(additionalReflectiveClasses); - reflectiveClasses.add(configurationClass); - reflectiveClasses.add(applyPayloadClass); - reflectiveClasses.add(rollbackPayloadClass); - reflectiveClasses.add(TemplateStep.class); - return reflectiveClasses; - } - - @Override - public void setChangeId(String changeId) { - this.changeId = changeId; - } - - @Override - public void setTransactional(boolean isTransactional) { - this.isTransactional = isTransactional; - } - - @Override - public void setConfiguration(SHARED_CONFIGURATION_FIELD configuration) { - this.configuration = configuration; - } - - @Override - public void setApplyPayload(APPLY_FIELD applyPayload) { - this.applyPayload = applyPayload; - } - - @Override - public void setRollbackPayload(ROLLBACK_FIELD rollbackPayload) { - this.rollbackPayload = rollbackPayload; - } - - @Override - public Class getConfigurationClass() { - return configurationClass; - } - - @Override - public Class getApplyPayloadClass() { - return applyPayloadClass; - } - - @Override - public Class getRollbackPayloadClass() { - return rollbackPayloadClass; - } - -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java deleted file mode 100644 index a83bd2dfa..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/ChangeTemplate.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -/** - * Core interface for Flamingock change templates. - * - *

Templates enable declarative, YAML-based changes. Implement this interface - * by extending {@link AbstractChangeTemplate} and annotating with - * {@link io.flamingock.api.annotations.ChangeTemplate}. - * - * @param shared configuration type (use {@code TemplateVoid} if none) - * @param apply payload type parsed from YAML - * @param rollback payload type parsed from YAML - * @see AbstractChangeTemplate - * @see io.flamingock.api.annotations.ChangeTemplate - */ -public interface ChangeTemplate extends ReflectionMetadataProvider { - - void setChangeId(String changeId); - - void setTransactional(boolean isTransactional); - - void setConfiguration(SHARED_CONFIG_FIELD configuration); - - void setApplyPayload(APPLY_FIELD applyPayload); - - void setRollbackPayload(ROLLBACK_FIELD rollbackPayload); - - Class getConfigurationClass(); - - Class getApplyPayloadClass(); - - Class getRollbackPayloadClass(); - -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/ReflectionMetadataProvider.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/ReflectionMetadataProvider.java deleted file mode 100644 index 1c34eb979..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/ReflectionMetadataProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -import java.util.Collection; - -/** - * Provides metadata about classes that require reflective access. - * - *

Implementations of this interface declare a collection of classes that should be registered - * for reflection at build time—commonly used in native image generation processes such as GraalVM. - */ -public interface ReflectionMetadataProvider { - - /** - * Returns a collection of classes that should be registered for reflective access. - * - *

This method does not perform any registration itself—it only declares the classes - * that need to be registered. The returned collection does not require a specific - * ordering and may contain any number of class references. - *

- * - * @return a collection of classes to be registered for reflection - */ - Collection> getReflectiveClasses(); - -} \ No newline at end of file diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateField.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateField.java deleted file mode 100644 index 449cbb9b4..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateField.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -import java.util.List; - -/** - * Base contract for all template field types (configuration, apply, rollback). - * - *

Provides structural validation at pipeline load time, before any change executes. - * Configuration fields extend this directly, while apply/rollback payloads extend - * {@link TemplatePayload} which adds transactional metadata via {@code getInfo()}. - */ -public interface TemplateField { - - /** - * Validates this field using the supplied change-level context - * and returns any errors found. - * - * @param context change-level metadata available during validation - * @return list of validation errors, empty if field is valid - */ - List validate(TemplateValidationContext context); -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayload.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayload.java deleted file mode 100644 index 198766a41..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayload.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -import java.util.List; - -/** - * Contract for template payload types (APPLY and ROLLBACK generics). - * - *

Extends {@link TemplateField} to inherit structural validation, and adds - * transactional metadata via {@link #getInfo()}. Configuration fields use - * {@code TemplateField} directly since they have no transactional semantics. - */ -public interface TemplatePayload extends TemplateField { - - /** - * Returns metadata about this payload so the framework can make - * centralized decisions based on payload characteristics. - * - * @return payload info; never {@code null} - */ - TemplatePayloadInfo getInfo(); -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayloadInfo.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayloadInfo.java deleted file mode 100644 index 133ec9f7d..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayloadInfo.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -import java.util.Optional; - -/** - * Metadata that a {@link TemplatePayload} exposes to the framework so - * centralized decisions can be made based on payload characteristics. - * - *

Binary-compatibility contract: new fields are added as - * getter/setter pairs. A {@code null} value means "not specified" — - * the payload makes no claim and the framework applies its own policy. - * Older implementations that return a default-constructed instance - * continue to work unchanged as new fields are introduced. - * - *

Current fields: - *

    - *
  • {@code supportsTransactions} — whether the payload's target - * system supports transactional execution. {@code null} (default) - * means the payload makes no claim.
  • - *
- */ -public class TemplatePayloadInfo { - - private Boolean supportsTransactions; - - /** - * Creates an info instance with all fields set to {@code null} - * (no claims made). - */ - public TemplatePayloadInfo() { - } - - /** - * Creates an info instance with all fields - */ - public TemplatePayloadInfo(boolean supportsTransactions) { - this.supportsTransactions = supportsTransactions; - } - - /** - * Returns whether the payload's target system supports transactional - * execution. - * - * @return an {@link Optional} containing {@code true} or {@code false} - * if the payload explicitly declares support; empty if the - * payload makes no claim - */ - public Optional getSupportsTransactions() { - return Optional.ofNullable(supportsTransactions); - } - - /** - * Sets whether the payload's target system supports transactional - * execution. - * - * @param supportsTransactions {@code true} if transactions are supported, - * {@code false} if not, or {@code null} to - * make no claim - */ - public void setSupportsTransactions(Boolean supportsTransactions) { - this.supportsTransactions = supportsTransactions; - } -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayloadValidationError.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayloadValidationError.java deleted file mode 100644 index 8ea614710..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplatePayloadValidationError.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -/** - * Represents a validation error found in a {@link TemplatePayload}. - * - *

Errors can be field-level (with a specific field name) or general (without a field). - */ -public class TemplatePayloadValidationError { - - private final String field; - private final String message; - - public TemplatePayloadValidationError(String message) { - this(null, message); - } - - public TemplatePayloadValidationError(String field, String message) { - this.field = field; - this.message = message; - } - - public String getField() { - return field; - } - - public String getMessage() { - return message; - } - - public String getFormattedMessage() { - return field != null ? String.format("[field: %s] %s", field, message) : message; - } - - @Override - public String toString() { - return getFormattedMessage(); - } -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateStep.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateStep.java deleted file mode 100644 index 84755de6b..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateStep.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -/** - * Represents a single step in a step-based change template. - * - *

Each step contains an {@code apply} operation that executes during the forward - * migration, and an optional {@code rollback} operation that executes if the step - * or a subsequent step fails.

- * - *

YAML Structure

- *
{@code
- * steps:
- *   - apply:
- *       type: createCollection
- *       collection: users
- *     rollback:
- *       type: dropCollection
- *       collection: users
- *   - apply:
- *       type: insert
- *       collection: users
- *       parameters:
- *         documents:
- *           - name: "John"
- *     rollback:
- *       type: delete
- *       collection: users
- *       parameters:
- *         filter: {}
- * }
- * - *

Rollback Behavior

- *
    - *
  • Rollback is optional - steps without rollback are skipped during rollback
  • - *
  • When a step fails, all previously successful steps are rolled back in reverse order
  • - *
  • Rollback errors are logged but don't stop the rollback process
  • - *
- * - * @param the type of the apply payload - * @param the type of the rollback payload - */ -public class TemplateStep { - - private APPLY applyPayload; - private ROLLBACK rollbackPayload; - - public TemplateStep() { - } - - public TemplateStep(APPLY applyPayload, ROLLBACK rollbackPayload) { - this.applyPayload = applyPayload; - this.rollbackPayload = rollbackPayload; - } - - /** - * Returns the apply payload for this step. - * - * @return the apply payload (required) - */ - public APPLY getApplyPayload() { - return applyPayload; - } - - /** - * Sets the apply payload for this step. - * - * @param applyPayload the apply payload - */ - public void setApplyPayload(APPLY applyPayload) { - this.applyPayload = applyPayload; - } - - /** - * Returns the rollback payload for this step. - * - * @return the rollback payload, or null if no rollback is defined - */ - public ROLLBACK getRollbackPayload() { - return rollbackPayload; - } - - /** - * Sets the rollback payload for this step. - * - * @param rollbackPayload the rollback payload (optional) - */ - public void setRollbackPayload(ROLLBACK rollbackPayload) { - this.rollbackPayload = rollbackPayload; - } - - /** - * Checks if this step has a rollback payload defined. - * - * @return true if a rollback payload is defined - */ - public boolean hasRollbackPayload() { - return rollbackPayload != null; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder("TemplateStep{"); - sb.append("apply=").append(applyPayload); - if (rollbackPayload != null) { - sb.append(", rollback=").append(rollbackPayload); - } - sb.append('}'); - return sb.toString(); - } -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateValidationContext.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateValidationContext.java deleted file mode 100644 index b15fabda5..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/TemplateValidationContext.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -/** - * Context provided to {@link TemplatePayload#validate(TemplateValidationContext)} - * so payload validation can access change-level metadata. - * - *

Binary-compatibility contract: new fields are added as - * getter/setter pairs with sensible defaults. Older template payloads - * that ignore the new fields continue to work unchanged. - * - *

Current fields: - *

    - *
  • {@code transactional} (default {@code false}) — whether the - * enclosing change is declared transactional.
  • - *
- */ -public class TemplateValidationContext { - - private boolean transactional; - - /** - * Creates a context with all fields set to their defaults. - */ - public TemplateValidationContext() { - } - - /** - * Returns whether the enclosing change is declared transactional. - * - * @return {@code true} if the change is transactional, {@code false} otherwise - */ - public boolean isTransactional() { - return transactional; - } - - /** - * Sets whether the enclosing change is declared transactional. - * - * @param transactional {@code true} if the change is transactional - */ - public void setTransactional(boolean transactional) { - this.transactional = transactional; - } -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/wrappers/TemplateString.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/wrappers/TemplateString.java deleted file mode 100644 index 56a97330a..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/wrappers/TemplateString.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template.wrappers; - -import io.flamingock.api.template.TemplatePayload; -import io.flamingock.api.template.TemplatePayloadInfo; -import io.flamingock.api.template.TemplatePayloadValidationError; -import io.flamingock.api.template.TemplateValidationContext; - -import java.util.Collections; -import java.util.List; - -/** - * A {@link TemplatePayload} wrapper for {@code String} payloads. - * - *

Provides a drop-in replacement for raw {@code String} payloads in templates, - * keeping YAML clean while satisfying the {@code TemplatePayload} contract. - * - *

Supports SnakeYAML deserialization via the no-arg constructor and scalar - * conversion via the {@code String} constructor. - */ -public class TemplateString implements TemplatePayload { - - private String value; - - /** - * No-arg constructor for SnakeYAML deserialization. - */ - public TemplateString() { - } - - /** - * Constructor for scalar conversion (e.g., from YAML string values). - * - * @param value the string value - */ - public TemplateString(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - @Override - public List validate(TemplateValidationContext context) { - if (value == null || value.trim().isEmpty()) { - return Collections.singletonList( - new TemplatePayloadValidationError("value", "must not be null or blank")); - } - return Collections.emptyList(); - } - - @Override - public TemplatePayloadInfo getInfo() { - return new TemplatePayloadInfo(); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TemplateString that = (TemplateString) o; - return value != null ? value.equals(that.value) : that.value == null; - } - - @Override - public int hashCode() { - return value != null ? value.hashCode() : 0; - } -} diff --git a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/wrappers/TemplateVoid.java b/core/flamingock-template-api/src/main/java/io/flamingock/api/template/wrappers/TemplateVoid.java deleted file mode 100644 index 7bb1bc2a9..000000000 --- a/core/flamingock-template-api/src/main/java/io/flamingock/api/template/wrappers/TemplateVoid.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template.wrappers; - -import io.flamingock.api.template.TemplatePayload; -import io.flamingock.api.template.TemplatePayloadInfo; -import io.flamingock.api.template.TemplatePayloadValidationError; -import io.flamingock.api.template.TemplateValidationContext; - -import java.util.Collections; -import java.util.List; - -/** - * A {@link TemplatePayload} sentinel representing "no value needed". - * - *

Replaces {@code Void} as a type parameter in templates that have no shared - * configuration or no rollback. Implements {@code TemplatePayload} so it can be - * used in any template type parameter position (CONFIG, APPLY, or ROLLBACK). - */ -public class TemplateVoid implements TemplatePayload { - - private static final TemplatePayloadInfo TEMPLATE_PAYLOAD_INFO = new TemplatePayloadInfo(false); - - @Override - public List validate(TemplateValidationContext context) { - return Collections.emptyList(); - } - - @Override - public TemplatePayloadInfo getInfo() { - return TEMPLATE_PAYLOAD_INFO; - } -} diff --git a/core/flamingock-template-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java b/core/flamingock-template-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java deleted file mode 100644 index e481d5006..000000000 --- a/core/flamingock-template-api/src/test/java/io/flamingock/api/template/AbstractChangeTemplateReflectiveClassesTest.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright 2026 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.api.template; - -import io.flamingock.api.annotations.ApplyTemplate; -import io.flamingock.api.annotations.ChangeTemplate; -import io.flamingock.api.annotations.RollbackTemplate; -import io.flamingock.api.template.wrappers.TemplateString; -import io.flamingock.api.template.wrappers.TemplateVoid; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class AbstractChangeTemplateReflectiveClassesTest { - - // Simple test configuration class - public static class TestConfig implements TemplatePayload { - public String configValue; - - @Override - public List validate(TemplateValidationContext context) { - return Collections.emptyList(); - } - - @Override - public TemplatePayloadInfo getInfo() { - return new TemplatePayloadInfo(); - } - } - - // Simple test apply payload class - public static class TestApplyPayload implements TemplatePayload { - public String applyData; - - @Override - public List validate(TemplateValidationContext context) { - return Collections.emptyList(); - } - - @Override - public TemplatePayloadInfo getInfo() { - return new TemplatePayloadInfo(); - } - } - - // Simple test rollback payload class - public static class TestRollbackPayload implements TemplatePayload { - public String rollbackData; - - @Override - public List validate(TemplateValidationContext context) { - return Collections.emptyList(); - } - - @Override - public TemplatePayloadInfo getInfo() { - return new TemplatePayloadInfo(); - } - } - - // Additional class for reflection - public static class AdditionalClass { - public String additionalData; - } - - // Another additional class for reflection - public static class AnotherAdditionalClass { - public String moreData; - } - - // Test template with custom generic types - @ChangeTemplate(name = "test-template-with-custom-types") - public static class TestTemplateWithCustomTypes - extends AbstractChangeTemplate { - - public TestTemplateWithCustomTypes() { - super(); - } - - @ApplyTemplate - public void apply() { - // Test implementation - } - - @RollbackTemplate - public void rollback() { - } - } - - // Test template with additional reflective classes - @ChangeTemplate(name = "test-template-with-additional-classes") - public static class TestTemplateWithAdditionalClasses - extends AbstractChangeTemplate { - - public TestTemplateWithAdditionalClasses() { - super(AdditionalClass.class, AnotherAdditionalClass.class); - } - - @ApplyTemplate - public void apply() { - // Test implementation - } - - @RollbackTemplate - public void rollback() { - } - } - - // Test template with TemplateVoid configuration - @ChangeTemplate(name = "test-template-with-void-config") - public static class TestTemplateWithVoidConfig - extends AbstractChangeTemplate { - - public TestTemplateWithVoidConfig() { - super(); - } - - @ApplyTemplate - public void apply() { - // Test implementation - } - - @RollbackTemplate - public void rollback() { - } - } - - @Test - @DisplayName("getReflectiveClasses should return set containing configuration class") - void getReflectiveClassesShouldContainConfigurationClass() { - TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.contains(TestConfig.class), - "Should contain configuration class TestConfig"); - } - - @Test - @DisplayName("getReflectiveClasses should return set containing apply payload class") - void getReflectiveClassesShouldContainApplyPayloadClass() { - TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.contains(TestApplyPayload.class), - "Should contain apply payload class TestApplyPayload"); - } - - @Test - @DisplayName("getReflectiveClasses should return set containing rollback payload class") - void getReflectiveClassesShouldContainRollbackPayloadClass() { - TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.contains(TestRollbackPayload.class), - "Should contain rollback payload class TestRollbackPayload"); - } - - @Test - @DisplayName("getReflectiveClasses should return set containing TemplateStep class") - void getReflectiveClassesShouldContainTemplateStepClass() { - TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.contains(TemplateStep.class), - "Should contain TemplateStep class"); - } - - @Test - @DisplayName("getReflectiveClasses should include additional reflective classes passed to constructor") - void getReflectiveClassesShouldIncludeAdditionalClasses() { - TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.contains(AdditionalClass.class), - "Should contain AdditionalClass"); - assertTrue(reflectiveClasses.contains(AnotherAdditionalClass.class), - "Should contain AnotherAdditionalClass"); - } - - @Test - @DisplayName("Multiple calls to getReflectiveClasses should return equivalent sets") - void multipleCallsShouldReturnEquivalentSets() { - TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes(); - - Collection> firstCall = template.getReflectiveClasses(); - Collection> secondCall = template.getReflectiveClasses(); - - assertEquals(firstCall.size(), secondCall.size(), - "Both calls should return sets of the same size"); - assertTrue(firstCall.containsAll(secondCall), - "First call should contain all elements of second call"); - assertTrue(secondCall.containsAll(firstCall), - "Second call should contain all elements of first call"); - } - - @Test - @DisplayName("getReflectiveClasses with TemplateVoid configuration should include TemplateVoid class") - void getReflectiveClassesWithVoidConfigShouldIncludeTemplateVoidClass() { - TestTemplateWithVoidConfig template = new TestTemplateWithVoidConfig(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.contains(TemplateVoid.class), - "Should contain TemplateVoid class for configuration"); - assertTrue(reflectiveClasses.contains(TemplateString.class), - "Should contain TemplateString class for apply/rollback payloads"); - } - - @Test - @DisplayName("getReflectiveClasses should return at least 4 classes (config, apply, rollback, TemplateStep)") - void getReflectiveClassesShouldReturnAtLeast4Classes() { - TestTemplateWithCustomTypes template = new TestTemplateWithCustomTypes(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.size() >= 4, - "Should return at least 4 classes (config, apply, rollback, TemplateStep)"); - } - - @Test - @DisplayName("getReflectiveClasses with additional classes should return more than 4 classes") - void getReflectiveClassesWithAdditionalClassesShouldReturnMoreThan4() { - TestTemplateWithAdditionalClasses template = new TestTemplateWithAdditionalClasses(); - - Collection> reflectiveClasses = template.getReflectiveClasses(); - - assertTrue(reflectiveClasses.size() >= 6, - "Should return at least 6 classes (config, apply, rollback, TemplateStep, + 2 additional)"); - } -} diff --git a/core/target-systems/couchbase-external-system-api/build.gradle.kts b/core/target-systems/couchbase-external-system-api/build.gradle.kts index a86c664e2..827c0ae61 100644 --- a/core/target-systems/couchbase-external-system-api/build.gradle.kts +++ b/core/target-systems/couchbase-external-system-api/build.gradle.kts @@ -1,5 +1,6 @@ +val coreApiVersion: String by extra dependencies { - implementation(project(":core:flamingock-core-api")) + implementation("io.flamingock:flamingock-core-api:${coreApiVersion}") //General compileOnly("com.couchbase.client:java-client:3.6.0") diff --git a/core/target-systems/couchbase-target-system/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java b/core/target-systems/couchbase-target-system/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java index 8c996fffe..1be4949b8 100644 --- a/core/target-systems/couchbase-target-system/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java +++ b/core/target-systems/couchbase-target-system/src/test/java/io/flamingock/targetsystem/couchbase/PipelineTestHelper.java @@ -89,6 +89,7 @@ public static FlamingockMetadata getPreviewPipeline(String stageName, Trio().configureEach { } configurations.testImplementation { extendsFrom(configurations.compileOnly.get()) -} \ No newline at end of file +} diff --git a/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java b/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java index 513e87d60..312d4b62c 100644 --- a/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java +++ b/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java @@ -23,6 +23,9 @@ import com.couchbase.client.java.manager.bucket.BucketSettings; import io.flamingock.api.annotations.EnableFlamingock; import io.flamingock.api.annotations.Stage; +import io.flamingock.core.kit.TestKit; +import io.flamingock.core.kit.audit.AuditTestHelper; +import io.flamingock.couchbase.kit.CouchbaseTestKit; import io.flamingock.internal.common.core.audit.AuditEntry; import io.flamingock.store.couchbase.CouchbaseAuditStore; import io.flamingock.internal.common.core.error.FlamingockException; @@ -46,8 +49,11 @@ import java.util.List; import java.util.stream.Collectors; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_SKIP_PROPERTY_KEY; import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CREATED_AT; import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_STATE; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -66,12 +72,19 @@ public class CouchbaseImporterTest { public static final String MONGOCK_SCOPE_NAME = CollectionIdentifier.DEFAULT_SCOPE; public static final String MONGOCK_COLLECTION_NAME = CollectionIdentifier.DEFAULT_COLLECTION; + public static final String CUSTOM_MONGOCK_ORIGIN_SCOPE = "mongockCustomScope"; + public static final String CUSTOM_MONGOCK_ORIGIN_COLLECTION = "mongockCustomOriginCollection"; + @Container static final CouchbaseContainer couchbaseContainer = new CouchbaseContainer("couchbase/server:7.2.4") .withBucket(new org.testcontainers.couchbase.BucketDefinition(FLAMINGOCK_BUCKET_NAME)); private static Cluster cluster; + private static CouchbaseTargetSystem targetSystem; + private static CouchbaseAuditStore auditStore; + private static TestKit testKit; + private static AuditTestHelper auditHelper; @BeforeAll static void setupAll() { @@ -86,6 +99,14 @@ static void setupAll() { // Setup Mongock Bucket, Scope and Collection BucketManager bucketManager = cluster.buckets(); + targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); + auditStore = CouchbaseAuditStore.from(targetSystem) + .withScopeName(FLAMINGOCK_SCOPE_NAME) + .withAuditRepositoryName(FLAMINGOCK_COLLECTION_NAME); + + testKit = CouchbaseTestKit.create(auditStore, cluster, FLAMINGOCK_BUCKET_NAME, FLAMINGOCK_SCOPE_NAME); + auditHelper = testKit.getAuditHelper(); + int ramQuotaMB = 100; if (!bucketManager.getAllBuckets().containsKey(MONGOCK_BUCKET_NAME)) { @@ -101,7 +122,15 @@ static void setupAll() { @AfterEach void cleanUp() { CouchbaseCollectionHelper.deleteAllDocuments(cluster, FLAMINGOCK_BUCKET_NAME, FLAMINGOCK_SCOPE_NAME, FLAMINGOCK_COLLECTION_NAME); + CouchbaseCollectionHelper.waitUntilEmpty(cluster, FLAMINGOCK_BUCKET_NAME, FLAMINGOCK_SCOPE_NAME, FLAMINGOCK_COLLECTION_NAME, Duration.ofSeconds(10)); + CouchbaseCollectionHelper.deleteAllDocuments(cluster, MONGOCK_BUCKET_NAME, MONGOCK_SCOPE_NAME, MONGOCK_COLLECTION_NAME); + CouchbaseCollectionHelper.waitUntilEmpty(cluster, MONGOCK_BUCKET_NAME, MONGOCK_SCOPE_NAME, MONGOCK_COLLECTION_NAME, Duration.ofSeconds(10)); + + if (CouchbaseCollectionHelper.collectionExists(cluster, MONGOCK_BUCKET_NAME, CUSTOM_MONGOCK_ORIGIN_SCOPE, CUSTOM_MONGOCK_ORIGIN_COLLECTION)) { + CouchbaseCollectionHelper.deleteAllDocuments(cluster, MONGOCK_BUCKET_NAME, CUSTOM_MONGOCK_ORIGIN_SCOPE, CUSTOM_MONGOCK_ORIGIN_COLLECTION); + CouchbaseCollectionHelper.waitUntilEmpty(cluster, MONGOCK_BUCKET_NAME, CUSTOM_MONGOCK_ORIGIN_SCOPE, CUSTOM_MONGOCK_ORIGIN_COLLECTION, Duration.ofSeconds(10)); + } } @Test @@ -115,38 +144,27 @@ void GIVEN_allMongockChangeUnitsAlreadyExecuted_WHEN_migratingToFlamingockCommun originCollection.upsert("mongock-change-1", createAuditObject("mongock-change-1")); originCollection.upsert("mongock-change-2", createAuditObject("mongock-change-2")); - CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - - Runner flamingock = FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(targetSystem) - .withScopeName(FLAMINGOCK_SCOPE_NAME) - .withAuditRepositoryName(FLAMINGOCK_COLLECTION_NAME)) + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) .addTargetSystem(targetSystem) .build(); flamingock.run(); - List auditLog = getAuditLog(); - - assertEquals(6, auditLog.size()); - - assertEquals("mongock-change-1", auditLog.get(0).getString("changeId")); - assertEquals("APPLIED", auditLog.get(0).getString("state")); + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), - assertEquals("mongock-change-2", auditLog.get(1).getString("changeId")); - assertEquals("APPLIED", auditLog.get(1).getString("state")); + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(2).getString("changeId")); - assertEquals("STARTED", auditLog.get(2).getString("state")); - - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(3).getString("changeId")); - assertEquals("APPLIED", auditLog.get(3).getString("state")); - - assertEquals("flamingock-change", auditLog.get(4).getString("changeId")); - assertEquals("STARTED", auditLog.get(4).getString("state")); + // Application stage - new changes + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); - assertEquals("flamingock-change", auditLog.get(5).getString("changeId")); - assertEquals("APPLIED", auditLog.get(5).getString("state")); } @Test @@ -161,41 +179,28 @@ void GIVEN_someChangeUnitsAlreadyExecuted_WHEN_migratingToFlamingockCommunity_TH originCollection.upsert("mongock-change-1", createAuditObject("mongock-change-1")); - CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - - Runner flamingock = FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(targetSystem) - .withScopeName(FLAMINGOCK_SCOPE_NAME) - .withAuditRepositoryName(FLAMINGOCK_COLLECTION_NAME)) + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) .addTargetSystem(targetSystem) .build(); flamingock.run(); - List auditLog = getAuditLog(); - - assertEquals(7, auditLog.size()); - - assertEquals("mongock-change-1", auditLog.get(0).getString("changeId")); - assertEquals("APPLIED", auditLog.get(0).getString("state")); - - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(1).getString("changeId")); - assertEquals("STARTED", auditLog.get(1).getString("state")); - - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(2).getString("changeId")); - assertEquals("APPLIED", auditLog.get(2).getString("state")); - - assertEquals("mongock-change-2", auditLog.get(3).getString("changeId")); - assertEquals("STARTED", auditLog.get(3).getString("state")); + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("mongock-change-1"), - assertEquals("mongock-change-2", auditLog.get(4).getString("changeId")); - assertEquals("APPLIED", auditLog.get(4).getString("state")); + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), - assertEquals("flamingock-change", auditLog.get(5).getString("changeId")); - assertEquals("STARTED", auditLog.get(5).getString("state")); + // Application stage - new changes + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); - assertEquals("flamingock-change", auditLog.get(6).getString("changeId")); - assertEquals("APPLIED", auditLog.get(6).getString("state")); } @Test @@ -205,12 +210,8 @@ void GIVEN_someChangeUnitsAlreadyExecuted_WHEN_migratingToFlamingockCommunity_TH "THEN should throw exception") void GIVEN_mongockAuditHistoryEmptyAndNoFailIfEmptyOriginValueProvided_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { - CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - - Runner flamingock = FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(targetSystem) - .withScopeName(FLAMINGOCK_SCOPE_NAME) - .withAuditRepositoryName(FLAMINGOCK_COLLECTION_NAME)) + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) .addTargetSystem(targetSystem) .build(); @@ -226,12 +227,8 @@ void GIVEN_mongockAuditHistoryEmptyAndNoFailIfEmptyOriginValueProvided_WHEN_migr "THEN should throw exception") void GIVEN_mongockAuditHistoryEmptyAndFailIfEmptyOriginEnabled_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { - CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - - Runner flamingock = FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(targetSystem) - .withScopeName(FLAMINGOCK_SCOPE_NAME) - .withAuditRepositoryName(FLAMINGOCK_COLLECTION_NAME)) + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) .addTargetSystem(targetSystem) .setProperty(MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY, Boolean.FALSE.toString()) .build(); @@ -251,105 +248,215 @@ void GIVEN_mongockAuditHistoryEmptyAndFailIfEmptyOriginDisabled_WHEN_migratingTo CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - Runner flamingock = FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(targetSystem) - .withScopeName(FLAMINGOCK_SCOPE_NAME) - .withAuditRepositoryName(FLAMINGOCK_COLLECTION_NAME)) + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) .addTargetSystem(targetSystem) .setProperty(MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY, Boolean.TRUE.toString()) .build(); flamingock.run(); + auditHelper.verifyAuditSequenceStrict( + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes + STARTED("mongock-change-1"), + APPLIED("mongock-change-1"), + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); + + } - List auditLog = getAuditLog(); + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed" + + "AND custom origin repository name provided" + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvided_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + // Setup Mongock entries + final String customMongockOrigin = String.format("%s.%s", CUSTOM_MONGOCK_ORIGIN_SCOPE, CUSTOM_MONGOCK_ORIGIN_COLLECTION); - assertEquals(8, auditLog.size()); + CouchbaseCollectionHelper.createScopeIfNotExists(cluster, MONGOCK_BUCKET_NAME, CUSTOM_MONGOCK_ORIGIN_SCOPE); + CouchbaseCollectionHelper.createCollectionIfNotExists(cluster, MONGOCK_BUCKET_NAME, CUSTOM_MONGOCK_ORIGIN_SCOPE, CUSTOM_MONGOCK_ORIGIN_COLLECTION); + CouchbaseCollectionHelper.createPrimaryIndexIfNotExists(cluster, MONGOCK_BUCKET_NAME, CUSTOM_MONGOCK_ORIGIN_SCOPE, CUSTOM_MONGOCK_ORIGIN_COLLECTION); - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(0).getString("changeId")); - assertEquals("STARTED", auditLog.get(0).getString("state")); + Collection originCollection = cluster.bucket(MONGOCK_BUCKET_NAME).scope(CUSTOM_MONGOCK_ORIGIN_SCOPE).collection(CUSTOM_MONGOCK_ORIGIN_COLLECTION); - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(1).getString("changeId")); - assertEquals("APPLIED", auditLog.get(1).getString("state")); + originCollection.upsert("mongock-change-1", createAuditObject("mongock-change-1")); - assertEquals("mongock-change-1", auditLog.get(2).getString("changeId")); - assertEquals("STARTED", auditLog.get(2).getString("state")); + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - assertEquals("mongock-change-1", auditLog.get(3).getString("changeId")); - assertEquals("APPLIED", auditLog.get(3).getString("state")); + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .setProperty(MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY, customMongockOrigin) + .build(); - assertEquals("mongock-change-2", auditLog.get(4).getString("changeId")); - assertEquals("STARTED", auditLog.get(4).getString("state")); + flamingock.run(); - assertEquals("mongock-change-2", auditLog.get(5).getString("changeId")); - assertEquals("APPLIED", auditLog.get(5).getString("state")); + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("mongock-change-1"), - assertEquals("flamingock-change", auditLog.get(6).getString("changeId")); - assertEquals("STARTED", auditLog.get(6).getString("state")); + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), - assertEquals("flamingock-change", auditLog.get(7).getString("changeId")); - assertEquals("APPLIED", auditLog.get(7).getString("state")); + // Application stage - new changes + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); } @Test - @DisplayName("GIVEN all Mongock changeUnits already executed" + - "AND custom origin repository name provided" + + @DisplayName("GIVEN skip import flag with invalid value " + + "WHEN migrating to Flamingock Community" + + "THEN should throw exception") + void GIVEN_skipImportFlagWithInvalidValue_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + + final String SKIP_IMPORT_VALUE = "invalid_value"; + + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + FlamingockException ex = assertThrows(FlamingockException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_SKIP_PROPERTY_KEY + ": " + SKIP_IMPORT_VALUE + + " (expected \"true\" or \"false\" or empty)", ex.getMessage()); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag enabled " + + "WHEN migrating to Flamingock Community" + + "THEN should not import any audit history entry " + + "AND execute the all mongock and flamingock changes") + void GIVEN_skipImportFlagEnabled_WHEN_migratingToFlamingockCommunity_THEN_shouldNotMigrateAnyAuditLog() { + + Collection originCollection = cluster.bucket(MONGOCK_BUCKET_NAME).scope(MONGOCK_SCOPE_NAME).collection(MONGOCK_COLLECTION_NAME); + + originCollection.upsert("mongock-change-1", createAuditObject("mongock-change-1")); + originCollection.upsert("mongock-change-2", createAuditObject("mongock-change-2")); + + final String SKIP_IMPORT_VALUE = "true"; + + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + auditHelper.verifyAuditSequenceStrict( + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes + STARTED("mongock-change-1"), + APPLIED("mongock-change-1"), + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); + + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (explicit) " + "WHEN migrating to Flamingock Community " + "THEN should import the entire history " + "AND execute the pending flamingock changes") - void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvided_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { - // Setup Mongock entries - final String customMongockOriginScope = "mongockCustomScope"; - final String customMongockOriginCollection = "mongockCustomOriginCollection"; - final String customMongockOrigin = String.format("%s.%s", customMongockOriginScope, customMongockOriginCollection); - - CouchbaseCollectionHelper.createScopeIfNotExists(cluster, MONGOCK_BUCKET_NAME, customMongockOriginScope); - CouchbaseCollectionHelper.createCollectionIfNotExists(cluster, MONGOCK_BUCKET_NAME, customMongockOriginScope, customMongockOriginCollection); - CouchbaseCollectionHelper.createPrimaryIndexIfNotExists(cluster, MONGOCK_BUCKET_NAME, customMongockOriginScope, customMongockOriginCollection); - - Collection originCollection = cluster.bucket(MONGOCK_BUCKET_NAME).scope(customMongockOriginScope).collection(customMongockOriginCollection); + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledExplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + Collection originCollection = cluster.bucket(MONGOCK_BUCKET_NAME).scope(MONGOCK_SCOPE_NAME).collection(MONGOCK_COLLECTION_NAME); originCollection.upsert("mongock-change-1", createAuditObject("mongock-change-1")); + originCollection.upsert("mongock-change-2", createAuditObject("mongock-change-2")); CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - Runner flamingock = FlamingockFactory.getCommunityBuilder() - .setAuditStore(CouchbaseAuditStore.from(targetSystem) - .withScopeName(FLAMINGOCK_SCOPE_NAME) - .withAuditRepositoryName(FLAMINGOCK_COLLECTION_NAME)) + final String SKIP_IMPORT_VALUE = "false"; + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) .addTargetSystem(targetSystem) - .setProperty(MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY, customMongockOrigin) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false .build(); flamingock.run(); + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); + + } + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (implicit) " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledImplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + Collection originCollection = cluster.bucket(MONGOCK_BUCKET_NAME).scope(MONGOCK_SCOPE_NAME).collection(MONGOCK_COLLECTION_NAME); - List auditLog = getAuditLog(); + originCollection.upsert("mongock-change-1", createAuditObject("mongock-change-1")); + originCollection.upsert("mongock-change-2", createAuditObject("mongock-change-2")); - assertEquals(7, auditLog.size()); + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); - assertEquals("mongock-change-1", auditLog.get(0).getString("changeId")); - assertEquals("APPLIED", auditLog.get(0).getString("state")); + final String SKIP_IMPORT_VALUE = ""; - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(1).getString("changeId")); - assertEquals("STARTED", auditLog.get(1).getString("state")); + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); - assertEquals("migration-mongock-to-flamingock-community", auditLog.get(2).getString("changeId")); - assertEquals("APPLIED", auditLog.get(2).getString("state")); + flamingock.run(); - assertEquals("mongock-change-2", auditLog.get(3).getString("changeId")); - assertEquals("STARTED", auditLog.get(3).getString("state")); + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), - assertEquals("mongock-change-2", auditLog.get(4).getString("changeId")); - assertEquals("APPLIED", auditLog.get(4).getString("state")); + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), - assertEquals("flamingock-change", auditLog.get(5).getString("changeId")); - assertEquals("STARTED", auditLog.get(5).getString("state")); + // Application stage - new changes + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); - assertEquals("flamingock-change", auditLog.get(6).getString("changeId")); - assertEquals("APPLIED", auditLog.get(6).getString("state")); } @@ -379,5 +486,4 @@ private static JsonObject createAuditObject(String value) { .put("_doctype", "mongockChangeEntry"); return doc; } - } diff --git a/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java b/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java index dc7214891..08388f080 100644 --- a/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java +++ b/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java @@ -49,6 +49,7 @@ import static io.flamingock.internal.common.core.metadata.Constants.DEFAULT_MONGOCK_ORIGIN; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_SKIP_PROPERTY_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -335,6 +336,188 @@ void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvided_WHEN_migr + // Validate actual table creation + assertTrue(client.listTables().tableNames().contains("users"), "Users table should exist"); + + // Verify table structure + DescribeTableResponse tableDescription = client.describeTable( + DescribeTableRequest.builder().tableName("users").build() + ); + assertEquals("email", tableDescription.table().keySchema().get(0).attributeName()); + assertEquals(KeyType.HASH, tableDescription.table().keySchema().get(0).keyType()); + } + + @Test + @DisplayName("GIVEN skip import flag with invalid value " + + "WHEN migrating to Flamingock Community" + + "THEN should throw exception") + void GIVEN_skipImportFlagWithInvalidValue_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + // Setup Mongock entries + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + final String SKIP_IMPORT_VALUE = "invalid_value"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + FlamingockException ex = assertThrows(FlamingockException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_SKIP_PROPERTY_KEY + ": " + SKIP_IMPORT_VALUE + + " (expected \"true\" or \"false\" or empty)", ex.getMessage()); + + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag enabled " + + "WHEN migrating to Flamingock Community" + + "THEN should not import any audit history entry " + + "AND execute the all mongock and flamingock changes") + void GIVEN_skipImportFlagEnabled_WHEN_migratingToFlamingockCommunity_THEN_shouldNotMigrateAnyAuditLog() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + final String SKIP_IMPORT_VALUE = "true"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 8 total entries as shown in actual execution + auditHelper.verifyAuditSequenceStrict( + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Legacy changes + STARTED("mongock-change-1"), + APPLIED("mongock-change-1"), + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + + // Application stage - new changes created by templates + STARTED("create-users-table"), + APPLIED("create-users-table") + ); + + + + // Validate actual table creation + assertTrue(client.listTables().tableNames().contains("users"), "Users table should exist"); + + // Verify table structure + DescribeTableResponse tableDescription = client.describeTable( + DescribeTableRequest.builder().tableName("users").build() + ); + assertEquals("email", tableDescription.table().keySchema().get(0).attributeName()); + assertEquals(KeyType.HASH, tableDescription.table().keySchema().get(0).keyType()); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (explicit) " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledExplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + final String SKIP_IMPORT_VALUE = "false"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 9 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-table"), + APPLIED("create-users-table") + ); + + + + // Validate actual table creation + assertTrue(client.listTables().tableNames().contains("users"), "Users table should exist"); + + // Verify table structure + DescribeTableResponse tableDescription = client.describeTable( + DescribeTableRequest.builder().tableName("users").build() + ); + assertEquals("email", tableDescription.table().keySchema().get(0).attributeName()); + assertEquals(KeyType.HASH, tableDescription.table().keySchema().get(0).keyType()); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (implicit) " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledImplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + final String SKIP_IMPORT_VALUE = ""; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 9 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-table"), + APPLIED("create-users-table") + ); + + + // Validate actual table creation assertTrue(client.listTables().tableNames().contains("users"), "Users table should exist"); diff --git a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java b/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java index a14e0cf49..d369afecf 100644 --- a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java +++ b/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java @@ -49,6 +49,7 @@ import static io.flamingock.internal.common.core.metadata.Constants.DEFAULT_MONGOCK_ORIGIN; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_SKIP_PROPERTY_KEY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -353,6 +354,203 @@ void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvidedByLiteralV + // Validate actual change + List users = database.getCollection("users") + .find() + .into(new ArrayList<>()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN skip import flag with invalid value " + + "WHEN migrating to Flamingock Community" + + "THEN should throw exception") + void GIVEN_skipImportFlagWithInvalidValue_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + // Setup Mongock entries + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = "invalid_value"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + FlamingockException ex = assertThrows(FlamingockException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_SKIP_PROPERTY_KEY + ": " + SKIP_IMPORT_VALUE + + " (expected \"true\" or \"false\" or empty)", ex.getMessage()); + } + + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag enabled " + + "WHEN migrating to Flamingock Community" + + "THEN should not import any audit history entry " + + "AND execute the all mongock and flamingock changes") + void GIVEN_skipImportFlagEnabled_WHEN_migratingToFlamingockCommunity_THEN_shouldNotMigrateAnyAuditLog() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = "true"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 10 total entries as shown in actual execution + auditHelper.verifyAuditSequenceStrict( + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Legacy changes + STARTED("mongock-change-1"), + APPLIED("mongock-change-1"), + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + // Validate actual change + List users = database.getCollection("users") + .find() + .into(new ArrayList<>()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (explicit) " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledExplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = "false"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 11 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + // Validate actual change + List users = database.getCollection("users") + .find() + .into(new ArrayList<>()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (implicit) " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledImplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = ""; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 11 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + // Validate actual change List users = database.getCollection("users") .find() diff --git a/legacy/mongock-support/build.gradle.kts b/legacy/mongock-support/build.gradle.kts index db20e408c..808216221 100644 --- a/legacy/mongock-support/build.gradle.kts +++ b/legacy/mongock-support/build.gradle.kts @@ -1,6 +1,7 @@ +val coreApiVersion: String by extra dependencies { api(project(":core:flamingock-core"))//todo implementation - api(project(":core:flamingock-core-api"))//todo remove. This should be imported by core + api("io.flamingock:flamingock-core-api:${coreApiVersion}")//todo remove. This should be imported by core } description = "Provides a compatibility layer for Mongock-based applications, including drop-in annotations and audit store migration utilities to Flamingock." diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java index 33a3454aa..2cdd278cf 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java @@ -23,7 +23,6 @@ import io.flamingock.internal.common.core.audit.AuditWriter; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.pipeline.PipelineDescriptor; -import io.flamingock.internal.common.core.util.ConfigValueParser; import io.flamingock.internal.core.external.targets.TargetSystemManager; import io.flamingock.internal.core.external.targets.operations.TargetSystemOps; import io.flamingock.internal.core.external.targets.operations.TransactionalTargetSystemOps; @@ -35,6 +34,7 @@ import static io.flamingock.internal.common.core.audit.AuditReaderType.MONGOCK; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_SKIP_PROPERTY_KEY; /** * This ChangeUnit is intentionally not annotated with @Change, @Apply, or similar, @@ -48,7 +48,12 @@ public void importHistory(@Named("change.targetSystem.id") String targetSystemId @NonLockGuarded TargetSystemManager targetSystemManager, @NonLockGuarded AuditWriter auditWriter, @NonLockGuarded PipelineDescriptor pipelineDescriptor, - @Nullable @Named(MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY) String emptyOriginAllowed) { + @Nullable @Named(MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY) String emptyOriginAllowed, + @Nullable @Named(MONGOCK_IMPORT_SKIP_PROPERTY_KEY) String skipImport) { + if (resolveSkipImport(skipImport)) { + logger.info("Mongock audit log import skipped (skipImport=true). No audit entries will be migrated."); + return; + } logger.info("Starting audit log migration from Mongock to Flamingock community audit store"); AuditHistoryReader legacyHistoryReader = getAuditHistoryReader(targetSystemId, targetSystemManager); PipelineHelper pipelineHelper = new PipelineHelper(pipelineDescriptor); @@ -102,4 +107,15 @@ private boolean resolveEmptyOriginAllowed(String raw) { throw new FlamingockException("Invalid value for " + MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY + ": " + raw + " (expected \"true\" or \"false\" or empty)"); } + + private boolean resolveSkipImport(String raw) { + if (raw == null || raw.trim().isEmpty()) { + return false; // default behaviour + } + String v = raw.trim(); + if ("true".equalsIgnoreCase(v)) return true; + if ("false".equalsIgnoreCase(v)) return false; + throw new FlamingockException("Invalid value for " + MONGOCK_IMPORT_SKIP_PROPERTY_KEY + ": " + raw + + " (expected \"true\" or \"false\" or empty)"); + } } diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java index 6e5feeee7..22e12d71b 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java @@ -81,6 +81,20 @@ */ String targetSystem(); + /** + * Determines whether Mongock import should be skipped. + *

+ * Expected literal values are {@code "true"} or {@code "false"}. + *

+ * + *

+ * If empty (default), it will be treated as {@code "false"}. + *

+ * + * @return {@code "true"} to skip import, {@code "false"} to process it; empty treated as {@code "false"} + */ + String skipImport() default ""; + /** * Defines the origin collection/table name where Mongock audit entries are stored. *

diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java index 8187db8d8..7d8eb4f1c 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java @@ -101,6 +101,7 @@ private List getCodePreviewChangesFromChangeUnit(TypeElement getBeforeExecutionChangeOrder(order), author, sourceClassPath, + null, constructor, beforeExecutionMethod, rollbackBeforeExecutionMethod, @@ -118,6 +119,7 @@ private List getCodePreviewChangesFromChangeUnit(TypeElement getExecutionChangeOrder(order, beforeExecutionMethod != null), author, sourceClassPath, + null, constructor, executionMethod, rollbackMethod, @@ -229,6 +231,7 @@ private List getCodePreviewChangesFromChangeLog(TypeElement t getChangeSetOrder(sourceClassPath, changeLogAnnotation.order(), changeSetAnnotation.order()), changeSetAnnotation.author(), sourceClassPath, + null, getPreviewConstructor(typeElement), previewMethodFromExecutableElement(changeSetMethod), null, diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java index 7c4f845fc..1df06f4ff 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java @@ -50,6 +50,7 @@ import java.util.stream.Stream; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_SKIP_PROPERTY_KEY; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY; @SuppressWarnings("deprecation") @@ -84,6 +85,7 @@ public Collection findAnnotatedChanges() { .flatMap(List::stream) .filter(Objects::nonNull) .collect(Collectors.toList()); + changes.add(getImporterChange(mongockTargetSystemId)); return changes; @@ -142,16 +144,23 @@ private void processConfigurationProperties(MongockSupport mongockSupport, Map getAuditEntries() { + return CouchbaseCollectionHelper.selectAllDocuments( + cluster, auditCollection.bucketName(), auditCollection.scopeName(), auditCollection.name()) + .stream() + .map(mapper::fromDocument) + .collect(Collectors.toList()); + } + + @Override + public List getAuditEntriesForChange(String changeId) { + return CouchbaseCollectionHelper.selectAllDocuments( + cluster, auditCollection.bucketName(), auditCollection.scopeName(), auditCollection.name()) + .stream() + .filter(entry -> entry.getString(KEY_CHANGE_ID).equals(changeId)) + .map(mapper::fromDocument) + .collect(Collectors.toList()); + } + + @Override + public long countAuditEntriesWithStatus(AuditEntry.Status status) { + return CouchbaseCollectionHelper.selectAllDocuments( + cluster, auditCollection.bucketName(), auditCollection.scopeName(), auditCollection.name()) + .stream() + .filter(entry -> entry.getString(KEY_STATE).equals(status.name())) + .count(); + } + + @Override + public boolean hasAuditEntries() { + return !this.getAuditEntries().isEmpty(); + } + + @Override + public void clear() { + CouchbaseCollectionHelper.deleteAllDocuments(cluster, auditCollection.bucketName(), auditCollection.scopeName(), auditCollection.name()); + } + + private String toKey(AuditEntry auditEntry) { + return auditEntry.getExecutionId() + + '#' + + auditEntry.getTaskId() + + '#' + + auditEntry.getState().name(); + } +} diff --git a/utils/couchbase-test-kit/src/main/java/io/flamingock/couchbase/kit/CouchbaseLockStorage.java b/utils/couchbase-test-kit/src/main/java/io/flamingock/couchbase/kit/CouchbaseLockStorage.java new file mode 100644 index 000000000..9a86d2526 --- /dev/null +++ b/utils/couchbase-test-kit/src/main/java/io/flamingock/couchbase/kit/CouchbaseLockStorage.java @@ -0,0 +1,155 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.couchbase.kit; + +import com.couchbase.client.core.error.DocumentNotFoundException; +import com.couchbase.client.java.Cluster; +import com.couchbase.client.java.Collection; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.client.java.kv.GetResult; +import com.couchbase.client.java.kv.RemoveOptions; +import com.couchbase.client.java.kv.ReplaceOptions; +import io.flamingock.core.kit.lock.LockStorage; +import io.flamingock.internal.common.couchbase.CouchbaseCollectionHelper; +import io.flamingock.internal.common.couchbase.CouchbaseLockMapper; +import io.flamingock.internal.core.external.store.lock.LockAcquisition; +import io.flamingock.internal.core.external.store.lock.LockKey; +import io.flamingock.internal.core.external.store.lock.LockServiceException; +import io.flamingock.internal.core.external.store.lock.LockStatus; +import io.flamingock.internal.core.external.store.lock.community.CommunityLockEntry; +import io.flamingock.internal.util.TimeService; +import io.flamingock.internal.util.id.RunnerId; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME; + +/** + * Couchbase implementation of LockStorage for real database testing. + * Only depends on Couchbase client/database and core Flamingock classes. + * Does not depend on Couchbase-specific Flamingock components like CouchbaseTargetSystem. + */ +public class CouchbaseLockStorage implements LockStorage { + + private final Cluster cluster; + private final Collection lockCollection; + private final Map metadata = new ConcurrentHashMap<>(); + private final TimeService timeService; + + private final CouchbaseLockMapper mapper = new CouchbaseLockMapper(); + + public CouchbaseLockStorage(Cluster cluster, String bucketName, String scopeName) { + this(cluster, bucketName, scopeName, DEFAULT_LOCK_STORE_NAME); + } + + public CouchbaseLockStorage(Cluster cluster, String bucketName, String scopeName, String lockCollectionName) { + this.cluster = cluster; + this.lockCollection = cluster.bucket(bucketName).scope(scopeName).collection(lockCollectionName); + this.timeService = TimeService.getDefault(); + } + + @Override + public void storeLock(LockKey key, LockAcquisition acquisition) { + CommunityLockEntry newLock = new CommunityLockEntry(key.toString(), LockStatus.LOCK_HELD, acquisition.getOwner().toString(), timeService.currentDatePlusMillis(acquisition.getAcquiredForMillis())); + String keyId = toKey(newLock); + try { + GetResult result = lockCollection.get(keyId); + CommunityLockEntry existingLock = mapper.lockEntryFromDocument(result.contentAsObject()); + if (newLock.getOwner().equals(existingLock.getOwner()) || + LocalDateTime.now().isAfter(existingLock.getExpiresAt())) { + lockCollection.replace(keyId, mapper.toDocument(newLock), ReplaceOptions.replaceOptions().cas(result.cas())); + } else if (LocalDateTime.now().isBefore(existingLock.getExpiresAt())) { + throw new LockServiceException("Get By" + keyId, newLock.toString(), + "Still locked by " + existingLock.getOwner() + " until " + existingLock.getExpiresAt()); + } + } catch (DocumentNotFoundException documentNotFoundException) { + lockCollection.insert(keyId, mapper.toDocument(newLock)); + } + } + + @Override + public LockAcquisition getLock(LockKey lockKey) { + String key = toKey(lockKey); + try { + GetResult result = lockCollection.get(key); + return mapper.lockAcquisitionFromDocument(result.contentAsObject()); + } catch (DocumentNotFoundException documentNotFoundException) { + return null; + } + } + + @Override + public Map getAllLocks() { + Map locks = new HashMap<>(); + return CouchbaseCollectionHelper.selectAllDocuments( + cluster, lockCollection.bucketName(), lockCollection.scopeName(), lockCollection.name()) + .stream() + .collect(Collectors.toMap( + entry -> LockKey.fromString(entry.getString("key")), + this::documentToLockAcquisition + )); + } + + @Override + public void removeLock(LockKey lockKey) { + String key = toKey(lockKey); + try { + GetResult result = lockCollection.get(key); + lockCollection.remove(key, RemoveOptions.removeOptions().cas(result.cas())); + } catch (DocumentNotFoundException documentNotFoundException) { + // Lock for key is not found, nothing to do + } + } + + @Override + public boolean hasLocks() { + return !this.getAllLocks().isEmpty(); + } + + @Override + public void clear() { + CouchbaseCollectionHelper.deleteAllDocuments(cluster, lockCollection.bucketName(), lockCollection.scopeName(), lockCollection.name()); + metadata.clear(); + } + + @Override + public void setMetadata(String key, Object value) { + metadata.put(key, value); + } + + @Override + public Object getMetadata(String key) { + return metadata.get(key); + } + + private String toKey(CommunityLockEntry lockEntry) { + return lockEntry.getKey(); + } + + private String toKey(LockKey lockKey) { + return lockKey.toString(); + } + + private LockAcquisition documentToLockAcquisition(JsonObject doc) { + RunnerId owner = RunnerId.fromString(doc.getString("owner")); + long leaseMillis = doc.getLong("leaseMillis"); + + return new LockAcquisition(owner, leaseMillis); + } +} diff --git a/utils/couchbase-test-kit/src/main/java/io/flamingock/couchbase/kit/CouchbaseTestKit.java b/utils/couchbase-test-kit/src/main/java/io/flamingock/couchbase/kit/CouchbaseTestKit.java new file mode 100644 index 000000000..f703e0223 --- /dev/null +++ b/utils/couchbase-test-kit/src/main/java/io/flamingock/couchbase/kit/CouchbaseTestKit.java @@ -0,0 +1,81 @@ +/* + * Copyright 2025 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.couchbase.kit; + +import com.couchbase.client.core.io.CollectionIdentifier; +import com.couchbase.client.java.Cluster; +import io.flamingock.core.kit.AbstractTestKit; +import io.flamingock.core.kit.audit.AuditStorage; +import io.flamingock.core.kit.lock.LockStorage; +import io.flamingock.internal.common.couchbase.CouchbaseCollectionHelper; +import io.flamingock.internal.core.external.store.CommunityAuditStore; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class CouchbaseTestKit extends AbstractTestKit { + + private final Cluster cluster; + private final String bucketName; + private final String scopeName; + + public CouchbaseTestKit(AuditStorage auditStorage, LockStorage lockStorage, CommunityAuditStore AuditStore, Cluster cluster, String bucketName, String scopeName) { + super(auditStorage, lockStorage, AuditStore); + this.cluster = cluster; + this.bucketName = bucketName; + this.scopeName = scopeName; + } + + @Override + public void cleanUp() { + cluster.bucket(bucketName).collections().getAllScopes().stream() + .filter(scopeSpec -> scopeSpec.name().equals(scopeName)) + .flatMap(scopeSpec -> scopeSpec.collections().stream()) + .forEach(collectionSpec -> { + CouchbaseCollectionHelper.deleteAllDocuments(cluster, bucketName, scopeName, collectionSpec.name()); + cluster.bucket(bucketName).scope(scopeName).collection(collectionSpec.name()) + .queryIndexes().getAllIndexes().stream() + .filter(index -> !index.primary()) + .forEach(index -> + CouchbaseCollectionHelper.dropIndexIfExists(cluster, bucketName, scopeName, collectionSpec.name(), index.name()) + ); + if (!collectionSpec.name().equals(CollectionIdentifier.DEFAULT_COLLECTION)) { + CouchbaseCollectionHelper.dropPrimaryIndexIfExists(cluster, bucketName, scopeName, collectionSpec.name()); + CouchbaseCollectionHelper.dropCollectionIfExists(cluster, bucketName, scopeName, collectionSpec.name()); + } + }); + } + + /** + * Create a new CouchbaseTestKit with Couchbase cluster and bucketName + */ + public static CouchbaseTestKit create(CommunityAuditStore AuditStore, Cluster cluster, String bucketName) { + CouchbaseAuditStorage auditStorage = new CouchbaseAuditStorage(cluster, bucketName, CollectionIdentifier.DEFAULT_SCOPE); + CouchbaseLockStorage lockStorage = new CouchbaseLockStorage(cluster, bucketName, CollectionIdentifier.DEFAULT_SCOPE); + return new CouchbaseTestKit(auditStorage, lockStorage, AuditStore, cluster, bucketName, CollectionIdentifier.DEFAULT_SCOPE); + } + + /** + * Create a new CouchbaseTestKit with Couchbase cluster, bucketName and scopeName + */ + public static CouchbaseTestKit create(CommunityAuditStore AuditStore, Cluster cluster, String bucketName, String scopeName) { + CouchbaseAuditStorage auditStorage = new CouchbaseAuditStorage(cluster, bucketName, scopeName); + CouchbaseLockStorage lockStorage = new CouchbaseLockStorage(cluster, bucketName, scopeName); + return new CouchbaseTestKit(auditStorage, lockStorage, AuditStore, cluster, bucketName, scopeName); + } +} diff --git a/utils/couchbase-util/src/main/java/io/flamingock/internal/common/couchbase/CouchbaseCollectionHelper.java b/utils/couchbase-util/src/main/java/io/flamingock/internal/common/couchbase/CouchbaseCollectionHelper.java index 7e0be42bf..8744a27b0 100644 --- a/utils/couchbase-util/src/main/java/io/flamingock/internal/common/couchbase/CouchbaseCollectionHelper.java +++ b/utils/couchbase-util/src/main/java/io/flamingock/internal/common/couchbase/CouchbaseCollectionHelper.java @@ -37,7 +37,7 @@ public final class CouchbaseCollectionHelper { private final static String KEYSPACE_TEMPLATE = "`%s`.`%s`.`%s`"; - private final static String SELECT_COUNT_QUERY_TEMPLATE = "SELECT COUNT(*) FROM `%s`.`%s`.`%s`"; + private final static String SELECT_COUNT_QUERY_TEMPLATE = "SELECT COUNT(*) as cnt FROM `%s`.`%s`.`%s`"; private final static String SELECT_ALL_QUERY_TEMPLATE = "SELECT %s.* FROM `%s`.`%s`.`%s`"; private final static String DELETE_ALL_QUERY_TEMPLATE = "DELETE FROM `%s`.`%s`.`%s`"; private final static String CREATE_PRIMARY_INDEX_TEMPLATE = "CREATE PRIMARY INDEX IF NOT EXISTS ON `%s`.`%s`.`%s`"; @@ -159,7 +159,8 @@ public static List selectAllDocuments(Cluster cluster, String bucket } public static void deleteAllDocuments(Cluster cluster, String bucketName, String scopeName, String collectionName) { - cluster.query(String.format(DELETE_ALL_QUERY_TEMPLATE, bucketName, scopeName, collectionName)); + cluster.query(String.format(DELETE_ALL_QUERY_TEMPLATE, bucketName, scopeName, collectionName), + QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)); } public static void createPrimaryIndexIfNotExists(Cluster cluster, String bucketName, String scopeName, String collectionName) { @@ -173,4 +174,44 @@ public static void dropPrimaryIndexIfExists(Cluster cluster, String bucketName, public static void dropIndexIfExists(Cluster cluster, String bucketName, String scopeName, String collectionName, String indexName) { cluster.query(String.format(DROP_INDEX_TEMPLATE, indexName, bucketName, scopeName, collectionName)); } + + public static void waitUntilEmpty( + Cluster cluster, + String bucketName, + String scopeName, + String collectionName, + Duration timeout + ) { + long deadline = System.nanoTime() + timeout.toNanos(); + + while (System.nanoTime() < deadline) { + String countQuery = String.format(SELECT_COUNT_QUERY_TEMPLATE, bucketName, scopeName, collectionName); + + List rows = cluster.query(countQuery, + QueryOptions.queryOptions().scanConsistency(QueryScanConsistency.REQUEST_PLUS)).rowsAsObject(); + long count = rows.isEmpty() ? 0L : rows.get(0).getLong("cnt"); + + if (count == 0L) { + return; + } + + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException( + "Interrupted while waiting for collection cleanup", + e + ); + } + } + + throw new IllegalStateException(String.format( + "Timeout waiting for collection %s.%s.%s to be empty after %s", + bucketName, + scopeName, + collectionName, + timeout + )); + } } diff --git a/utils/dynamodb-test-kit/build.gradle.kts b/utils/dynamodb-test-kit/build.gradle.kts index cb78b5954..c797bd9cd 100644 --- a/utils/dynamodb-test-kit/build.gradle.kts +++ b/utils/dynamodb-test-kit/build.gradle.kts @@ -1,7 +1,8 @@ +val generalUtilVersion: String by extra dependencies { implementation(project(":core:flamingock-core")) implementation(project(":utils:dynamodb-util")) - implementation(project(":utils:general-util")) + implementation("io.flamingock:flamingock-general-util:${generalUtilVersion}") implementation(project(":utils:test-util")) compileOnly("software.amazon.awssdk:dynamodb-enhanced:2.25.29") diff --git a/utils/dynamodb-util/build.gradle.kts b/utils/dynamodb-util/build.gradle.kts index 352c2acbc..4ad55d06d 100644 --- a/utils/dynamodb-util/build.gradle.kts +++ b/utils/dynamodb-util/build.gradle.kts @@ -1,6 +1,7 @@ +val generalUtilVersion: String by extra dependencies { implementation(project(":core:flamingock-core")) - implementation(project(":utils:general-util")) + implementation("io.flamingock:flamingock-general-util:${generalUtilVersion}") compileOnly("software.amazon.awssdk:dynamodb-enhanced:2.25.29") } diff --git a/utils/general-util/build.gradle.kts b/utils/general-util/build.gradle.kts deleted file mode 100644 index 578a3d754..000000000 --- a/utils/general-util/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -plugins { - id("java") -} - - -val jacksonVersion = "2.16.0" -dependencies { - api("org.yaml:snakeyaml:2.2") - - api("org.apache.httpcomponents:httpclient:4.5.14") - - api("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:$jacksonVersion") - implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") - -} - -description = "General-purpose utilities and helper classes shared across Flamingock modules" - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } -} \ No newline at end of file diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/CollectionUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/CollectionUtil.java deleted file mode 100644 index 387575b12..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/CollectionUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public final class CollectionUtil { - private CollectionUtil() { - } - - public static List assignOrEmpty(List list) { - return list != null ? list : new ArrayList<>(); - } - - public static Stream optionalToStream(Optional optional) { - return optional.map(Stream::of).orElseGet(Stream::empty); - } - - - public static List getClassNames(List> classses) { - return classses - .stream() - .map(Class::getName) - .collect(Collectors.toList()); - } - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/Constants.java b/utils/general-util/src/main/java/io/flamingock/internal/util/Constants.java deleted file mode 100644 index e0af6da58..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/Constants.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -public final class Constants { - public static final String PROXY_FLAMINGOCK_PREFIX = "_$$_flamingock_"; - - public static long DEFAULT_LOCK_ACQUIRED_FOR_MILLIS = 60 * 1000L;//1 minute - public static long DEFAULT_QUIT_TRYING_AFTER_MILLIS = 3 * 60 * 1000L;//3 minutes - public static long DEFAULT_TRY_FREQUENCY_MILLIS = 1000L;//1 second - - - public static final String DEFAULT_CLOUD_AUDIT_STORE = "cloud-audit-store"; - public static final String DEFAULT_MONGODB_AUDIT_STORE = "mongodb-audit-store"; - public static final String DEFAULT_DYNAMODB_AUDIT_STORE = "dynamodb-audit-store"; - public static final String DEFAULT_COUCHBASE_AUDIT_STORE = "couchbase-audit-store"; - public static final String DEFAULT_SQL_AUDIT_STORE = "sql-audit-store"; - public static final String DEFAULT_IN_MEMORY_AUDIT_STORE = "in_memory-audit-store"; - - - - private Constants() {} - - - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/FileUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/FileUtil.java deleted file mode 100644 index 3637e8e09..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/FileUtil.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.Constructor; - -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.StringWriter; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Files; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -public final class FileUtil { - - - private FileUtil() { - } - - - public static List getAllYamlFiles(File directory) { - FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".yaml"); - return getAllFiles(directory, fileNameFilter); - } - - public static List getAllFiles(File directory, FilenameFilter filenameFilter) { - File[] files = directory.listFiles(filenameFilter); - return Arrays.asList(Objects.requireNonNull(files)); - } - - - public static File getFile(String parentDir, String childDir, boolean check) { - File file = new File(parentDir, childDir); - if(check && !file.exists()) { - throw new RuntimeException("File not found: " + file.getAbsolutePath()); - } - return file; - } - - public static List loadFilesFromDirectory(String directory, ClassLoader classLoader) { - try { - URL resource = classLoader.getResource(directory); - if (resource == null) { - throw new RuntimeException("Resource not found: " + directory); - } - return Arrays.asList(Objects.requireNonNull(new File(resource.toURI()).listFiles())); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - public static T getFromYamlFile(File file, Class type) { - try { - return new Yaml(new Constructor(type, new LoaderOptions())).load(Files.newInputStream(file.toPath())); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - public static T convertToType(Class type, Object source) { - // Direct match shortcut - if (type.isInstance(source)) { - return type.cast(source); - } - // Scalar-to-constructor conversion: if source is a scalar type and target has a matching constructor - if (source instanceof String || source instanceof Number || source instanceof Boolean) { - try { - java.lang.reflect.Constructor ctor = type.getConstructor(source.getClass()); - return ctor.newInstance(source); - } catch (NoSuchMethodException ignored) { - // Fall through to YAML round-trip - } catch (Exception e) { - throw new RuntimeException("Failed to construct " + type.getSimpleName() + " from scalar: " + e.getMessage(), e); - } - } - Yaml yamlWriter = new Yaml(); - StringWriter writer = new StringWriter(); - yamlWriter.dump(source, writer); - String string = writer.toString(); - Constructor constructor = new Constructor(type, new LoaderOptions()); - Yaml yaml = new Yaml(constructor); - return yaml - .load(string); - } - - public static boolean isExistingDir(File directory) { - return directory.exists() && directory.isDirectory(); - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/FlamingockError.java b/utils/general-util/src/main/java/io/flamingock/internal/util/FlamingockError.java deleted file mode 100644 index 8d9161349..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/FlamingockError.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -public class FlamingockError { - - public static final String GENERIC_ERROR = "CLIENT_GENERIC_ERROR"; - - public static final String OBJECT_MAPPING_ERROR = "CLIENT_OBJECT_MAPPING_ERROR"; - - public static final String HTTP_CONNECTION_ERROR = "CLIENT_HTTP_CONNECTION_ERROR"; - - private String code; - - private String message; - - private boolean recoverable; - - public FlamingockError() { - } - - public FlamingockError(String code, boolean recoverable, String message) { - this.code = code; - this.recoverable = recoverable; - this.message = message; - } - - public String getCode() { - return code; - } - - public void setCode(String code) { - this.code = code; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public boolean isRecoverable() { - return recoverable; - } - - public void setRecoverable(boolean recoverable) { - this.recoverable = recoverable; - } - - @Override - public String toString() { - return "FlamingockError{" + - "code='" + code + '\'' + - ", message='" + message + '\'' + - ", recoverable=" + recoverable + - '}'; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/JdkUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/JdkUtil.java deleted file mode 100644 index 42daa8b80..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/JdkUtil.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.net.ContentHandlerFactory; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -public final class JdkUtil { - - private JdkUtil() { - } - - private static final List jdkInternalPackages = Arrays.asList( - "java.", - "com.sun.", - "javax.", - "jdk.", - "sun.", - "netscape.javascript.", - "org.ietf.jgss.", - "org.w3c.", - "org.xml."); - - public static boolean isInternalJdkClass(Class clazz) { - return clazz.isPrimitive() - || isJdkNativeType(clazz) - || isJdkDataStructure(clazz) - || isInternalJdkPackage(clazz) - || isOtherWellKnownClassesNonProxiable(clazz); - } - - private static boolean isInternalJdkPackage(Class clazz) { - //Some JDK internal classes return null in method getPackage() - String packageName = clazz.getPackage() != null ? clazz.getPackage().getName() : clazz.getName(); - return jdkInternalPackages.stream().anyMatch(packageName::startsWith); - } - - //should be added all the extra classes that shouldn't be proxiable - private static boolean isOtherWellKnownClassesNonProxiable(Class clazz) { - return ContentHandlerFactory.class.isAssignableFrom(clazz); - } - - private static boolean isJdkNativeType(Class clazz) { - return Boolean.class.equals(clazz) - || String.class.equals(clazz) - || Class.class.equals(clazz) - || Character.class.equals(clazz) - || Byte.class.equals(clazz) - || Short.class.equals(clazz) - || Integer.class.equals(clazz) - || Long.class.equals(clazz) - || Float.class.equals(clazz) - || Double.class.equals(clazz) - || Void.class.equals(clazz); - } - - private static boolean isJdkDataStructure(Class clazz) { - return Iterable.class.isAssignableFrom(clazz) - || Map.class.isAssignableFrom(clazz); - //should be added all the JDK data structure that shouldn't be proxied - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/JsonObjectMapper.java b/utils/general-util/src/main/java/io/flamingock/internal/util/JsonObjectMapper.java deleted file mode 100644 index f9cb10947..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/JsonObjectMapper.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -import static com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS; - -public final class JsonObjectMapper { - - private JsonObjectMapper(){} - - public final static ObjectMapper DEFAULT_INSTANCE = JsonMapper.builder() - .addModule(new Jdk8Module()) - .addModule(new JavaTimeModule()) - .enable(ACCEPT_CASE_INSENSITIVE_ENUMS) - .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .serializationInclusion(JsonInclude.Include.NON_NULL) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .disable(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS) - .build(); -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/NotThreadSafe.java b/utils/general-util/src/main/java/io/flamingock/internal/util/NotThreadSafe.java deleted file mode 100644 index 241d868b2..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/NotThreadSafe.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface NotThreadSafe { - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/ObjectUtils.java b/utils/general-util/src/main/java/io/flamingock/internal/util/ObjectUtils.java deleted file mode 100644 index c0f69f301..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/ObjectUtils.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -public class ObjectUtils { - - private ObjectUtils() { - throw new AssertionError("Instances of ObjectUtils not allowed"); - } - - public static T requireNonNull(T obj) { - if (obj == null) - throw new NullPointerException(); - return obj; - } - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/Pair.java b/utils/general-util/src/main/java/io/flamingock/internal/util/Pair.java deleted file mode 100644 index 98e9268c0..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/Pair.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.util.Objects; - -public class Pair { - private final A first; - private final B second; - - public Pair(A first, B second) { - this.first = first; - this.second = second; - } - - public A getFirst() { - return first; - } - - public B getSecond() { - return second; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Pair)) return false; - Pair pair = (Pair) o; - return Objects.equals(first, pair.first) && Objects.equals(second, pair.second); - } - - @Override - public int hashCode() { - return Objects.hash(first, second); - } - - @Override - public String toString() { - return "Pair{first=" + first + ", second=" + second + "}"; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/Property.java b/utils/general-util/src/main/java/io/flamingock/internal/util/Property.java deleted file mode 100644 index 7b185781e..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/Property.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -public interface Property { - - String getKey(); -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/ReflectionUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/ReflectionUtil.java deleted file mode 100644 index 681ef3b29..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/ReflectionUtil.java +++ /dev/null @@ -1,586 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; -import java.lang.annotation.Annotation; -import java.lang.reflect.Array; -import java.lang.reflect.Constructor; -import java.lang.reflect.Executable; -import java.lang.reflect.GenericArrayType; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; -import java.lang.reflect.WildcardType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -public final class ReflectionUtil { - private ReflectionUtil() {} - - /** - * Retrieves the actual type arguments used in a class's generic superclass as Class objects. - * This method traverses the class hierarchy to find the first parameterized superclass - * and returns its type arguments as Class objects. - * - * @param clazz The class to analyze for generic type arguments - * @return An array of Class objects representing the actual type arguments - * @throws IllegalStateException If no parameterized superclass can be found in the hierarchy - * @throws ClassCastException If any type argument is not a Class (e.g., type variable, wildcard) - */ - @SuppressWarnings("unchecked") - public static Class[] getActualTypeArguments(Class clazz) { - if (clazz == null) { - throw new IllegalArgumentException("Class cannot be null"); - } - - Class currentClass = clazz; - while (currentClass != null && currentClass != Object.class) { - // Check superclass for generic type parameters - Type genericSuperclass = currentClass.getGenericSuperclass(); - if (genericSuperclass instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) genericSuperclass; - Type[] typeArgs = pt.getActualTypeArguments(); - Class[] classArgs = new Class[typeArgs.length]; - - for (int i = 0; i < typeArgs.length; i++) { - if (!(typeArgs[i] instanceof Class)) { - throw new ClassCastException("Type argument " + typeArgs[i] + " is not a Class"); - } - classArgs[i] = (Class) typeArgs[i]; - } - - return classArgs; - } - - // Check interfaces for generic type parameters - for (Type genericInterface : currentClass.getGenericInterfaces()) { - if (genericInterface instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) genericInterface; - Type[] typeArgs = pt.getActualTypeArguments(); - Class[] classArgs = new Class[typeArgs.length]; - - for (int i = 0; i < typeArgs.length; i++) { - if (!(typeArgs[i] instanceof Class)) { - throw new ClassCastException("Type argument " + typeArgs[i] + " is not a Class"); - } - classArgs[i] = (Class) typeArgs[i]; - } - - return classArgs; - } - } - - currentClass = currentClass.getSuperclass(); - } - - throw new IllegalStateException("Unable to determine generic type arguments from class hierarchy"); - } - - /** - * Resolves the type arguments that {@code concreteClass} supplies for the generic superclass or interface {@code targetGeneric}. - * - * @throws IllegalArgumentException if {@code targetGeneric} is not in the hierarchy of {@code concreteClass}. - */ - public static Type[] resolveTypeArguments(Class concreteClass, Class targetGeneric) { - Objects.requireNonNull(concreteClass, "concreteClass"); - Objects.requireNonNull(targetGeneric, "targetGeneric"); - Map, Type> assigns = new HashMap<>(); - Type[] result = resolveUpwards(concreteClass, targetGeneric, assigns); - if (result == null) { - throw new IllegalArgumentException( - "The target type " + targetGeneric.getName() + " is not in the hierarchy of " + concreteClass.getName()); - } - return result; - } - - /** Convenience overload: uses the runtime class of the given instance. */ - public static Type[] resolveTypeArguments(Object instance, Class targetGeneric) { - return resolveTypeArguments(instance.getClass(), targetGeneric); - } - - /** Variant returning raw classes (defaults to Object.class if resolution fails). */ - public static Class[] resolveTypeArgumentsAsClasses(Class concreteClass, Class targetGeneric) { - Type[] types = resolveTypeArguments(concreteClass, targetGeneric); - Class[] classes = new Class[types.length]; - for (int i = 0; i < types.length; i++) { - classes[i] = toClass(types[i]); - if (classes[i] == null) classes[i] = Object.class; - } - return classes; - } - - /** - * Searches the hierarchy (both classes and interfaces) for a path to {@code targetGeneric}. - * At each step, binds the type variables of the raw supertype to the actual arguments in the current context. - * Returns the final Types corresponding to the type parameters of {@code targetGeneric}, or null if no path exists. - */ - private static Type[] resolveUpwards(Class current, Class targetGeneric, - Map, Type> assigns) { - if (current == null || current == Object.class) return null; - - if (current == targetGeneric) { - // We are exactly at the target generic class/interface: resolve its own type parameters - TypeVariable[] params = current.getTypeParameters(); - Type[] out = new Type[params.length]; - for (int i = 0; i < params.length; i++) { - out[i] = resolve(params[i], assigns); - } - return out; - } - - // 1) Check generic superclass - Type superType = current.getGenericSuperclass(); - Type[] viaSuper = tryAscend(superType, targetGeneric, assigns); - if (viaSuper != null) return viaSuper; - - // 2) Check generic interfaces - for (Type itf : current.getGenericInterfaces()) { - Type[] viaItf = tryAscend(itf, targetGeneric, assigns); - if (viaItf != null) return viaItf; - } - - // 3) Continue via raw superclass (if non-parameterised) so as not to lose the path - Class rawSuper = current.getSuperclass(); - return resolveUpwards(rawSuper, targetGeneric, assigns); - } - - /** Attempts to ascend one step (superclass or interface), extending {@code assigns}, and continues the search. */ - private static Type[] tryAscend(Type superType, Class targetGeneric, Map, Type> assigns) { - if (superType == null) return null; - - if (superType instanceof ParameterizedType) { - ParameterizedType p = (ParameterizedType)superType; - Class raw = (Class) p.getRawType(); - Map, Type> next = new HashMap<>(assigns); - TypeVariable[] params = raw.getTypeParameters(); - Type[] actualTypeArguments = p.getActualTypeArguments(); - for (int i = 0; i < params.length; i++) { - next.put(params[i], resolve(actualTypeArguments[i], assigns)); - } - return resolveUpwards(raw, targetGeneric, next); - } else if (superType instanceof Class) { - // No type parameters at this step - Class c = (Class)superType; - return resolveUpwards(c, targetGeneric, assigns); - } - return null; - } - - /** Recursively resolves a {@link Type} using the accumulated assignments. */ - private static Type resolve(Type t, Map, Type> assigns) { - while (t instanceof TypeVariable) { - TypeVariable tv = (TypeVariable)t; - Type mapped = assigns.get(tv); - if (mapped == null) return tv; // Not yet resolved - t = mapped; - } - if (t instanceof WildcardType) { - WildcardType w = (WildcardType)t; - Type[] upper = w.getUpperBounds(); - return upper.length > 0 ? resolve(upper[0], assigns) : Object.class; - } - if (t instanceof ParameterizedType) { - // Keep the ParameterizedType — caller can access its raw type or arguments - return t; - } - if (t instanceof GenericArrayType) { - GenericArrayType ga = (GenericArrayType)t; - Type comp = resolve(ga.getGenericComponentType(), assigns); - Class compClass = toClass(comp); - if (compClass != null) { - return Array.newInstance(compClass, 0).getClass(); - } - return ga; // Return generic array type if class cannot be materialised - } - return t; // Already a Class or other usable type - } - - /** Converts a {@link Type} to a {@link Class} where possible; if ParameterizedType, returns its raw type. */ - private static Class toClass(Type t) { - if (t instanceof Class) return (Class)t; - if (t instanceof ParameterizedType) return (Class) ((ParameterizedType)t).getRawType(); - if (t instanceof GenericArrayType) { - GenericArrayType ga = (GenericArrayType)t; - Class comp = toClass(ga.getGenericComponentType()); - return comp != null ? Array.newInstance(comp, 0).getClass() : null; - } - if (t instanceof TypeVariable || t instanceof WildcardType) return Object.class; - return null; - } - - - @SuppressWarnings("unchecked") - public static Optional findFirstAnnotatedMethod(Class source, Class annotation) { - return Arrays.stream(source.getMethods()) - .filter(method -> method.isAnnotationPresent(annotation)) - .findFirst(); - } - - @SuppressWarnings("unchecked") - public static Optional findFirstMethodByName(Class source, String methodName) { - return Arrays.stream(source.getMethods()) - .filter(method -> method.getName().equals(methodName)) - .findFirst(); - } - - - public static List> getParameters(Executable executable) { - return Arrays.asList(executable.getParameterTypes()); - } - - public static List> getAnnotatedConstructors(Class source, Class annotationClass) { - return getConstructors(source) - .stream() - .filter(constructor -> isConstructorAnnotationPresent(constructor, annotationClass)) - .collect(Collectors.toList()); - } - - public static Constructor getConstructorWithAnnotationPreference(Class source, Class annotationClass) { - List> annotatedConstructors = ReflectionUtil.getAnnotatedConstructors(source, annotationClass); - if (annotatedConstructors.size() == 1) { - return annotatedConstructors.get(0); - } else if (annotatedConstructors.size() > 1) { - throw new MultipleAnnotatedConstructorsFound(); - } - Constructor[] constructors = source.getConstructors(); - if (constructors.length == 0) { - throw new ConstructorNotFound(); - } - if (constructors.length > 1) { - throw new MultipleConstructorsFound(); - } - return constructors[0]; - } - - /** - * Collects all instances of a specific annotation type from a class, its superclass hierarchy, and implemented interfaces. - * This method recursively traverses the inheritance chain and interface hierarchy, collecting declared annotations - * of the specified type from each level. Only directly declared annotations are collected (not inherited ones). - * Duplicate annotations from the same class/interface are avoided through cycle detection. - * - * @param The annotation type to collect - * @param clazz The class to start the search from - * @param annotationType The class object representing the annotation type to collect - * @return A list containing all found annotations of the specified type, ordered by traversal: starting class, - * then superclasses (bottom-up), then interfaces at each level. Returns an empty list if no annotations are found. - * @throws NullPointerException if clazz or annotationType is null - */ - public static List findAllAnnotations(Class clazz, Class annotationType) { - Set> visited = new HashSet<>(); - List result = new ArrayList<>(); - - findAllAnnotationsInternal(clazz, annotationType, visited, result); - - return result; - } - - private static void findAllAnnotationsInternal(Class clazz, Class annotationType, Set> visited, List result) { - if (clazz == null || clazz == Object.class || !visited.add(clazz)) return; - - A annotation = clazz.getDeclaredAnnotation(annotationType); - if (annotation != null) { - result.add(annotation); - } - - findAllAnnotationsInternal(clazz.getSuperclass(), annotationType, visited, result); - - for (Class iface : clazz.getInterfaces()) { - findAllAnnotationsInternal(iface, annotationType, visited, result); - } - } - - - public static List> getConstructors(Class source) { - return Arrays.stream(source.getConstructors()) - .collect(Collectors.toList()); - } - - private static boolean isConstructorAnnotationPresent(Constructor constructor, Class annotationClass) { - return constructor.isAnnotationPresent(annotationClass) ; - } - - public static class ConstructorNotFound extends RuntimeException { - } - - public static class MultipleAnnotatedConstructorsFound extends RuntimeException { - } - - public static class MultipleConstructorsFound extends RuntimeException { - } - - public static ExecutableElement getConstructorWithAnnotationPreference(TypeElement typeElement, Class annotationClass) { - List constructors = getConstructors(typeElement); - if (constructors.isEmpty()) { - throw new ReflectionUtil.ConstructorNotFound(); - } else if (constructors.size() == 1) { - return constructors.get(0); - } else { - List annotatedConstructors = filterAnnotatedConstructors(constructors, annotationClass); - if (annotatedConstructors.isEmpty()) { - throw new ReflectionUtil.MultipleConstructorsFound(); - } else if (annotatedConstructors.size() == 1) { - return annotatedConstructors.get(0); - } else { - throw new ReflectionUtil.MultipleAnnotatedConstructorsFound(); - } - } - } - - public static List filterAnnotatedConstructors(List constructorElements, Class annotationClass) { - return constructorElements - .stream() - .filter(constructor -> isConstructorAnnotationPresent(constructor, annotationClass)) - .collect(Collectors.toList()); - } - - public static List getConstructors(TypeElement typeElement) { - return typeElement.getEnclosedElements() - .stream() - .filter(e -> e.getKind() == ElementKind.CONSTRUCTOR) - .map(e -> (ExecutableElement)e) - .collect(Collectors.toList()); - } - - private static boolean isConstructorAnnotationPresent(Element constructorElement, Class annotationClass) { - return constructorElement.getAnnotation(annotationClass) != null; - } - - public static List getParametersTypesQualifiedNames(ExecutableElement element) { - List parameterTypes = new ArrayList<>(); - for (VariableElement param : element.getParameters()) { - TypeMirror paramType = param.asType(); - parameterTypes.add(paramType.toString()); // fully qualified name (e.g., java.lang.String) - } - return parameterTypes; - } - - - /** - * Finds the default constructor of a class - * - * @param targetClass the class where the constructor is declared - * @return the matching constructor - */ - public static Constructor getDefaultConstructor(Class targetClass) { - return getConstructorFromParameterTypeNames(targetClass, Collections.emptyList()); - } - - /** - * Finds a constructor that matches the provided parameter type names. - * - If parameterTypeNames is null/empty: return the default (no-arg) constructor, - * whether implicit or explicit. - * - Otherwise: search only declared constructors. - * * First, filter by same arity (parameter count). - * * If exactly one remains, return it. - * * If several remain, require an exact declared match by resolved types. - * - * @param targetClass the class where the constructor is declared - * @param parameterTypeNames list of parameter type names (e.g., "java.lang.String", "int[]") - * @return the matching constructor - */ - public static Constructor getConstructorFromParameterTypeNames( - Class targetClass, - List parameterTypeNames) { - - if (targetClass == null) { - throw new IllegalArgumentException("targetClass cannot be null"); - } - - // Case 1: default constructor (0 parameters) - if (parameterTypeNames == null || parameterTypeNames.isEmpty()) { - // Try explicit declared no-arg first - try { - return targetClass.getDeclaredConstructor(); - } catch (NoSuchMethodException ignore) { - // If there is no declared one, try the implicit/public one (inherited allowed) - try { - return targetClass.getConstructor(); - } catch (NoSuchMethodException e) { - throw new RuntimeException("No default constructor (implicit or explicit) found in " + targetClass.getName(), e); - } - } - } - - // Case 2: N > 0 parameters: search declared constructors only - final Constructor[] declared = targetClass.getDeclaredConstructors(); - final int paramCount = parameterTypeNames.size(); - - // Filter by arity - List> sameArity = Arrays.stream(declared) - .filter(c -> c.getParameterCount() == paramCount) - .collect(Collectors.toList()); - - if (sameArity.isEmpty()) { - throw new RuntimeException("No declared constructor in " + targetClass.getName() - + " with parameter count = " + paramCount); - } - if (sameArity.size() == 1) { - return sameArity.get(0); - } - - final Class[] exactTypes = toClasses(parameterTypeNames); - - try { - return targetClass.getDeclaredConstructor(exactTypes); - } catch (NoSuchMethodException e) { - String sig = "(" + Arrays.stream(exactTypes).map(Class::getTypeName).collect(Collectors.joining(", ")) + ")"; - throw new RuntimeException("No declared constructor in " + targetClass.getName() - + " exactly matching parameter types " + sig, e); - } - } - - /** - * Finds a declared method by name using your 3-step heuristic: - * 1) Filter declared methods by name. - * - If exactly one remains, return it. - * 2) Else, filter by parameter count == parameterTypeNames.size(). - * - If exactly one remains, return it. - * 3) Else, resolve the parameter type names to classes and call getDeclaredMethod(...) - * for an exact type match. If not found, throw. - * Notes: - * - Only declared methods are considered (no inheritance). - */ - public static Method getDeclaredMethodFromParameterTypeNames( - Class targetClass, - String methodName, - List parameterTypeNames) { - - if (targetClass == null) { - throw new RuntimeException("targetClass cannot be null"); - } - if (methodName == null || methodName.isEmpty()) { - throw new RuntimeException("methodName cannot be null or empty"); - } - - // Step 1: by name - List sameName = Arrays.stream(targetClass.getDeclaredMethods()) - .filter(m -> m.getName().equals(methodName)) - .collect(Collectors.toList()); - - if (sameName.isEmpty()) { - throw new RuntimeException("No declared method named '" + methodName - + "' found in " + targetClass.getName()); - } - else if (sameName.size() == 1) { - return sameName.get(0); - } - - // Step 2: by arity - final int paramCount = (parameterTypeNames == null) ? 0 : parameterTypeNames.size(); - List sameArity = sameName.stream() - .filter(m -> m.getParameterCount() == paramCount) - .collect(Collectors.toList()); - - if (sameArity.isEmpty()) { - throw new RuntimeException("No declared method named '" + methodName - + "' with parameter count = " + paramCount - + " found in " + targetClass.getName()); - } - else if (sameArity.size() == 1) { - return sameArity.get(0); - } - - // Step 3: exact type match via getDeclaredMethod(...) - final Class[] exactTypes = toClasses(parameterTypeNames); - - try { - return targetClass.getDeclaredMethod(methodName, exactTypes); - } catch (NoSuchMethodException e) { - String sig = "(" + Arrays.stream(exactTypes).map(Class::getTypeName).collect(Collectors.joining(", ")) + ")"; - throw new RuntimeException("No declared method named '" + methodName - + "' exactly matching parameter types " + sig - + " in " + targetClass.getName(), e); - } - } - - private static Class[] toClasses(List names) { - if (names == null || names.isEmpty()) return new Class[0]; - Class[] result = new Class[names.size()]; - for (int i = 0; i < names.size(); i++) { - result[i] = loadType(names.get(i).trim()); - } - return result; - } - - private static Class loadType(String typeName) { - // Direct primitive - Class primitive = getPrimitiveClassFromName(typeName); - if (primitive != null) return primitive; - - // Handle array suffix "[]" - int dims = 0; - while (typeName.endsWith("[]")) { - dims++; - typeName = typeName.substring(0, typeName.length() - 2); - } - - Class base; - Class primBase = getPrimitiveClassFromName(typeName); - if (primBase != null) { - base = primBase; - } else { - try { - base = Class.forName(typeName); - } - catch (ClassNotFoundException ignore) { - throw new RuntimeException("Class not found for parameter type name: " + typeName); - } - } - - if (dims == 0) return base; - - // Build multi-dimensional array type - int[] zeros = new int[dims]; - Object array = Array.newInstance(base, zeros); - return array.getClass(); - } - - /** - * Returns the primitive Class for a given primitive name, or null if not a primitive name. - * Accepted names: boolean, byte, short, char, int, long, float, double, void - */ - private static Class getPrimitiveClassFromName(String name) { - if (name == null) return null; - switch (name) { - case "boolean": return boolean.class; - case "byte": return byte.class; - case "short": return short.class; - case "char": return char.class; - case "int": return int.class; - case "long": return long.class; - case "float": return float.class; - case "double": return double.class; - case "void": return void.class; - default: return null; - } - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/Result.java b/utils/general-util/src/main/java/io/flamingock/internal/util/Result.java deleted file mode 100644 index d62c62b9e..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/Result.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -public abstract class Result { - - private static Ok okInstance; - - public static Ok OK() { - if (okInstance == null) { - okInstance = new Ok(); - } - return okInstance; - } - - - private Result() { - } - - public final boolean isError() { - return this instanceof Error; - } - - - public static class Ok extends Result { - public Ok() { - super(); - } - - } - - public static class Error extends Result { - - private final Throwable throwable; - - public Error(Throwable throwable) { - super(); - this.throwable = throwable; - } - - - public Throwable getError() { - return throwable; - } - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/ServerException.java b/utils/general-util/src/main/java/io/flamingock/internal/util/ServerException.java deleted file mode 100644 index 59f7c31c0..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/ServerException.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -public class ServerException extends RuntimeException { - - private final String request; - - private final String body; - - private final FlamingockError error; - - public ServerException(String request, - String body, - FlamingockError error) { - super(error.toString()); - this.request = request; - this.body = body; - this.error = error; - } - - public FlamingockError getError() { - return error; - } - - public String getRequestString() { - return request; - } - - public String getBodyString() { - return body; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/StopWatch.java b/utils/general-util/src/main/java/io/flamingock/internal/util/StopWatch.java deleted file mode 100644 index 8d26fd49d..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/StopWatch.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -public class StopWatch { - - private long startedAt = -1L; - - public static StopWatch getNoStarted() { - return new StopWatch(); - } - - public static StopWatch startAndGet() { - StopWatch stopWatch = new StopWatch(); - stopWatch.run(); - return stopWatch; - } - - private StopWatch() { - } - - /** - * Idempotent operation. If it's already started, it doesn't have effect - */ - public void run() { - if (isNotStarted()) { - startedAt = System.currentTimeMillis(); - } - } - - public void reset() { - startedAt = System.currentTimeMillis(); - } - - /** - * @return the current stopwatch's elapse - */ - public long getElapsed() { - if (isNotStarted()) { - return 0L; - } - return System.currentTimeMillis() - startedAt; - } - - public boolean hasReached(long limitMillis) { - return getElapsed() >= limitMillis; - } - - public boolean isNotStarted() { - return startedAt <= -1L; - } - - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/StreamUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/StreamUtil.java deleted file mode 100644 index 8dde66cb6..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/StreamUtil.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.util.Optional; -import java.util.function.Predicate; -import java.util.stream.Stream; - -public final class StreamUtil { - - public static Optional processUntil(Stream stream, Predicate predicate) { - return stream.filter(predicate).findFirst(); - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/StringUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/StringUtil.java deleted file mode 100644 index 6d6440ac8..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/StringUtil.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.net.InetAddress; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.UUID; - -public final class StringUtil { - private StringUtil() { - } - - public static String executionId() { - return String.format( - "%s-%s", - LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss_SSSSSSSSS")), - String.valueOf(UUID.randomUUID().getMostSignificantBits()).replace("-", "")); - } - - public static String hostname() { - return hostname(""); - } - - public static String hostname(String serviceIdentifier) { - String hostname; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (Exception e) { - hostname = "unknown-host"; - } - - if (!isEmpty(serviceIdentifier)) { - hostname += "-"; - hostname += serviceIdentifier; - } - return hostname; - } - - public static boolean isEmpty(CharSequence cs) { - return cs == null || cs.length() == 0; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/ThreadSleeper.java b/utils/general-util/src/main/java/io/flamingock/internal/util/ThreadSleeper.java deleted file mode 100644 index 362750da9..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/ThreadSleeper.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.function.Function; - -public class ThreadSleeper { - - private static final Logger logger = LoggerFactory.getLogger(ThreadSleeper.class); - - - private final long totalMaxTimeWaitingMillis; - private final StopWatch stopWatch; - private final Function exceptionThrower; - - public ThreadSleeper(long totalMaxTimeWaitingMillis, - Function exceptionThrower) { - this.totalMaxTimeWaitingMillis = totalMaxTimeWaitingMillis; - this.stopWatch = StopWatch.startAndGet(); - this.exceptionThrower = exceptionThrower; - } - - /** - * It checks if the threshold hasn't been reached. In that case it will decide if it waits the maximum allowed - * (maxTimeAllowedToWait) or less, which it's restricted by totalMaxTimeWaitingMillis - * @param maxTimeToWait Max time allowed to wait in this iteration. - */ - public void checkThresholdAndWait(long maxTimeToWait) { - if (stopWatch.hasReached(totalMaxTimeWaitingMillis)) { - throwException("Maximum waiting millis reached: " + totalMaxTimeWaitingMillis); - } - if (maxTimeToWait > 0) { - logger.info("Trying going to sleep for maximum {}ms", maxTimeToWait); - waitForMillis(maxTimeToWait); - } else { - logger.info("Not going to sleep. Because max time to wait[{}] is less than zero", maxTimeToWait); - } - } - - private void waitForMillis(long maxAllowedTimeToWait) { - try { - long timeToSleep = maxAllowedTimeToWait; - - //How log until max Time waiting reached - long remainingTime = getRemainingMillis(); - if (remainingTime <= 0) { - throwException("Maximum waiting millis reached: " + totalMaxTimeWaitingMillis); - } - - if (timeToSleep > remainingTime) { - timeToSleep = remainingTime; - } - logger.info("Going to sleep finally for {}ms", timeToSleep); - Thread.sleep(timeToSleep); - logger.info("Woke up"); - } catch (InterruptedException ex) { - logger.warn(ex.getMessage(), ex); - Thread.currentThread().interrupt(); - } - } - - private void throwException(String cause) { - throw exceptionThrower.apply(String.format( - "Quit trying to acquire the lock after %d millis[ %s ]", - stopWatch.getElapsed(), - cause)); - } - - private long getRemainingMillis() { - return totalMaxTimeWaitingMillis - stopWatch.getElapsed(); - } - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/ThrowableUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/ThrowableUtil.java deleted file mode 100644 index 5ea3c4984..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/ThrowableUtil.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.util.*; -import java.util.stream.Collectors; - -public final class ThrowableUtil { - private static final ObjectMapper MAPPER = new ObjectMapper(); - - // límite global de caracteres en la salida JSON - private static final int MAX_CHARS = 20000; - - private ThrowableUtil() { - } - - public static String messageOf(Throwable t) { - String msg = t.getMessage(); - return (msg != null && !msg.isEmpty()) ? msg : t.getClass().getName(); - } - - public static String serialize(Throwable e) { - return serialize(e, 100); - } - - public static String serialize(Throwable e, int maxFrames) { - if (e == null) { - return ""; - } - - Map errorInfo = new LinkedHashMap<>(); - errorInfo.put("type", e.getClass().getName()); - errorInfo.put("message", Objects.toString(e.getMessage(), "")); - - Throwable root = getRootCause(e); - if (root != null && root != e) { - Map rootInfo = new LinkedHashMap<>(); - rootInfo.put("type", root.getClass().getName()); - rootInfo.put("message", Objects.toString(root.getMessage(), "")); - errorInfo.put("rootCause", rootInfo); - } - - List> frames = Arrays.stream(e.getStackTrace()) - .limit(maxFrames) - .map(frame -> { - Map f = new LinkedHashMap<>(); - f.put("className", frame.getClassName()); - f.put("methodName", frame.getMethodName()); - f.put("fileName", frame.getFileName()); - f.put("lineNumber", frame.getLineNumber()); - return f; - }) - .collect(Collectors.toList()); - - errorInfo.put("stackTrace", frames); - - try { - String json = MAPPER.writeValueAsString(errorInfo); - if (json.length() > MAX_CHARS) { - return json.substring(0, MAX_CHARS) + "..."; - } - return json; - } catch (Exception ex) { - return "{\"type\":\"" + e.getClass().getName() + "\",\"serializationError\":\"" + ex.getMessage() + "\"}"; - } - } - - private static Throwable getRootCause(Throwable e) { - Throwable cause = e.getCause(); - if (cause == null) return null; - while (cause.getCause() != null) { - cause = cause.getCause(); - } - return cause; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/TimeService.java b/utils/general-util/src/main/java/io/flamingock/internal/util/TimeService.java deleted file mode 100644 index 35e3d47c9..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/TimeService.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import org.jetbrains.annotations.TestOnly; - -import java.time.Clock; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.Date; - -/** - * Extending `TimeUtil` to avoid hard coding instances(Instant.now, LocalDateTime.now(), etc.) - */ -public class TimeService { - - private static TimeService defaultInstance = new TimeService(); - - public static TimeService getDefault() { - return defaultInstance; - } - - @TestOnly - public static void setDefault(TimeService newDefault) { - defaultInstance = newDefault; - } - - /** - * @param millis milliseconds to add to the Date - * @return current date plus milliseconds passed as parameter - */ - public LocalDateTime currentDatePlusMillis(long millis) { - return LocalDateTime.now().plus(millis, ChronoUnit.MILLIS); - } - - /** - * @return current Date - */ - @Deprecated - public Date currentTimeOld() { - return new Date(System.currentTimeMillis()); - } - - - public LocalDateTime currentDateTime() { - return LocalDateTime.now(); - } - - public long currentMillis() { - return Instant.now().toEpochMilli(); - } - - - - - private Instant nowInstant() { - return Instant.now(Clock.systemDefaultZone()); - } - - public Instant nowPlusMillis(long millis) { - return nowInstant().plusMillis(millis); - } - - public boolean isPast(Instant moment) { - return nowInstant().isAfter(moment); - } - - public boolean isPast(LocalDateTime dateTime) { - return currentDateTime().isAfter(dateTime); - } - - public long daysToMills(int days) { - return (long) days * 24 * 60 * 60 * 1000 ; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/TimeUtil.java b/utils/general-util/src/main/java/io/flamingock/internal/util/TimeUtil.java deleted file mode 100644 index 169e34bd0..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/TimeUtil.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.TimeZone; - -public final class TimeUtil { - private TimeUtil() { - } - - public static LocalDateTime fromIso8601(String dateFormatted) { - return LocalDateTime.parse(dateFormatted, DateTimeFormatter.ISO_DATE_TIME); - } - - public static LocalDateTime toLocalDateTime(Object value) { - if (value == null) { - return null; - } else if (value.getClass().equals(Date.class)) { - return toLocalDateTime((Date)value); - } else if (value.getClass().equals(LocalDateTime.class)) { - return (LocalDateTime) value; - } else if (value.getClass().equals(Long.class)) { - return LocalDateTime.ofInstant(Instant.ofEpochMilli((Long)value), TimeZone.getDefault().toZoneId()); - } - else { - throw new RuntimeException(String.format("%s cannot be cast to %s", value.getClass().getName(), Date.class.getName())); - } - } - - public static Date toDate(LocalDateTime localDateTime) { - return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); - } - - public static Date toDate(long epochMillis) { - LocalDateTime localDateTime = Instant. - ofEpochMilli(epochMillis) - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); - return toDate(localDateTime); - } - - public static LocalDateTime toLocalDateTime(Date date) { - return date.toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); - } - - /** - * Converts minutes to milliseconds - * - * @param minutes minutes to be converted - * @return equivalent to the minutes passed in milliseconds - */ - public static long millisToMinutes(long minutes) { - return minutes / (60 * 1000); - } - - public static long toMillis(LocalDateTime dateTime) { - return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); - } - - public static long diffInMillis(LocalDateTime dateTime1, LocalDateTime dateTime2) { - return toMillis(dateTime1) - toMillis(dateTime2); - } - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/TriConsumer.java b/utils/general-util/src/main/java/io/flamingock/internal/util/TriConsumer.java deleted file mode 100644 index 8a8a86f89..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/TriConsumer.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.util.Objects; - -@FunctionalInterface -public interface TriConsumer { - - void accept(T t, U u, V v); - - default TriConsumer andThen(TriConsumer after) { - Objects.requireNonNull(after); - - return (t, u, v) -> { - accept(t, u, v); - after.accept(t, u, v); - }; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/Trio.java b/utils/general-util/src/main/java/io/flamingock/internal/util/Trio.java deleted file mode 100644 index 382428666..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/Trio.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -import java.util.Objects; - -public class Trio { - private final A first; - private final B second; - private final C third; - - - public Trio(A first) { - this(first, null, null); - } - - public Trio(A first, B second) { - this(first, second, null); - } - - public Trio(A first, B second, C third) { - this.first = first; - this.second = second; - this.third = third; - } - - public A getFirst() { - return first; - } - - public B getSecond() { - return second; - } - - public C getThird() { - return third; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Trio)) return false; - Trio trio = (Trio) o; - return Objects.equals(first, trio.first) - && Objects.equals(second, trio.second) - && Objects.equals(third, trio.third); - } - - @Override - public int hashCode() { - return Objects.hash(first, second, third); - } - - @Override - public String toString() { - return "Trio{first=" + first + ", second=" + second + ", third=" + third + "}"; - } -} - diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/Wrapper.java b/utils/general-util/src/main/java/io/flamingock/internal/util/Wrapper.java deleted file mode 100644 index 16cd6d8f6..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/Wrapper.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util; - -/** - * Simple mutable holder for a value. - *

- * This class is not thread-safe. It is intended for use in scenarios - * such as capturing and mutating a variable from within a lambda expression. - * - * @param the type of value being wrapped - */ -public final class Wrapper { - private T value; - - public Wrapper() { - } - - public Wrapper(T value) { - this.value = value; - } - - /** - * Returns the current value. - */ - public T getValue() { - return value; - } - - /** - * Sets a new value. - */ - public void setValue(T value) { - this.value = value; - } - - @Override - public String toString() { - return String.valueOf(value); - } -} \ No newline at end of file diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/constants/AuditEntryFieldConstants.java b/utils/general-util/src/main/java/io/flamingock/internal/util/constants/AuditEntryFieldConstants.java deleted file mode 100644 index 14e8e31cd..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/constants/AuditEntryFieldConstants.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.constants; - -public class AuditEntryFieldConstants { - public static final String KEY_EXECUTION_ID = "executionId"; - public static final String KEY_STAGE_ID = "stageId"; - public static final String KEY_CHANGE_ID = "changeId"; - public static final String KEY_AUTHOR = "author"; - public static final String KEY_CREATED_AT = "createdAt"; - public static final String KEY_STATE = "state"; - public static final String KEY_TYPE = "type"; - public static final String KEY_INVOKED_CLASS = "invokedClass"; - public static final String KEY_INVOKED_METHOD = "invokedMethod"; - public static final String KEY_SOURCE_FILE = "sourceFile"; - public static final String KEY_METADATA = "metadata"; - public static final String KEY_EXECUTION_MILLIS = "executionMillis"; - public static final String KEY_EXECUTION_HOSTNAME = "executionHostname"; - public static final String KEY_ERROR_TRACE = "errorTrace"; - public static final String KEY_SYSTEM_CHANGE = "systemChange"; - public static final String KEY_TX_STRATEGY = "txStrategy"; - public static final String KEY_TARGET_SYSTEM_ID = "targetSystemId"; - public static final String KEY_CHANGE_ORDER = "changeOrder"; - public static final String KEY_RECOVERY_STRATEGY = "recoveryStrategy"; - public static final String KEY_TRANSACTION_FLAG = "transactionFlag"; -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/constants/CommunityPersistenceConstants.java b/utils/general-util/src/main/java/io/flamingock/internal/util/constants/CommunityPersistenceConstants.java deleted file mode 100644 index cf7a1e3f6..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/constants/CommunityPersistenceConstants.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2025 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.constants; - -public final class CommunityPersistenceConstants { - - public static final String DEFAULT_AUDIT_STORE_NAME = "flamingockAuditLog"; - public static final String DEFAULT_LOCK_STORE_NAME = "flamingockLock"; - public static final String DEFAULT_MARKER_STORE_NAME = "flamingockAuditMarker"; - - private CommunityPersistenceConstants() { - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/http/Http.java b/utils/general-util/src/main/java/io/flamingock/internal/util/http/Http.java deleted file mode 100644 index b1811c346..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/http/Http.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.http; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.flamingock.internal.util.JsonObjectMapper; -import io.flamingock.internal.util.id.RunnerId; -import io.flamingock.internal.util.ServerException; -import io.flamingock.internal.util.FlamingockError; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.Map; - -import static io.flamingock.internal.util.FlamingockError.GENERIC_ERROR; -import static io.flamingock.internal.util.FlamingockError.HTTP_CONNECTION_ERROR; -import static io.flamingock.internal.util.FlamingockError.OBJECT_MAPPING_ERROR; - -public final class Http { - - private static final String RUNNER_ID_HEADER_NAME = "flamingock-runner-id"; - private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; - - private enum Method { - GET, POST, PUT, DELETE - } - - - public static final RequestBuilderFactory DEFAULT_INSTANCE = Http.builderFactory(HttpClients.createDefault(), JsonObjectMapper.DEFAULT_INSTANCE); - - public static RequestBuilderFactory builderFactory(CloseableHttpClient client, - ObjectMapper objectMapper) { - return new RequestBuilderFactory(client, objectMapper); - } - - private Http() { - } - - - public static class RequestBuilderFactory implements AutoCloseable { - - private final CloseableHttpClient client; - - private final ObjectMapper objectMapper; - - private RequestBuilderFactory(CloseableHttpClient client, ObjectMapper objectMapper) { - this.client = client; - this.objectMapper = objectMapper; - } - - public RequestBuilder getRequestBuilder(String host) { - return new RequestBuilder(host, client, objectMapper); - } - - @Override - public void close() throws IOException { - if(client != null) { - client.close(); - } - } - } - - public static class RequestBuilder { - private final CloseableHttpClient client; - - private final ObjectMapper objectMapper; - - private final Map sharedHeaders = new HashMap<>(); - - private final String host; - - - private RequestBuilder(String host, - CloseableHttpClient client, - ObjectMapper objectMapper) { - this.host = host; - this.client = client; - this.objectMapper = objectMapper; - } - - public RequestWithBody POST(String pathTemplate) { - return newRequestWithBody(Method.POST, pathTemplate); - } - - public Request GET(String pathTemplate) { - return newRequest(Method.POST, pathTemplate); - } - - public RequestWithBody PUT(String pathTemplate) { - return newRequestWithBody(Method.PUT, pathTemplate); - } - - public Request DELETE(String pathTemplate) { - return newRequest(Method.DELETE, pathTemplate); - } - - private Request newRequest(Method method, String pathTemplate) { - return new Request(host, pathTemplate, method, client, objectMapper, new HashMap<>(sharedHeaders)); - } - - private RequestWithBody newRequestWithBody(Method method, String pathTemplate) { - return new RequestWithBody(host, pathTemplate, method, client, objectMapper, new HashMap<>(sharedHeaders)); - } - } - - public static abstract class AbstractRequest> { - - - protected static final Logger logger = LoggerFactory.getLogger(Http.class); - - protected final CloseableHttpClient client; - - protected final ObjectMapper objectMapper; - - protected final String host; - - protected final String pathTemplate; - - protected final Map headers; - - protected final Map pathParameters; - - protected final Map queryParameters; - - protected final Method method; - - protected boolean json = true; - - protected AbstractRequest(String host, - String pathTemplate, - Method method, - CloseableHttpClient client, - ObjectMapper objectMapper, - Map headers) { - this.host = host; - this.pathTemplate = pathTemplate; - this.method = method; - this.client = client; - this.objectMapper = objectMapper; - this.headers = headers; - this.pathParameters = new HashMap<>(); - this.queryParameters = new HashMap<>(); - } - - public SELF notJson() { - json = false; - return getInstance(); - } - - public SELF addPathParameter(String paramName, Object paramValue) { - pathParameters.put(paramName, paramValue); - return getInstance(); - } - - public SELF addQueryParameter(String paramName, Object paramValue) { - queryParameters.put(paramName, paramValue); - return getInstance(); - } - - public SELF addHeader(String headerName, String value) { - headers.put(headerName, value); - return getInstance(); - } - - public SELF withBearerToken(String token) { - headers.put(AUTHORIZATION_HEADER_NAME, "Bearer "+ token); - return getInstance(); - } - - public SELF withRunnerId(RunnerId runnerId) { - addHeader(RUNNER_ID_HEADER_NAME, runnerId.toString()); - return getInstance(); - } - - public String getRequestString() { - return String.format("%s %s", method.toString(), getFinalUrl()); - } - - public void execute() { - executeInternal(null); - } - - public T execute(Class type) { - if (type == null) { - throw new RuntimeException("Response type expected not to be null"); - } - return executeInternal(type); - } - - private T executeInternal(Class type) { - if (json) { - headers.put("Content-Type", "application/json"); - } - String url = getFinalUrl(); - HttpRequestBase request = getRequest(url); - setHeaders(request); - try (CloseableHttpResponse response = client.execute(request)) { - int statusCode = response.getStatusLine().getStatusCode(); - if (statusCode >= 200 && statusCode < 300) { - if (type != null && response.getEntity() != null) { - try { - return mapResult(response.getEntity(), type); - - } catch (IOException ex) { - throw new ServerException( - request.toString(), - getBodyIfPresent(), - new HttpFlamingockError(statusCode, - OBJECT_MAPPING_ERROR, - String.format("Http connection OK. Error mapping to[%s] the response:\n %s", - type.getSimpleName(), - response.getEntity().toString() - ))); - } - } else { - return null; - } - - } else { - if (response.getEntity() != null) { - FlamingockError error; - try { - error = mapResult(response.getEntity(), FlamingockError.class); - - } catch (Throwable ex) { - error = new FlamingockError(GENERIC_ERROR, false, response.getEntity().toString()); - } - - throw new ServerException(request.toString(), getBodyIfPresent(), new HttpFlamingockError(statusCode, error)); - } else { - throw new ServerException(request.toString(), getBodyIfPresent(), new HttpFlamingockError(statusCode, GENERIC_ERROR, "No error info returned")); - } - } - - } catch (ServerException e) { - throw e; - } catch (IOException e) { - throw new ServerException(request.toString(), getBodyIfPresent(), new FlamingockError(HTTP_CONNECTION_ERROR, false, e.getMessage())); - } catch (Throwable e) { - throw new ServerException(request.toString(), getBodyIfPresent(), new FlamingockError(GENERIC_ERROR, false, e.getMessage())); - } - } - - private String getBodyIfPresent() { - if (this instanceof RequestWithBody) { - RequestWithBody request = (RequestWithBody) this; - return request.body != null ? request.body.toString() : null; - } else { - return null; - } - } - - - private T mapResult(HttpEntity responseEntity, Class type) throws IOException { - T mappedBody; - String responseBody = EntityUtils.toString(responseEntity); - logger.trace("Response Status Code: {}", responseBody); - mappedBody = objectMapper.readValue(responseBody, type); - return mappedBody; - } - - private String getFinalUrl() { - try { - String path = pathTemplate; - for (Map.Entry entry : pathParameters.entrySet()) { - path = path.replace("{" + entry.getKey() + "}", entry.getValue().toString()); - } - - URIBuilder uriBuilder = new URIBuilder(host) - .setPath(path); - queryParameters.entrySet() - .stream() - .filter(entry -> entry.getKey() != null && entry.getValue() != null) - .forEach(entry -> uriBuilder.addParameter(entry.getKey(), entry.getValue().toString())); - return uriBuilder.toString(); - - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - - } - - protected HttpRequestBase getRequest(String url) { - switch (method) { - case GET: - return new HttpGet(url); - case POST: - return new HttpPost(url); - case PUT: - return new HttpPut(url); - case DELETE: - return new HttpDelete(url); - default: - throw new RuntimeException("Not found http method: " + method); - } - } - - private void setHeaders(HttpRequestBase request) { - headers.forEach(request::setHeader); - } - - private static boolean isSuccessfull(org.apache.http.HttpResponse response) { - int statusCode = response.getStatusLine().getStatusCode(); - logger.trace("Response Status Code: {}", statusCode); - return statusCode >= 200 && statusCode < 300; - } - - protected abstract SELF getInstance(); - } - - public static class Request extends AbstractRequest { - - protected Request(String host, - String pathTemplate, - Method method, - CloseableHttpClient client, - ObjectMapper objectMapper, - Map headers) { - super(host, pathTemplate, method, client, objectMapper, headers); - } - - @Override - protected Request getInstance() { - return this; - } - } - - public static class RequestWithBody extends AbstractRequest { - - - private Object body; - - private RequestWithBody(String host, - String pathTemplate, - Method method, - CloseableHttpClient client, - ObjectMapper objectMapper, - Map headers) { - super(host, pathTemplate, method, client, objectMapper, headers); - } - - public RequestWithBody setBody(Object body) { - this.body = body; - return this; - } - - @Override - public String getRequestString() { - try { - return super.getRequestString() + "\n" + objectMapper.writeValueAsString(body); - } catch (JsonProcessingException e) { - logger.warn(e.getMessage(), e); - return super.getRequestString(); - } - } - - protected HttpEntityEnclosingRequestBase getRequest(String url) { - HttpEntityEnclosingRequestBase request; - switch (method) { - case POST: - request = new HttpPost(url); - break; - case PUT: - request = new HttpPut(url); - break; - default: - throw new RuntimeException("Not found http method: " + method); - } - addRequestBody(request); - return request; - } - - @Override - protected RequestWithBody getInstance() { - return this; - } - - - private void addRequestBody(HttpEntityEnclosingRequestBase request) { - if (body == null) { - throw new RuntimeException(String.format("%s request requires non-null body", method)); - } - try { - request.setEntity(new StringEntity(objectMapper.writeValueAsString(body))); - - } catch (UnsupportedEncodingException | JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - } - -} \ No newline at end of file diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/http/HttpFlamingockError.java b/utils/general-util/src/main/java/io/flamingock/internal/util/http/HttpFlamingockError.java deleted file mode 100644 index 795dedefd..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/http/HttpFlamingockError.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.http; - -import io.flamingock.internal.util.FlamingockError; - -public class HttpFlamingockError extends FlamingockError { - - private final int statusCode; - - public HttpFlamingockError(int statusCode, String errorCode, String message) { - super(errorCode, statusCode < 500, message); - this.statusCode = statusCode; - } - - public HttpFlamingockError(int statusCode, FlamingockError error) { - super(error.getCode(), error.isRecoverable(), error.getMessage()); - this.statusCode = statusCode; - } - - public int getStatusCode() { - return statusCode; - } - - @Override - public String toString() { - return "Error{httpStatus[" + statusCode + "], code[" + getCode() + "], message['" + getMessage() + "', recoverable[" + isRecoverable() + "]]"; - } - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/id/EnvironmentId.java b/utils/general-util/src/main/java/io/flamingock/internal/util/id/EnvironmentId.java deleted file mode 100644 index a9c74c846..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/id/EnvironmentId.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.id; - -import io.flamingock.internal.util.Property; - -public class EnvironmentId extends Id implements Property { - - private final static String PROPERTY_KEY = "cloud.environment.id"; - - public static EnvironmentId fromString(String value) { - return new EnvironmentId(value); - } - - private EnvironmentId(String value) { - super(value); - } - - @Override - public boolean equals(Object o) { - return super.equals(o) && o instanceof EnvironmentId; - } - - - @Override - public String getKey() { - return PROPERTY_KEY; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/id/Id.java b/utils/general-util/src/main/java/io/flamingock/internal/util/id/Id.java deleted file mode 100644 index fb3bd5fb1..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/id/Id.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.id; - -public abstract class Id { - - private final String value; - - protected Id(String value) { - if(value == null || value.isEmpty()) { - String name = this.getClass().getSimpleName(); - throw new RuntimeException(name + " cannot be null or empty"); - } - this.value = value; - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Id that = (Id) o; - - return value.equals(that.value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/id/JwtProperty.java b/utils/general-util/src/main/java/io/flamingock/internal/util/id/JwtProperty.java deleted file mode 100644 index 7c6034da9..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/id/JwtProperty.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.id; - -import io.flamingock.internal.util.Property; - -public class JwtProperty extends Id implements Property { - - private final static String PROPERTY_KEY = "cloud.jwt"; - - public static JwtProperty fromString(String value) { - return new JwtProperty(value); - } - - private JwtProperty(String value) { - super(value); - } - - @Override - public boolean equals(Object o) { - return super.equals(o) && o instanceof JwtProperty; - } - - - @Override - public String getKey() { - return PROPERTY_KEY; - } -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/id/RunnerId.java b/utils/general-util/src/main/java/io/flamingock/internal/util/id/RunnerId.java deleted file mode 100644 index 8e649ea61..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/id/RunnerId.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.id; - -import io.flamingock.internal.util.Property; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.Inet4Address; -import java.net.UnknownHostException; -import java.util.UUID; - -/** - * {@code RunnerId} represents a unique identifier for a specific runner execution. - *

- * The ID format is: {@code @#}, where: - *

- *

- * This format is optimized for: - *

    - *
  • Uniqueness across executions
  • - *
  • Traceability across services and instances
  • - *
  • Safe exposure in external systems and logs
  • - *
  • Persistence and log readability
  • - *
- */ -public final class RunnerId extends Id implements Property { - - private static final Logger logger = LoggerFactory.getLogger(RunnerId.class); - private static final String PROPERTY_KEY = "runner.id"; - - /** - * Generates a new {@link RunnerId} using a randomly generated service identifier. - * - * @return a unique {@code RunnerId} for the current execution - */ - public static RunnerId generate() { - return generate(null); - } - - /** - * Generates a new {@link RunnerId} using the provided service identifier. - *

- * If the service identifier or hostname is null or empty, a UUID will be used instead. - * - * @param serviceIdentifier the logical name of the service (can be null) - * @return a unique {@code RunnerId} for the current execution - */ - public static RunnerId generate(String serviceIdentifier) { - return new RunnerId(cleanOrUuid(serviceIdentifier) + "@" + resolveSafeHostname() + "#" + UUID.randomUUID()); - } - - /** - * Creates a {@link RunnerId} from a raw string. - * - * @param value the raw identifier value - * @return a {@code RunnerId} wrapping the given value - */ - public static RunnerId fromString(String value) { - return new RunnerId(value); - } - - private RunnerId(String value) { - super(value); - } - - @Override - public String getKey() { - return PROPERTY_KEY; - } - - - /** - * Returns the local hostname in a safe, sanitized format. - * If resolution fails or is blank, returns a new UUID instead. - */ - private static String resolveSafeHostname() { - try { - return cleanOrUuid(Inet4Address.getLocalHost().getHostName()); - } catch (UnknownHostException e) { - logger.warn("Unable to resolve hostname: {}", e.getMessage()); - return UUID.randomUUID().toString(); - } - } - - /** - * Returns the given input if non-blank, replacing any invalid characters. - * If the input is blank or null, returns a new UUID instead. - */ - private static String cleanOrUuid(String input) { - return (input == null || input.trim().isEmpty()) - ? UUID.randomUUID().toString() - : input.replaceAll("[^a-zA-Z0-9._-]", "_"); - } -} \ No newline at end of file diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/id/ServiceId.java b/utils/general-util/src/main/java/io/flamingock/internal/util/id/ServiceId.java deleted file mode 100644 index d14593fc1..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/id/ServiceId.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.id; - -import io.flamingock.internal.util.Property; - -public class ServiceId extends Id implements Property { - private final static String PROPERTY_KEY = "cloud.service.id"; - - public static ServiceId fromString(String value) { - return new ServiceId(value); - } - - private ServiceId(String value) { - super(value); - } - - @Override - public boolean equals(Object o) { - return super.equals(o) && o instanceof ServiceId; - } - - @Override - public String getKey() { - return PROPERTY_KEY; - } - -} diff --git a/utils/general-util/src/main/java/io/flamingock/internal/util/log/FlamingockLoggerFactory.java b/utils/general-util/src/main/java/io/flamingock/internal/util/log/FlamingockLoggerFactory.java deleted file mode 100644 index b5616f09b..000000000 --- a/utils/general-util/src/main/java/io/flamingock/internal/util/log/FlamingockLoggerFactory.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.internal.util.log; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Centralized logger factory for consistent Flamingock logger naming. - * - *

All Flamingock loggers use "FK-" prefix with 20-character alignment for clean log output. - * This factory ensures consistent branding and visual alignment across all components. - * - *

Compatible with GraalVM native image compilation - uses only safe string operations - * and Class.getSimpleName() to avoid reflection issues. - * - *

Usage Examples:

- *
- * // String-based logger (preferred for core components)
- * private static final Logger logger = FlamingockLoggerFactory.getLogger("ChangeExecution");
- * 
- * // Class-based logger (useful for community/platform components)  
- * private static final Logger logger = FlamingockLoggerFactory.getLogger(MyClass.class);
- * 
- * - *

Output Format:

- *
- * FK-ChangeExecution   - Starting change execution [change=create-user-table]
- * FK-Lock              - Process lock acquired successfully [duration=120ms]
- * FK-StageExecutor     - Stage execution completed [duration=1.2s tasks=3]
- * 
- * - * @since 6.0.0 - */ -public final class FlamingockLoggerFactory { - - private static final String PREFIX = "FK-"; - private static final int TOTAL_WIDTH = 20; - private static final int MAX_NAME_LENGTH = TOTAL_WIDTH - PREFIX.length(); // 17 chars - - private FlamingockLoggerFactory() { - // Utility class - prevent instantiation - } - - /** - * Create logger with component name. - * - *

The name will be prefixed with "FK-" and padded to exactly 20 characters - * for consistent alignment in log output. - * - * @param name the component name (e.g., "ChangeExecution", "Lock", "StageExecutor") - * @return SLF4J Logger instance with formatted name - */ - public static Logger getLogger(String name) { - String formattedName = formatLoggerName(name); - return LoggerFactory.getLogger(formattedName); - } - - /** - * Create logger with class type. - * - *

Uses the class simple name (Class.getSimpleName()) which is GraalVM native image safe. - * The name will be prefixed with "FK-" and padded to exactly 20 characters. - * - * @param clazz the class (simple name will be extracted and used) - * @return SLF4J Logger instance with formatted name - */ - public static Logger getLogger(Class clazz) { - String className = clazz.getSimpleName(); - String formattedName = formatLoggerName(className); - return LoggerFactory.getLogger(formattedName); - } - - /** - * Formats a name into the standard Flamingock logger format. - * - *

Rules: - *

    - *
  • Prefixed with "FK-"
  • - *
  • Truncated to 17 characters if longer (to fit within 20-char total)
  • - *
  • Right-padded with spaces to exactly 20 characters
  • - *
  • Null/empty names default to "Unknown"
  • - *
- * - * @param name the raw component or class name - * @return formatted logger name (exactly 20 characters) - */ - private static String formatLoggerName(String name) { - if (name == null || name.isEmpty()) { - name = "Unknown"; - } - - // Truncate if too long to fit within total width - if (name.length() > MAX_NAME_LENGTH) { - name = name.substring(0, MAX_NAME_LENGTH); - } - - // Format: "FK-ComponentName " (right-padded to exactly 20 chars) - return String.format("%-" + TOTAL_WIDTH + "s", PREFIX + name); - } -} \ No newline at end of file diff --git a/utils/sql-test-kit/build.gradle.kts b/utils/sql-test-kit/build.gradle.kts new file mode 100644 index 000000000..beb364614 --- /dev/null +++ b/utils/sql-test-kit/build.gradle.kts @@ -0,0 +1,13 @@ +dependencies { + implementation(project(":core:flamingock-core")) + implementation(project(":utils:sql-util")) + implementation(project(":utils:test-util")) +} + +description = "SQL TestKit for Flamingock testing" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} diff --git a/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlAuditStorage.java b/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlAuditStorage.java new file mode 100644 index 000000000..3c6b896c7 --- /dev/null +++ b/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlAuditStorage.java @@ -0,0 +1,181 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.sql.kit; + +import io.flamingock.core.kit.audit.AuditStorage; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.audit.AuditTxType; +import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.dialectHelpers.SqlAuditorDialectHelper; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; + +/** + * SQL implementation of AuditStorage for real database testing. + * Only depends on SQL client/database and core Flamingock classes. + * Does not depend on SQL-specific Flamingock components like SqlTargetSystem. + */ +public class SqlAuditStorage implements AuditStorage { + + private final DataSource dataSource; + private final SqlAuditorDialectHelper dialectHelper; + private final String auditTableName; + + public SqlAuditStorage(DataSource dataSource) throws SQLException { + this(dataSource, DEFAULT_AUDIT_STORE_NAME); + } + + public SqlAuditStorage(DataSource dataSource, String tableName) throws SQLException { + this.auditTableName = tableName; + this.dataSource = dataSource; + try (Connection conn = dataSource.getConnection()) { + this.dialectHelper = new SqlAuditorDialectHelper(conn); + } + } + + @Override + public void addAuditEntry(AuditEntry auditEntry) { + try (Connection connection = dataSource.getConnection()) { + // For Informix, ensure autoCommit is enabled for audit writes + if (dialectHelper.getSqlDialect() == SqlDialect.INFORMIX) { + connection.setAutoCommit(true); + } + + try (PreparedStatement ps = connection.prepareStatement( + dialectHelper.getInsertSqlString(auditTableName))) { + ps.setString(1, auditEntry.getExecutionId()); + ps.setString(2, auditEntry.getStageId()); + ps.setString(3, auditEntry.getTaskId()); + ps.setString(4, auditEntry.getAuthor()); + ps.setTimestamp(5, Timestamp.valueOf(auditEntry.getCreatedAt())); + ps.setString(6, auditEntry.getState() != null ? auditEntry.getState().name() : null); + ps.setString(7, auditEntry.getClassName()); + ps.setString(8, auditEntry.getMethodName()); + ps.setString(9, auditEntry.getSourceFile()); + ps.setString(10, auditEntry.getMetadata() != null ? auditEntry.getMetadata().toString() : null); + ps.setLong(11, auditEntry.getExecutionMillis()); + ps.setString(12, auditEntry.getExecutionHostname()); + ps.setString(13, auditEntry.getErrorTrace()); + ps.setString(14, auditEntry.getType() != null ? auditEntry.getType().name() : null); + ps.setString(15, auditEntry.getTxType() != null ? auditEntry.getTxType().name() : null); + ps.setString(16, auditEntry.getTargetSystemId()); + ps.setString(17, auditEntry.getOrder()); + ps.setString(18, auditEntry.getRecoveryStrategy() != null ? auditEntry.getRecoveryStrategy().name() : null); + ps.setObject(19, auditEntry.getTransactionFlag()); + ps.setObject(20, auditEntry.getSystemChange()); + ps.executeUpdate(); + } + } catch (SQLException e) { + throw new RuntimeException("Failed to add audit entry", e); + } + // Log but don't throw + } + + @Override + public List getAuditEntries() { + List entries = new ArrayList<>(); + try (Connection connection = dataSource.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(dialectHelper.getSelectHistorySqlString(auditTableName))) { + while (rs.next()) { + AuditEntry entry = toAuditEntry(rs); + entries.add(entry); + } + } catch (SQLException e) { + throw new RuntimeException("Failed to read audit history", e); + } + return entries; + } + + @Override + public List getAuditEntriesForChange(String changeId) { + List entries = new ArrayList<>(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps = connection.prepareStatement(dialectHelper.getSelectHistoryByChangeIdSqlString(auditTableName))) { + ps.setString(1, changeId); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + AuditEntry entry = toAuditEntry(rs); + entries.add(entry); + } + } + } catch (SQLException e) { + throw new RuntimeException("Failed to read audit history", e); + } + return entries; + } + + @Override + public long countAuditEntriesWithStatus(AuditEntry.Status status) { + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps = connection.prepareStatement(dialectHelper.getCountByStatusSqlString(auditTableName))) { + ps.setString(1, status.toString()); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getLong(1); + } + } + } catch (SQLException e) { + throw new RuntimeException("Failed to count audit entries with status: " + status, e); + } + return 0; + } + + @Override + public boolean hasAuditEntries() { + return !this.getAuditEntries().isEmpty(); + } + + @Override + public void clear() { + try(Connection connection = dataSource.getConnection(); + Statement stmt = connection.createStatement()) { + stmt.executeUpdate(String.format("DELETE FROM %s", auditTableName)); + } catch (SQLException e) { + throw new RuntimeException("Failed to clear audit entries", e); + } + } + + private AuditEntry toAuditEntry(ResultSet rs) throws SQLException { + return new AuditEntry( + rs.getString("execution_id"), + rs.getString("stage_id"), + rs.getString("change_id"), + rs.getString("author"), + rs.getTimestamp("created_at").toLocalDateTime(), + rs.getString("state") != null ? AuditEntry.Status.valueOf(rs.getString("state")) : null, + rs.getString("type") != null ? AuditEntry.ChangeType.valueOf(rs.getString("type")) : null, + rs.getString("invoked_class"), + rs.getString("invoked_method"), + rs.getString("source_file"), + rs.getLong("execution_millis"), + rs.getString("execution_hostname"), + rs.getString("metadata"), + rs.getBoolean("system_change"), + rs.getString("error_trace"), + AuditTxType.fromString(rs.getString("tx_strategy")), + rs.getString("target_system_id"), + rs.getString("change_order"), + rs.getString("recovery_strategy") != null ? io.flamingock.api.RecoveryStrategy.valueOf(rs.getString("recovery_strategy")) : null, + rs.getObject("transaction_flag") != null ? rs.getBoolean("transaction_flag") : null + ); + } +} diff --git a/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlLockStorage.java b/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlLockStorage.java new file mode 100644 index 000000000..5c980cec4 --- /dev/null +++ b/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlLockStorage.java @@ -0,0 +1,262 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.sql.kit; + +import io.flamingock.core.kit.lock.LockStorage; +import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.dialectHelpers.SqlLockDialectHelper; +import io.flamingock.internal.core.external.store.lock.LockAcquisition; +import io.flamingock.internal.core.external.store.lock.LockKey; +import io.flamingock.internal.core.external.store.lock.LockServiceException; +import io.flamingock.internal.core.external.store.lock.LockStatus; +import io.flamingock.internal.core.external.store.lock.community.CommunityLockEntry; +import io.flamingock.internal.util.id.RunnerId; + +import javax.sql.DataSource; +import java.sql.*; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME; + +/** + * SQL implementation of LockStorage for real database testing. + * Only depends on SQL client/database and core Flamingock classes. + * Does not depend on SQL-specific Flamingock components like SqlTargetSystem. + */ +public class SqlLockStorage implements LockStorage { + + private final String lockTableName; + private final Map metadata = new ConcurrentHashMap<>(); + private final DataSource dataSource; + private final SqlLockDialectHelper dialectHelper; + + public SqlLockStorage(DataSource dataSource) throws SQLException { + this(dataSource, DEFAULT_LOCK_STORE_NAME); + } + + public SqlLockStorage(DataSource dataSource, String lockTableName) throws SQLException { + this.lockTableName = lockTableName; + this.dataSource = dataSource; + try (Connection conn = dataSource.getConnection()) { + this.dialectHelper = new SqlLockDialectHelper(conn); + } + } + + @Override + public void storeLock(LockKey key, LockAcquisition acquisition) { + String keyStr = key.toString(); + RunnerId owner = acquisition.getOwner(); + LocalDateTime expiresAt = LocalDateTime.now().plusNanos(acquisition.getAcquiredForMillis() * 1_000_000); + + Connection connection = null; + try { + connection = dataSource.getConnection(); + // For Informix, use shorter timeout and simpler transaction handling + // For Sybase we MUST disable auto-commit so HOLDLOCK works as intended + boolean isInformix = dialectHelper.getSqlDialect() == SqlDialect.INFORMIX; + boolean isSybase = dialectHelper.getSqlDialect() == SqlDialect.SYBASE; + connection.setAutoCommit(isInformix); // Informix uses autocommit + + try { + + if (isSybase) { + // For Sybase, use HOLDLOCK to prevent race conditions during lock check + String selectSql = "SELECT lock_key, status, owner, expires_at " + + "FROM " + lockTableName + " HOLDLOCK " + + "WHERE lock_key = ?"; + + CommunityLockEntry existing = null; + try (PreparedStatement ps = connection.prepareStatement(selectSql)) { + ps.setString(1, keyStr); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + existing = new CommunityLockEntry( + rs.getString("lock_key"), + LockStatus.valueOf(rs.getString("status")), + rs.getString("owner"), + rs.getTimestamp("expires_at").toLocalDateTime() + ); + } + } + } + + if (existing == null || + owner.toString().equals(existing.getOwner()) || + LocalDateTime.now().isAfter(existing.getExpiresAt())) { + // Delete existing lock first, then insert new one + try (PreparedStatement delete = connection.prepareStatement( + "DELETE FROM " + lockTableName + " WHERE lock_key = ?")) { + delete.setString(1, keyStr); + delete.executeUpdate(); + } + dialectHelper.upsertLockEntry(connection, lockTableName, keyStr, owner.toString(), LockStatus.LOCK_HELD.name(), expiresAt); + connection.commit(); + return; + } else { + connection.rollback(); + throw new LockServiceException("upsert", keyStr, + "Still locked by " + existing.getOwner() + " until " + existing.getExpiresAt()); + } + } + + CommunityLockEntry existing = getLockEntry(connection, keyStr); + if (existing == null || + owner.toString().equals(existing.getOwner()) || + LocalDateTime.now().isAfter(existing.getExpiresAt())) { + dialectHelper.upsertLockEntry(connection, lockTableName, keyStr, owner.toString(), LockStatus.LOCK_HELD.name(), expiresAt); + // Commit for all dialects except Informix (which uses auto-commit above) + if (dialectHelper.getSqlDialect() != SqlDialect.INFORMIX) { + connection.commit(); + } + } else { + if (dialectHelper.getSqlDialect() != SqlDialect.INFORMIX) { + connection.rollback(); + } + throw new LockServiceException("upsert", keyStr, + "Still locked by " + existing.getOwner() + " until " + existing.getExpiresAt()); + } + } catch (Exception e) { + if (dialectHelper.getSqlDialect() != SqlDialect.INFORMIX) { + connection.rollback(); + } + throw e; + } finally { + if (dialectHelper.getSqlDialect() != SqlDialect.INFORMIX) { + connection.setAutoCommit(true); + } + } + } catch (SQLException e) { + throw new LockServiceException("upsert", keyStr, e.getMessage()); + } finally { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + // Log but don't throw + } + } + } + } + + @Override + public LockAcquisition getLock(LockKey key) { + try (Connection connection = dataSource.getConnection()) { + CommunityLockEntry entry = getLockEntry(connection, key.toString()); + if (entry != null) { + return new LockAcquisition(RunnerId.fromString(entry.getOwner()), + Timestamp.valueOf(entry.getExpiresAt()).getTime() - System.currentTimeMillis()); + } + } catch (SQLException e) { + // ignore + } + return null; + } + + private CommunityLockEntry getLockEntry(Connection conn, String key) throws SQLException { + try (PreparedStatement ps = conn.prepareStatement( + dialectHelper.getSelectLockSqlString(lockTableName))) { + + // Set query timeout for Informix to prevent long waits + if (dialectHelper.getSqlDialect() == SqlDialect.INFORMIX) { + ps.setQueryTimeout(5); + } + + ps.setString(1, key); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new CommunityLockEntry( + rs.getString(1), + LockStatus.valueOf(rs.getString("status")), + rs.getString("owner"), + rs.getTimestamp("expires_at").toLocalDateTime() + ); + } + } + } + return null; + } + + @Override + public Map getAllLocks() { + Map locks = new HashMap<>(); + + try (Connection connection = dataSource.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(dialectHelper.getSelectAllLocksSqlString(lockTableName))) { + while (rs.next()) { + LockKey key = LockKey.fromString(rs.getString(1)); + LockAcquisition acquisition = new LockAcquisition( + RunnerId.fromString(rs.getString("owner")), + Timestamp.valueOf(rs.getTimestamp("expires_at").toLocalDateTime()).getTime() - System.currentTimeMillis() + ); + locks.put(key, acquisition); + } + } catch (SQLException e) { + throw new RuntimeException("Failed to read locks from database", e); + } + + return locks; + } + + @Override + public void removeLock(LockKey key) { + try { + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps = connection.prepareStatement( + dialectHelper.getDeleteLockSqlString(lockTableName))) { + + // Set query timeout for Informix to prevent long waits + if (dialectHelper.getSqlDialect() == SqlDialect.INFORMIX) { + ps.setQueryTimeout(5); + } + ps.setString(1, key.toString()); + ps.executeUpdate(); + } + } catch (SQLException e) { + // ignore + } + } + + @Override + public boolean hasLocks() { + return !this.getAllLocks().isEmpty(); + } + + @Override + public void clear() { + try(Connection connection = dataSource.getConnection(); + Statement stmt = connection.createStatement()) { + stmt.executeUpdate(String.format("DELETE FROM %s", lockTableName)); + } catch (SQLException e) { + throw new RuntimeException("Failed to clear lock entries", e); + } + metadata.clear(); + } + + @Override + public void setMetadata(String key, Object value) { + metadata.put(key, value); + } + + @Override + public Object getMetadata(String key) { + return metadata.get(key); + } + +} diff --git a/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlTestKit.java b/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlTestKit.java new file mode 100644 index 000000000..9b6020e14 --- /dev/null +++ b/utils/sql-test-kit/src/main/java/io/flamingock/sql/kit/SqlTestKit.java @@ -0,0 +1,88 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.sql.kit; + +import io.flamingock.core.kit.AbstractTestKit; +import io.flamingock.core.kit.audit.AuditStorage; +import io.flamingock.core.kit.lock.LockStorage; +import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.dialectHelpers.SqlTestKitDialectHelper; +import io.flamingock.internal.core.external.store.CommunityAuditStore; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +public class SqlTestKit extends AbstractTestKit { + + private final DataSource dataSource; + private final SqlTestKitDialectHelper dialectHelper; + + public SqlTestKit(AuditStorage auditStorage, LockStorage lockStorage, CommunityAuditStore auditStore, DataSource dataSource) throws SQLException { + super(auditStorage, lockStorage, auditStore); + this.dataSource = dataSource; + try (Connection conn = dataSource.getConnection()) { + this.dialectHelper = new SqlTestKitDialectHelper(conn); + } + } + + @Override + public void cleanUp() { + try { + try (Connection connection = dataSource.getConnection()) { + if (dialectHelper.getSqlDialect() == SqlDialect.H2) { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("DROP ALL OBJECTS"); + } + return; + } + List tables = dialectHelper.getUserTables(connection); + + if (tables.isEmpty()) { + return; + } + + dialectHelper.disableForeignKeyChecks(connection); + try { + for (String tableName : tables) { + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate(dialectHelper.getDropTableSql(tableName)); + } catch (SQLException e) { + if (dialectHelper.getSqlDialect() != SqlDialect.SYBASE) { + throw e; + } + } + } + } finally { + dialectHelper.enableForeignKeyChecks(connection); + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Create a new SqlTestKit with client and database + */ + public static SqlTestKit create(CommunityAuditStore auditStore, DataSource dataSource) throws SQLException { + SqlAuditStorage auditStorage = new SqlAuditStorage(dataSource); + SqlLockStorage lockStorage = new SqlLockStorage(dataSource); + return new SqlTestKit(auditStorage, lockStorage, auditStore, dataSource); + } +} diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlAuditorDialectHelper.java b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlAuditorDialectHelper.java similarity index 91% rename from community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlAuditorDialectHelper.java rename to utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlAuditorDialectHelper.java index 924584a33..31a5ea4b5 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlAuditorDialectHelper.java +++ b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlAuditorDialectHelper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.store.sql.internal; +package io.flamingock.internal.common.sql.dialectHelpers; import io.flamingock.internal.common.sql.SqlDialectFactory; import io.flamingock.internal.common.sql.SqlDialect; @@ -279,19 +279,36 @@ public String getCreateTableSqlString(String tableName) { public String getInsertSqlString(String tableName) { return String.format( - "INSERT INTO %s (" + - "execution_id, stage_id, change_id, author, created_at, state, invoked_class, invoked_method, source_file, metadata, " + - "execution_millis, execution_hostname, error_trace, type, tx_strategy, target_system_id, change_order, recovery_strategy, transaction_flag, system_change" + - ") VALUES (" + - "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" + - ")", tableName); + "INSERT INTO %s (" + + "execution_id, stage_id, change_id, author, created_at, state, invoked_class, invoked_method, source_file, metadata, " + + "execution_millis, execution_hostname, error_trace, type, tx_strategy, target_system_id, change_order, recovery_strategy, transaction_flag, system_change" + + ") VALUES (" + + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" + + ")", tableName); } public String getSelectHistorySqlString(String tableName) { return String.format( - "SELECT execution_id, stage_id, change_id, author, created_at, state, type, invoked_class, invoked_method, source_file, " + - "execution_millis, execution_hostname, metadata, system_change, error_trace, tx_strategy, target_system_id, change_order, recovery_strategy, transaction_flag " + - "FROM %s ORDER BY id ASC", tableName); + "SELECT execution_id, stage_id, change_id, author, created_at, state, type, invoked_class, invoked_method, source_file, " + + "execution_millis, execution_hostname, metadata, system_change, error_trace, tx_strategy, target_system_id, change_order, recovery_strategy, transaction_flag " + + "FROM %s " + + "ORDER BY id ASC", tableName); + } + + public String getSelectHistoryByChangeIdSqlString(String tableName) { + return String.format( + "SELECT execution_id, stage_id, change_id, author, created_at, state, type, invoked_class, invoked_method, source_file, " + + "execution_millis, execution_hostname, metadata, system_change, error_trace, tx_strategy, target_system_id, change_order, recovery_strategy, transaction_flag " + + "FROM %s " + + "WHERE change_id = ? " + + "ORDER BY id ASC", tableName); + } + + public String getCountByStatusSqlString(String tableName) { + return String.format( + "SELECT COUNT(change_id) " + + "FROM %s " + + "WHERE state = ?", tableName); } private String getAutoIncrementType() { diff --git a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlLockDialectHelper.java b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlLockDialectHelper.java similarity index 63% rename from community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlLockDialectHelper.java rename to utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlLockDialectHelper.java index be4950e0c..f1439917a 100644 --- a/community/flamingock-auditstore-sql/src/main/java/io/flamingock/store/sql/internal/SqlLockDialectHelper.java +++ b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlLockDialectHelper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.flamingock.store.sql.internal; +package io.flamingock.internal.common.sql.dialectHelpers; import io.flamingock.internal.common.sql.SqlDialectFactory; import io.flamingock.internal.common.sql.SqlDialect; @@ -21,7 +21,6 @@ import java.sql.*; import java.time.LocalDateTime; -import java.util.Objects; public final class SqlLockDialectHelper { @@ -117,6 +116,7 @@ public String getCreateTableSqlString(String tableName) { public String getSelectLockSqlString(String tableName) { switch (sqlDialect) { case POSTGRESQL: + case H2: return String.format("SELECT \"key\", status, owner, expires_at FROM %s WHERE \"key\" = ?", tableName); case DB2: // Select lock_key as the first column (getLockEntry expects rs.getString(1) to be the key) @@ -125,10 +125,10 @@ public String getSelectLockSqlString(String tableName) { return String.format("SELECT [key], status, owner, expires_at FROM %s WITH (UPDLOCK, ROWLOCK) WHERE [key] = ?", tableName); case SYBASE: return String.format( - "SELECT lock_key, status, owner, expires_at " + - "FROM %s HOLDLOCK " + - "WHERE lock_key = ?", - tableName + "SELECT lock_key, status, owner, expires_at " + + "FROM %s HOLDLOCK " + + "WHERE lock_key = ?", + tableName ); case ORACLE: return String.format("SELECT \"key\", status, owner, expires_at FROM %s WHERE \"key\" = ? FOR UPDATE", tableName); case INFORMIX: @@ -140,102 +140,131 @@ public String getSelectLockSqlString(String tableName) { } } + public String getSelectAllLocksSqlString(String tableName) { + switch (sqlDialect) { + case POSTGRESQL: + case H2: + return String.format("SELECT \"key\", status, owner, expires_at FROM %s", tableName); + case SQLSERVER: + return String.format("SELECT [key], status, owner, expires_at FROM %s WITH (UPDLOCK, ROWLOCK)", tableName); + case SYBASE: + return String.format( + "SELECT lock_key, status, owner, expires_at " + + "FROM %s HOLDLOCK", + tableName + ); + case ORACLE: + return String.format("SELECT \"key\", status, owner, expires_at FROM %s FOR UPDATE", tableName); + case DB2: + case INFORMIX: + case FIREBIRD: + return String.format("SELECT lock_key, status, owner, expires_at FROM %s", tableName); + default: + return String.format("SELECT `key`, status, owner, expires_at FROM %s", tableName); + } + } + public String getInsertOrUpdateLockSqlString(String tableName) { switch (sqlDialect) { case MYSQL: case MARIADB: return String.format( - "INSERT INTO %s (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?) " + - "ON DUPLICATE KEY UPDATE status = VALUES(status), owner = VALUES(owner), expires_at = VALUES(expires_at)", - tableName); + "INSERT INTO %s (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE status = VALUES(status), owner = VALUES(owner), expires_at = VALUES(expires_at)", + tableName); case POSTGRESQL: return String.format( - "INSERT INTO %s (\"key\", status, owner, expires_at) VALUES (?, ?, ?, ?) " + - "ON CONFLICT (\"key\") DO UPDATE SET status = EXCLUDED.status, owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at", - tableName); + "INSERT INTO %s (\"key\", status, owner, expires_at) VALUES (?, ?, ?, ?) " + + "ON CONFLICT (\"key\") DO UPDATE SET status = EXCLUDED.status, owner = EXCLUDED.owner, expires_at = EXCLUDED.expires_at", + tableName); case SQLITE: return String.format( - "INSERT OR REPLACE INTO %s (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?)", - tableName); + "INSERT OR REPLACE INTO %s (`key`, status, owner, expires_at) VALUES (?, ?, ?, ?)", + tableName); case SQLSERVER: return String.format( - "BEGIN TRANSACTION; " + - "UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE [key] = ?; " + - "IF @@ROWCOUNT = 0 " + - "BEGIN " + - "INSERT INTO %s ([key], status, owner, expires_at) VALUES (?, ?, ?, ?) " + - "END; " + - "COMMIT TRANSACTION;", - tableName, tableName); + "BEGIN TRANSACTION; " + + "UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE [key] = ?; " + + "IF @@ROWCOUNT = 0 " + + "BEGIN " + + "INSERT INTO %s ([key], status, owner, expires_at) VALUES (?, ?, ?, ?) " + + "END; " + + "COMMIT TRANSACTION;", + tableName, tableName); case SYBASE: return String.format( - "BEGIN TRAN " + - "UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?; " + - "IF @@ROWCOUNT = 0 " + - "BEGIN " + - " INSERT INTO %s (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?); " + - "END " + - "COMMIT TRAN", - tableName, tableName + "BEGIN TRAN " + + "UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?; " + + "IF @@ROWCOUNT = 0 " + + "BEGIN " + + " INSERT INTO %s (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?); " + + "END " + + "COMMIT TRAN", + tableName, tableName ); case ORACLE: return String.format( - "MERGE INTO %s t USING (SELECT ? AS \"key\", ? AS status, ? AS owner, ? AS expires_at FROM dual) s " + - "ON (t.\"key\" = s.\"key\") " + - "WHEN MATCHED THEN UPDATE SET t.status = s.status, t.owner = s.owner, t.expires_at = s.expires_at " + - "WHEN NOT MATCHED THEN INSERT (\"key\", status, owner, expires_at) VALUES (s.\"key\", s.status, s.owner, s.expires_at)", - tableName); + "MERGE INTO %s t USING (SELECT ? AS \"key\", ? AS status, ? AS owner, ? AS expires_at FROM dual) s " + + "ON (t.\"key\" = s.\"key\") " + + "WHEN MATCHED THEN UPDATE SET t.status = s.status, t.owner = s.owner, t.expires_at = s.expires_at " + + "WHEN NOT MATCHED THEN INSERT (\"key\", status, owner, expires_at) VALUES (s.\"key\", s.status, s.owner, s.expires_at)", + tableName); case H2: return String.format( - "MERGE INTO %s (`key`, status, owner, expires_at) KEY (`key`) VALUES (?, ?, ?, ?)", - tableName); + "MERGE INTO %s (\"key\", status, owner, expires_at) KEY (\"key\") VALUES (?, ?, ?, ?)", + tableName); case DB2: // Use a VALUES-derived table and a target alias for DB2 to avoid parsing issues return String.format( - "MERGE INTO %s tgt USING (VALUES (?, ?, ?, ?)) src(lock_key, status, owner, expires_at) " + - "ON (tgt.lock_key = src.lock_key) " + - "WHEN MATCHED THEN UPDATE SET status = src.status, owner = src.owner, expires_at = src.expires_at " + - "WHEN NOT MATCHED THEN INSERT (lock_key, status, owner, expires_at) VALUES (src.lock_key, src.status, src.owner, src.expires_at)", - tableName); + "MERGE INTO %s tgt USING (VALUES (?, ?, ?, ?)) src(lock_key, status, owner, expires_at) " + + "ON (tgt.lock_key = src.lock_key) " + + "WHEN MATCHED THEN UPDATE SET status = src.status, owner = src.owner, expires_at = src.expires_at " + + "WHEN NOT MATCHED THEN INSERT (lock_key, status, owner, expires_at) VALUES (src.lock_key, src.status, src.owner, src.expires_at)", + tableName); case FIREBIRD: - return String.format("UPDATE " + tableName + " SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?", tableName); + return String.format("UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?", tableName); case INFORMIX: // Informix doesn't support ON DUPLICATE KEY UPDATE // Use a procedural approach similar to SQL Server return String.format( - "UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?; " + - "INSERT INTO %s (lock_key, status, owner, expires_at) " + - "SELECT ?, ?, ?, ? FROM sysmaster:sysdual " + - "WHERE NOT EXISTS (SELECT 1 FROM %s WHERE lock_key = ?)", - tableName, tableName, tableName); + "UPDATE %s SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?; " + + "INSERT INTO %s (lock_key, status, owner, expires_at) " + + "SELECT ?, ?, ?, ? FROM sysmaster:sysdual " + + "WHERE NOT EXISTS (SELECT 1 FROM %s WHERE lock_key = ?)", + tableName, tableName, tableName); default: throw new UnsupportedOperationException("Dialect not supported for upsert: " + sqlDialect.name()); } } public String getDeleteLockSqlString(String tableName) { - if (Objects.requireNonNull(sqlDialect) == SqlDialect.POSTGRESQL) { - return String.format("DELETE FROM %s WHERE \"key\" = ?", tableName); - } - if (sqlDialect == SqlDialect.INFORMIX || sqlDialect == SqlDialect.DB2) { - return String.format("DELETE FROM %s WHERE lock_key = ?", tableName); - } - if (sqlDialect == SqlDialect.FIREBIRD) { - return String.format("DELETE FROM %s WHERE lock_key = ?", tableName); + switch (sqlDialect) { + case POSTGRESQL: + case ORACLE: + return String.format("DELETE FROM %s WHERE \"key\" = ?", tableName); + case INFORMIX: + case DB2: + case FIREBIRD: + case SYBASE: + return String.format("DELETE FROM %s WHERE lock_key = ?", tableName); + case SQLSERVER: + return String.format("DELETE FROM %s WHERE [key] = ?", tableName); + default: // MYSQL, MARIADB, SQLITE, H2 + return String.format("DELETE FROM %s WHERE `key` = ?", tableName); } - return String.format("DELETE FROM %s WHERE `key` = ?", tableName); } - public void upsertLockEntry(Connection conn, String tableName, String key, String owner, LocalDateTime expiresAt) throws SQLException { + public void upsertLockEntry(Connection conn, String tableName, String key, String owner, String lockStatus, LocalDateTime expiresAt) throws SQLException { String sql = getInsertOrUpdateLockSqlString(tableName); if (sqlDialect == SqlDialect.DB2) { // UPDATE first try (PreparedStatement update = conn.prepareStatement( - "UPDATE " + tableName + " SET owner = ?, expires_at = ? WHERE lock_key = ?")) { - update.setString(1, owner); - update.setTimestamp(2, Timestamp.valueOf(expiresAt)); - update.setString(3, key); + "UPDATE " + tableName + " SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?")) { + update.setString(1, lockStatus); + update.setString(2, owner); + update.setTimestamp(3, Timestamp.valueOf(expiresAt)); + update.setString(4, key); int updated = update.executeUpdate(); if (updated > 0) { return; @@ -244,9 +273,9 @@ public void upsertLockEntry(Connection conn, String tableName, String key, Strin // If no row updated, try INSERT try (PreparedStatement insert = conn.prepareStatement( - "INSERT INTO " + tableName + " (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?)")) { + "INSERT INTO " + tableName + " (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?)")) { insert.setString(1, key); - insert.setString(2, LockStatus.LOCK_HELD.name()); + insert.setString(2, lockStatus); insert.setString(3, owner); insert.setTimestamp(4, Timestamp.valueOf(expiresAt)); insert.executeUpdate(); @@ -257,8 +286,8 @@ public void upsertLockEntry(Connection conn, String tableName, String key, Strin if (getSqlDialect() == SqlDialect.INFORMIX) { // Try UPDATE first try (PreparedStatement update = conn.prepareStatement( - "UPDATE " + tableName + " SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?")) { - update.setString(1, LockStatus.LOCK_HELD.name()); + "UPDATE " + tableName + " SET status = ?, owner = ?, expires_at = ? WHERE lock_key = ?")) { + update.setString(1, lockStatus); update.setString(2, owner); update.setTimestamp(3, Timestamp.valueOf(expiresAt)); update.setString(4, key); @@ -270,9 +299,9 @@ public void upsertLockEntry(Connection conn, String tableName, String key, Strin // If no row updated, try INSERT try (PreparedStatement insert = conn.prepareStatement( - "INSERT INTO " + tableName + " (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?)")) { + "INSERT INTO " + tableName + " (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?)")) { insert.setString(1, key); - insert.setString(2, LockStatus.LOCK_HELD.name()); + insert.setString(2, lockStatus); insert.setString(3, owner); insert.setTimestamp(4, Timestamp.valueOf(expiresAt)); insert.executeUpdate(); @@ -284,23 +313,22 @@ public void upsertLockEntry(Connection conn, String tableName, String key, Strin // For SQL Server/Sybase, use Statement and format SQL try (Statement stmt = conn.createStatement()) { String formattedSql = sql - .replaceFirst("\\?", "'" + LockStatus.LOCK_HELD.name() + "'") - .replaceFirst("\\?", "'" + owner + "'") - .replaceFirst("\\?", "'" + Timestamp.valueOf(expiresAt) + "'") - .replaceFirst("\\?", "'" + key + "'") - .replaceFirst("\\?", "'" + key + "'") - .replaceFirst("\\?", "'" + LockStatus.LOCK_HELD.name() + "'") - .replaceFirst("\\?", "'" + owner + "'") - .replaceFirst("\\?", "'" + Timestamp.valueOf(expiresAt) + "'"); + .replaceFirst("\\?", "'" + lockStatus + "'") + .replaceFirst("\\?", "'" + owner + "'") + .replaceFirst("\\?", "'" + Timestamp.valueOf(expiresAt) + "'") + .replaceFirst("\\?", "'" + key + "'") + .replaceFirst("\\?", "'" + key + "'") + .replaceFirst("\\?", "'" + lockStatus + "'") + .replaceFirst("\\?", "'" + owner + "'") + .replaceFirst("\\?", "'" + Timestamp.valueOf(expiresAt) + "'"); stmt.execute(formattedSql); } return; } if (sqlDialect == SqlDialect.FIREBIRD) { - String updateSql = getInsertOrUpdateLockSqlString(tableName); - try (PreparedStatement ps = conn.prepareStatement(updateSql)) { - ps.setString(1, LockStatus.LOCK_HELD.name()); + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, lockStatus); ps.setString(2, owner); ps.setTimestamp(3, Timestamp.valueOf(expiresAt)); ps.setString(4, key); @@ -309,7 +337,7 @@ public void upsertLockEntry(Connection conn, String tableName, String key, Strin String insertSql = "INSERT INTO " + tableName + " (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?)"; try (PreparedStatement ins = conn.prepareStatement(insertSql)) { ins.setString(1, key); - ins.setString(2, LockStatus.LOCK_HELD.name()); + ins.setString(2, lockStatus); ins.setString(3, owner); ins.setTimestamp(4, Timestamp.valueOf(expiresAt)); ins.executeUpdate(); @@ -322,9 +350,9 @@ public void upsertLockEntry(Connection conn, String tableName, String key, Strin if (sqlDialect == SqlDialect.SYBASE) { // The lock was already deleted in acquireLockQuery for Sybase try (PreparedStatement insert = conn.prepareStatement( - "INSERT INTO " + tableName + " (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?)")) { + "INSERT INTO " + tableName + " (lock_key, status, owner, expires_at) VALUES (?, ?, ?, ?)")) { insert.setString(1, key); - insert.setString(2, LockStatus.LOCK_HELD.name()); + insert.setString(2, lockStatus); insert.setString(3, owner); insert.setTimestamp(4, Timestamp.valueOf(expiresAt)); insert.executeUpdate(); @@ -336,7 +364,7 @@ public void upsertLockEntry(Connection conn, String tableName, String key, Strin // Default case for other dialects try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, key); - ps.setString(2, LockStatus.LOCK_HELD.name()); + ps.setString(2, lockStatus); ps.setString(3, owner); ps.setTimestamp(4, Timestamp.valueOf(expiresAt)); ps.executeUpdate(); diff --git a/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlTestKitDialectHelper.java b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlTestKitDialectHelper.java new file mode 100644 index 000000000..61076b687 --- /dev/null +++ b/utils/sql-util/src/main/java/io/flamingock/internal/common/sql/dialectHelpers/SqlTestKitDialectHelper.java @@ -0,0 +1,151 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.common.sql.dialectHelpers; + +import io.flamingock.internal.common.sql.SqlDialect; +import io.flamingock.internal.common.sql.SqlDialectFactory; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public final class SqlTestKitDialectHelper { + + final private SqlDialect sqlDialect; + + public SqlTestKitDialectHelper(Connection connection) { + this.sqlDialect = SqlDialectFactory.getSqlDialect(connection); + } + + public void disableForeignKeyChecks(Connection conn) throws SQLException { + switch (sqlDialect) { + case MYSQL: + case MARIADB: + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("SET FOREIGN_KEY_CHECKS=0"); + } + break; + case SQLITE: + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("PRAGMA foreign_keys = OFF"); + } + break; + case H2: + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("SET REFERENTIAL_INTEGRITY FALSE"); + } + break; + case SQLSERVER: + case SYBASE: + case FIREBIRD: + dropAllForeignKeys(conn); + break; + default: + break; + } + } + + public void enableForeignKeyChecks(Connection conn) throws SQLException { + switch (sqlDialect) { + case MYSQL: + case MARIADB: + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("SET FOREIGN_KEY_CHECKS=1"); + } + break; + case SQLITE: + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("PRAGMA foreign_keys = ON"); + } + break; + case H2: + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate("SET REFERENTIAL_INTEGRITY TRUE"); + } + break; + default: + break; + } + } + + private void dropAllForeignKeys(Connection conn) throws SQLException { + DatabaseMetaData meta = conn.getMetaData(); + String schema = conn.getSchema(); + String catalog = conn.getCatalog(); + + try (ResultSet tables = meta.getTables(catalog, schema, "%", new String[]{"TABLE"})) { + while (tables.next()) { + String tableName = tables.getString("TABLE_NAME"); + try (ResultSet fks = meta.getExportedKeys(catalog, schema, tableName)) { + while (fks.next()) { + String fkName = fks.getString("FK_NAME"); + String fkTable = fks.getString("FKTABLE_NAME"); + if (fkName != null) { + try (Statement stmt = conn.createStatement()) { + stmt.executeUpdate( + "ALTER TABLE " + fkTable + " DROP CONSTRAINT " + fkName); + } catch (SQLException e) { + } + } + } + } + } + } + } + + public String getDropTableSql(String tableName) { + switch (sqlDialect) { + case POSTGRESQL: + return "DROP TABLE IF EXISTS " + tableName + " CASCADE"; + case ORACLE: + return "DROP TABLE " + tableName + " CASCADE CONSTRAINTS PURGE"; + case SQLSERVER: + return "IF OBJECT_ID('" + tableName + "', 'U') IS NOT NULL DROP TABLE " + tableName; + case SYBASE: + return "DROP TABLE " + tableName; + default: + // MYSQL, MARIADB, SQLITE, H2, DB2, INFORMIX, FIREBIRD + return "DROP TABLE IF EXISTS " + tableName; + } + } + + public List getUserTables(Connection conn) throws SQLException { + List tables = new ArrayList<>(); + DatabaseMetaData meta = conn.getMetaData(); + String schema = conn.getSchema(); + String catalog = conn.getCatalog(); + + if (sqlDialect == SqlDialect.INFORMIX && schema == null) { + schema = meta.getUserName(); + } + + try (ResultSet rs = meta.getTables(catalog, schema, "%", new String[]{"TABLE"})) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String upperName = tableName.toUpperCase(); + if (sqlDialect == SqlDialect.FIREBIRD && (upperName.startsWith("RDB$") || upperName.startsWith("MON$") || upperName.startsWith("SEC$"))) { + continue; + } + tables.add(tableName); + } + } + return tables; + } + + public SqlDialect getSqlDialect() { + return sqlDialect; + } +} diff --git a/utils/test-util/build.gradle.kts b/utils/test-util/build.gradle.kts index 9514492e6..ddf5bb7a6 100644 --- a/utils/test-util/build.gradle.kts +++ b/utils/test-util/build.gradle.kts @@ -1,7 +1,8 @@ val jacksonVersion = "2.16.0" +val generalUtilVersion: String by extra dependencies { - api(project(":utils:general-util")) + api("io.flamingock:flamingock-general-util:${generalUtilVersion}") api(project(":core:flamingock-core")) api(project(":core:flamingock-core-commons")) api(project(":core:flamingock-processor")) diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/CodeChangeTestDefinition.java b/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/CodeChangeTestDefinition.java index cbcd6c39f..9ddfd7f19 100644 --- a/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/CodeChangeTestDefinition.java +++ b/utils/test-util/src/main/java/io/flamingock/common/test/pipeline/CodeChangeTestDefinition.java @@ -115,6 +115,7 @@ public AbstractPreviewTask toPreview() { getOrder(), author, // Default author for tests className, + null, PreviewConstructor.getDefault(), new PreviewMethod("apply", executionParameterNames), rollback,