diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractor.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractor.java index 5e2c6bd01..8bc320c28 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractor.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractor.java @@ -29,10 +29,10 @@ public final class ChangeOrderExtractor { // Captures ORDER before double underscore separator private static final Pattern SIMPLE_FILE_ORDER_REGEX_PATTERN = Pattern.compile("^_(.+?)__(.+)$"); - // For class names: must have _order__ at the beginning of the class name after package - // (e.g., com.mycompany.mypackage._002__MyChange or com.mycompany.OuterClass$_V1_2_3__InnerChange) + // For class names: must have _order__ at the beginning of the class name, after package, or after enclosing class + // (e.g., _001__MyChange, com.mycompany.mypackage._002__MyChange or com.mycompany.OuterClass$_V1_2_3__InnerChange) // Captures ORDER before double underscore separator - private static final Pattern FILE_WITH_PACKAGE_ORDER_REGEX_PATTERN = Pattern.compile("[.$]_(.+?)__(.+)$"); + private static final Pattern FILE_WITH_PACKAGE_ORDER_REGEX_PATTERN = Pattern.compile("(?:^|[.$])_(.+?)__(.+)$"); private ChangeOrderExtractor() { } 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 a9d261f9f..e4ede2dba 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 @@ -50,7 +50,12 @@ public CodePreviewChange(String id, this.previewConstructor = previewConstructor; this.applyPreviewMethod = applyPreviewMethod; this.rollbackPreviewMethod = rollbackPreviewMethod; - this.sourcePackage = sourceClassPath.substring(0, sourceClassPath.lastIndexOf(".")); + this.sourcePackage = extractSourcePackage(sourceClassPath); + } + + private String extractSourcePackage(String sourceClassPath) { + int packageEndIndex = sourceClassPath != null ? sourceClassPath.lastIndexOf(".") : -1; + return packageEndIndex > 0 ? sourceClassPath.substring(0, packageEndIndex) : ""; } public PreviewConstructor getPreviewConstructor() { return previewConstructor; } 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 a6f9a7f5d..7847ebe6d 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 @@ -28,6 +28,7 @@ import io.flamingock.internal.common.core.preview.PreviewMethod; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.internal.common.core.task.TargetSystemDescriptor; +import io.flamingock.internal.common.core.util.TypeElementNameUtils; import io.flamingock.internal.util.ReflectionUtil; import org.jetbrains.annotations.NotNull; @@ -132,8 +133,9 @@ CodePreviewTaskBuilder setTypeElement(TypeElement typeElement) { Recovery recoveryAnnotation = typeElement.getAnnotation(Recovery.class); if(changeAnnotation != null) { + validateNestedChangeClass(typeElement); String changeId = changeAnnotation.id(); - String classPath = typeElement.getQualifiedName().toString(); + String classPath = TypeElementNameUtils.getBinaryName(typeElement); String order = ChangeOrderExtractor.extractOrderFromClassName(changeId, classPath); setId(changeId); setOrder(order); @@ -157,6 +159,14 @@ CodePreviewTaskBuilder setTypeElement(TypeElement typeElement) { return this; } + private void validateNestedChangeClass(TypeElement typeElement) { + if (TypeElementNameUtils.isNonStaticNestedType(typeElement)) { + throw new FlamingockException( + "Change class [%s] is a non-static nested class. Nested change classes must be static so Flamingock can instantiate them without an enclosing class instance.", + TypeElementNameUtils.getBinaryName(typeElement)); + } + } + @Override diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/util/TypeElementNameUtils.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/util/TypeElementNameUtils.java new file mode 100644 index 000000000..d106d5118 --- /dev/null +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/util/TypeElementNameUtils.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.util; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; + +public final class TypeElementNameUtils { + + private TypeElementNameUtils() { + } + + public static String getBinaryName(TypeElement typeElement) { + Element enclosingElement = typeElement.getEnclosingElement(); + if (enclosingElement instanceof TypeElement && isTypeElement(enclosingElement)) { + return getBinaryName((TypeElement) enclosingElement) + "$" + typeElement.getSimpleName(); + } + return typeElement.getQualifiedName().toString(); + } + + private static boolean isTypeElement(Element element) { + ElementKind kind = element.getKind(); + return kind == ElementKind.CLASS + || kind == ElementKind.INTERFACE + || kind == ElementKind.ENUM + || kind == ElementKind.ANNOTATION_TYPE; + } + + public static boolean isNonStaticNestedType(TypeElement typeElement) { + return typeElement.getEnclosingElement() instanceof TypeElement + && !typeElement.getModifiers().contains(Modifier.STATIC); + } +} diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractorTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractorTest.java new file mode 100644 index 000000000..2641bfc28 --- /dev/null +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractorTest.java @@ -0,0 +1,44 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ChangeOrderExtractorTest { + + @Test + void shouldExtractOrderFromDefaultPackageClassName() { + String order = ChangeOrderExtractor.extractOrderFromClassName("dummy-change", "_0001__DummyChange"); + + assertEquals("0001", order); + } + + @Test + void shouldExtractOrderFromPackagedClassName() { + String order = ChangeOrderExtractor.extractOrderFromClassName("dummy-change", "io.flamingock._0002__DummyChange"); + + assertEquals("0002", order); + } + + @Test + void shouldExtractOrderFromNestedBinaryClassName() { + String order = ChangeOrderExtractor.extractOrderFromClassName("dummy-change", "io.flamingock.ChangeGroup$_0003__DummyChange"); + + assertEquals("0003", order); + } +} 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 index 8b55ae683..568848c3b 100644 --- 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 @@ -15,15 +15,25 @@ */ package io.flamingock.internal.common.core.preview.builder; +import io.flamingock.api.annotations.Change; +import io.flamingock.internal.common.core.error.FlamingockException; 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 javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; import java.util.Collections; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class CodePreviewTaskBuilderTest { @@ -45,4 +55,101 @@ void shouldBuildNullSourceFileForCodeChanges() { assertNull(preview.getSourceFile()); } + + @Test + void shouldExtractSourcePackageFromNestedBinaryClassName() { + CodePreviewChange preview = buildPreview("io.flamingock.ChangeGroup$_001__NestedChange"); + + assertEquals("io.flamingock", preview.getSourcePackage()); + } + + @Test + void shouldSetEmptySourcePackageWhenSourceClassIsNotDeclaredInPackage() { + CodePreviewChange preview = buildPreview("_001__DefaultPackageChange"); + + assertEquals("", preview.getSourcePackage()); + } + + @Test + void shouldThrowWhenChangeIsNonStaticNestedClass() { + TypeElement group = mockTypeElement("io.flamingock.GroupClass", "GroupClass", packageElement()); + TypeElement change = mockTypeElement("io.flamingock.GroupClass._1001__InnerChange", "_1001__InnerChange", group); + Change changeAnnotation = changeAnnotation(); + when(change.getAnnotation(Change.class)).thenReturn(changeAnnotation); + + FlamingockException exception = assertThrows( + FlamingockException.class, + () -> CodePreviewTaskBuilder.instance(change).build()); + + assertEquals( + "Change class [io.flamingock.GroupClass$_1001__InnerChange] is a non-static nested class. Nested change classes must be static so Flamingock can instantiate them without an enclosing class instance.", + exception.getMessage()); + } + + private CodePreviewChange buildPreview(String sourceClassPath) { + return CodePreviewTaskBuilder.instance() + .setId("test-id") + .setOrder("001") + .setAuthor("author") + .setSourceClassPath(sourceClassPath) + .setConstructor(PreviewConstructor.getDefault()) + .setApplyMethod(new PreviewMethod("apply", Collections.emptyList())) + .setRollbackMethod(null) + .setRunAlways(false) + .setTransactionalFlag(true) + .setSystem(false) + .setRecovery(RecoveryDescriptor.getDefault()) + .build(); + } + + private TypeElement mockTypeElement(String qualifiedName, String simpleName, Element enclosingElement) { + TypeElement typeElement = mock(TypeElement.class); + when(typeElement.getKind()).thenReturn(ElementKind.CLASS); + when(typeElement.getQualifiedName()).thenReturn(name(qualifiedName)); + when(typeElement.getSimpleName()).thenReturn(name(simpleName)); + when(typeElement.getEnclosingElement()).thenReturn(enclosingElement); + when(typeElement.getModifiers()).thenReturn(Collections.emptySet()); + return typeElement; + } + + private Element packageElement() { + Element element = mock(Element.class); + when(element.getKind()).thenReturn(ElementKind.PACKAGE); + return element; + } + + private Change changeAnnotation() { + Change change = mock(Change.class); + when(change.id()).thenReturn("inner-change"); + return change; + } + + private Name name(String value) { + return new Name() { + @Override + public boolean contentEquals(CharSequence cs) { + return value.contentEquals(cs); + } + + @Override + public int length() { + return value.length(); + } + + @Override + public char charAt(int index) { + return value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return value.subSequence(start, end); + } + + @Override + public String toString() { + return value; + } + }; + } } diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/util/TypeElementNameUtilsTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/util/TypeElementNameUtilsTest.java new file mode 100644 index 000000000..5e1d04148 --- /dev/null +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/util/TypeElementNameUtilsTest.java @@ -0,0 +1,132 @@ +/* + * 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.util; + +import org.junit.jupiter.api.Test; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; + +import java.util.Collections; +import java.util.EnumSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TypeElementNameUtilsTest { + + @Test + void shouldReturnQualifiedNameForTopLevelClass() { + TypeElement change = mockTypeElement("io.flamingock.changes.Change001", "Change001", packageElement()); + + assertEquals("io.flamingock.changes.Change001", TypeElementNameUtils.getBinaryName(change)); + } + + @Test + void shouldReturnBinaryNameForNestedClass() { + TypeElement group = mockTypeElement("io.flamingock.changes.ChangeGroup", "ChangeGroup", packageElement()); + TypeElement change = mockTypeElement("io.flamingock.changes.ChangeGroup.Change001", "Change001", group); + + assertEquals("io.flamingock.changes.ChangeGroup$Change001", TypeElementNameUtils.getBinaryName(change)); + } + + @Test + void shouldReturnBinaryNameForDeeplyNestedClass() { + TypeElement group = mockTypeElement("io.flamingock.changes.ChangeGroup", "ChangeGroup", packageElement()); + TypeElement subgroup = mockTypeElement("io.flamingock.changes.ChangeGroup.SubGroup", "SubGroup", group); + TypeElement change = mockTypeElement("io.flamingock.changes.ChangeGroup.SubGroup.Change001", "Change001", subgroup); + + assertEquals("io.flamingock.changes.ChangeGroup$SubGroup$Change001", TypeElementNameUtils.getBinaryName(change)); + } + + @Test + void shouldDetectNonStaticNestedClass() { + TypeElement group = mockTypeElement("io.flamingock.changes.ChangeGroup", "ChangeGroup", packageElement()); + TypeElement change = mockTypeElement("io.flamingock.changes.ChangeGroup.Change001", "Change001", group); + + assertTrue(TypeElementNameUtils.isNonStaticNestedType(change)); + } + + @Test + void shouldNotDetectStaticNestedClassAsNonStaticNestedClass() { + TypeElement group = mockTypeElement("io.flamingock.changes.ChangeGroup", "ChangeGroup", packageElement()); + TypeElement change = mockTypeElement("io.flamingock.changes.ChangeGroup.Change001", "Change001", group, EnumSet.of(Modifier.STATIC)); + + assertFalse(TypeElementNameUtils.isNonStaticNestedType(change)); + } + + @Test + void shouldNotDetectTopLevelClassAsNonStaticNestedClass() { + TypeElement change = mockTypeElement("io.flamingock.changes.Change001", "Change001", packageElement()); + + assertFalse(TypeElementNameUtils.isNonStaticNestedType(change)); + } + + private TypeElement mockTypeElement(String qualifiedName, String simpleName, Element enclosingElement) { + return mockTypeElement(qualifiedName, simpleName, enclosingElement, Collections.emptySet()); + } + + private TypeElement mockTypeElement(String qualifiedName, String simpleName, Element enclosingElement, java.util.Set modifiers) { + TypeElement typeElement = mock(TypeElement.class); + when(typeElement.getKind()).thenReturn(ElementKind.CLASS); + when(typeElement.getQualifiedName()).thenReturn(name(qualifiedName)); + when(typeElement.getSimpleName()).thenReturn(name(simpleName)); + when(typeElement.getEnclosingElement()).thenReturn(enclosingElement); + when(typeElement.getModifiers()).thenReturn(modifiers); + return typeElement; + } + + private Element packageElement() { + Element element = mock(Element.class); + when(element.getKind()).thenReturn(ElementKind.PACKAGE); + return element; + } + + private Name name(String value) { + return new Name() { + @Override + public boolean contentEquals(CharSequence cs) { + return value.contentEquals(cs); + } + + @Override + public int length() { + return value.length(); + } + + @Override + public char charAt(int index) { + return value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return value.subSequence(start, end); + } + + @Override + public String toString() { + return value; + } + }; + } +} 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 3096538ae..70c9e51cb 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 @@ -18,10 +18,16 @@ import io.flamingock.api.annotations.Apply; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.api.annotations.Change; +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.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.Collections; + import static org.junit.jupiter.api.Assertions.*; class CodeLoadedTaskBuilderTest { @@ -173,6 +179,14 @@ void shouldDefaultToTrueWhenTransactionalIsNull() { assertTrue(result.isTransactional()); } + @Test + @DisplayName("Should load preview with nested class binary name") + void shouldLoadPreviewWithNestedClassBinaryName() { + CodeLoadedChange change = CodeLoadedTaskBuilder.getInstanceFromPreview(preview(Group.NestedChange.class.getName())).build(); + + assertEquals(Group.NestedChange.class, change.getImplementationClass()); + } + // Test class with Change annotation for testing setFromFlamingockChangeAnnotation @Change(id = "annotation-test", transactional = false, author = "aperezdieppa") public static class _100__TestChangeClass { @@ -233,4 +247,32 @@ void shouldBuildFromAnnotatedClassCorrectlyWhenOrderInAnnotationNull() { assertNull(result.getSourceFile()); } + private CodePreviewChange preview(String sourceClassPath) { + return new CodePreviewChange( + "nested-change", + "001", + "flamingock", + sourceClassPath, + null, + PreviewConstructor.getDefault(), + new PreviewMethod("apply", Collections.emptyList()), + null, + false, + true, + false, + null, + RecoveryDescriptor.getDefault(), + false); + } + + static class Group { + + static class NestedChange { + + @Apply + void apply() { + } + } + } + } diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java index 2a3438feb..344c03cc3 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/FlamingockAnnotationProcessor.java @@ -779,7 +779,7 @@ private void validateAllChangesAreMappedToStages(Map" : pkg; + } + } diff --git a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java index 5f5dc7ea9..5e82ee8a4 100644 --- a/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java +++ b/core/flamingock-processor/src/main/java/io/flamingock/core/processor/util/AnnotationFinder.java @@ -22,6 +22,7 @@ import io.flamingock.internal.common.core.metadata.BuilderProviderInfo; import io.flamingock.internal.common.core.preview.CodePreviewChange; import io.flamingock.internal.common.core.util.LoggerPreProcessor; +import io.flamingock.internal.common.core.util.TypeElementNameUtils; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; @@ -112,7 +113,7 @@ public Optional findBuilderProvider() { validateReturnType(method); TypeElement enclosingClass = (TypeElement) method.getEnclosingElement(); - String className = enclosingClass.getQualifiedName().toString(); + String className = TypeElementNameUtils.getBinaryName(enclosingClass); String methodName = method.getSimpleName().toString(); String signature = acceptsArgs ? "(String[] args)" : "()"; @@ -195,4 +196,4 @@ private void validateReturnType(ExecutableElement method) { // If we can't find the builder type (shouldn't happen), we skip validation } -} \ No newline at end of file +} 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 1bce4e857..6f96113b3 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 @@ -316,6 +316,37 @@ void shouldThrowErrorForUnmappedChanges() throws Exception { assertTrue(cause.getMessage().contains("com.example.unmapped"), "Should mention the unmapped package"); } + @Test + @DisplayName("Should report default package when empty package changes are not mapped") + void shouldReportDefaultPackageForUnmappedEmptyPackageChanges() throws Exception { + FlamingockAnnotationProcessor processor = new FlamingockAnnotationProcessor(); + + Map> changesByPackage = new HashMap<>(); + changesByPackage.put("", Collections.emptyList()); + + PreviewStage mappedStage = PreviewStage.defaultBuilder(StageType.DEFAULT) + .setName("migrations") + .setSourcesPackage("com.example.migrations") + .setSourcesRoots(Collections.emptyList()) + .setResourcesRoot("src/main/resources") + .setChanges(Collections.singletonList(new io.flamingock.internal.common.core.preview.CodePreviewChange())) + .build(); + + PreviewPipeline pipeline = new PreviewPipeline(Collections.singletonList(mappedStage)); + + Method validator = FlamingockAnnotationProcessor.class.getDeclaredMethod( + "validateAllChangesAreMappedToStages", Map.class, PreviewPipeline.class, Boolean.class); + validator.setAccessible(true); + + InvocationTargetException ex = assertThrows(InvocationTargetException.class, () -> + validator.invoke(processor, changesByPackage, pipeline, true)); + + Throwable cause = ex.getCause(); + assertNotNull(cause, "Validator should throw a RuntimeException cause"); + assertInstanceOf(RuntimeException.class, cause, "Cause should be RuntimeException"); + assertEquals("Changes are not mapped to any stage: ", cause.getMessage()); + } + // Helper methods using reflection to test the internal pipeline building logic @@ -517,4 +548,4 @@ public boolean strictStageMapping() { }; } } -} \ No newline at end of file +} diff --git a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChange2.java b/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChange2.java deleted file mode 100644 index c2dce8ee5..000000000 --- a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChange2.java +++ /dev/null @@ -1,28 +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.importer.mongock.mongodb.changes; - -import io.mongock.api.annotations.ChangeUnit; -import io.mongock.api.annotations.Execution; - -@ChangeUnit(id = "mongock-change-2", order = "2", author = "flamingock-team") -public class MongockChange2 { - - @Execution - public void apply() { - System.out.println("Client Initializer"); - } -} diff --git a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChange1.java b/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChangeGroup.java similarity index 60% rename from legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChange1.java rename to legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChangeGroup.java index 2c9ee9736..9f66a3177 100644 --- a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChange1.java +++ b/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/changes/MongockChangeGroup.java @@ -18,11 +18,23 @@ import io.mongock.api.annotations.ChangeUnit; import io.mongock.api.annotations.Execution; -@ChangeUnit(id = "mongock-change-1", order = "1", author = "flamingock-team") -public class MongockChange1 { +public class MongockChangeGroup { - @Execution - public void apply() { - System.out.println("Client Initializer"); + @ChangeUnit(id = "mongock-change-1", order = "1", author = "flamingock-team") + public static class MongockChange1 { + + @Execution + public void apply() { + System.out.println("Client Initializer"); + } + } + + @ChangeUnit(id = "mongock-change-2", order = "2", author = "flamingock-team") + public static class MongockChange2 { + + @Execution + public void apply() { + System.out.println("Client Initializer"); + } } -} +} \ No newline at end of file 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 7d8eb4f1c..b911440c4 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 @@ -23,6 +23,7 @@ import io.flamingock.internal.common.core.preview.PreviewMethod; import io.flamingock.internal.common.core.task.RecoveryDescriptor; import io.flamingock.internal.common.core.task.TargetSystemDescriptor; +import io.flamingock.internal.common.core.util.TypeElementNameUtils; import io.flamingock.internal.util.ReflectionUtil; import io.mongock.api.annotations.BeforeExecution; import io.mongock.api.annotations.ChangeUnit; @@ -52,6 +53,8 @@ public class MongockCodePreviewChangeHelper { @NotNull public List getCodePreviewChanges(TypeElement typeElement, String mongockTargetSystemId) { + validateNestedChangeClass(typeElement); + ChangeUnit changeUnitAnnotation = typeElement.getAnnotation(ChangeUnit.class); if (changeUnitAnnotation != null) { return getCodePreviewChangesFromChangeUnit(typeElement, mongockTargetSystemId, changeUnitAnnotation); @@ -68,6 +71,14 @@ public List getCodePreviewChanges(TypeElement typeElement, St } } + private void validateNestedChangeClass(TypeElement typeElement) { + if (TypeElementNameUtils.isNonStaticNestedType(typeElement)) { + throw new FlamingockException( + "Mongock change class [%s] is a non-static nested class. Nested change classes must be static so Flamingock can instantiate them without an enclosing class instance.", + TypeElementNameUtils.getBinaryName(typeElement)); + } + } + @NotNull private List getCodePreviewChangesFromChangeUnit(TypeElement typeElement, String mongockTargetSystemId, ChangeUnit changeUnitAnnotation) { @@ -80,7 +91,7 @@ private List getCodePreviewChangesFromChangeUnit(TypeElement List changes = new ArrayList<>(); String id = changeUnitAnnotation.id(); - String sourceClassPath = typeElement.getQualifiedName().toString(); + String sourceClassPath = TypeElementNameUtils.getBinaryName(typeElement); String order = getChangeUnitOrder(changeUnitAnnotation.order(), sourceClassPath); String author = changeUnitAnnotation.author(); PreviewConstructor constructor = getPreviewConstructor(typeElement); @@ -211,7 +222,7 @@ private List getCodePreviewChangesFromChangeLog(TypeElement t List changes = new ArrayList<>(); - String sourceClassPath = typeElement.getQualifiedName().toString(); + String sourceClassPath = TypeElementNameUtils.getBinaryName(typeElement); boolean transactional = true; boolean system = false; TargetSystemDescriptor targetSystem = TargetSystemDescriptor.fromId(mongockTargetSystemId);