From 352b9361ea0f8103390b4a973a33fd821c2e7f96 Mon Sep 17 00:00:00 2001 From: chriptus13 Date: Thu, 22 Oct 2026 20:06:26 +0100 Subject: [PATCH 1/3] fix(preview): inherit annotated methods from change superclasses Resolve inherited apply and rollback methods when building previews for both current Flamingock changes and legacy Mongock change units. Add coverage for each path using shared annotation-processing test utilities. --- core/flamingock-core-commons/build.gradle.kts | 1 + .../builder/CodePreviewChangeBuilder.java | 19 +++- .../builder/CodePreviewChangeBuilderTest.java | 45 ++++++++- .../changes/_0001__ChangeUnderTest.java | 35 +++++++ legacy/mongock-support/build.gradle.kts | 7 +- .../MongockCodePreviewChangeHelper.java | 22 ++++- .../MongockCodePreviewChangeHelperTest.java | 74 +++++++++++++++ .../changes/ChangeUnitUnderTest.java | 34 +++++++ .../processor/AnnotatedElementProcessor.java | 62 ++++++++++++ .../common/test/processor/JavacHelper.java | 95 +++++++++++++++++++ 10 files changed, 380 insertions(+), 14 deletions(-) create mode 100644 core/flamingock-core-commons/src/test/resources/changes/_0001__ChangeUnderTest.java create mode 100644 legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java create mode 100644 legacy/mongock-support/src/test/resources/changes/ChangeUnitUnderTest.java create mode 100644 utils/test-util/src/main/java/io/flamingock/common/test/processor/AnnotatedElementProcessor.java create mode 100644 utils/test-util/src/main/java/io/flamingock/common/test/processor/JavacHelper.java diff --git a/core/flamingock-core-commons/build.gradle.kts b/core/flamingock-core-commons/build.gradle.kts index dec8953ee..28d2816bc 100644 --- a/core/flamingock-core-commons/build.gradle.kts +++ b/core/flamingock-core-commons/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") + testImplementation(project(":utils:test-util")) } description = "Shared internal utilities, preview system, and common infrastructure components" diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java index 7ee167856..e46d04357 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java @@ -36,6 +36,9 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import java.lang.annotation.Annotation; import java.util.List; import java.util.Optional; @@ -195,6 +198,9 @@ private CodePreviewChange getCodePreviewChange() { private Optional getAnnotatedMethodInfo(TypeElement typeElement, Class annotationType) { + if (typeElement == null) { + return Optional.empty(); + } for (Element enclosedElement : typeElement.getEnclosedElements()) { if (enclosedElement.getKind() == ElementKind.METHOD && enclosedElement.getAnnotation(annotationType) != null) { @@ -208,7 +214,18 @@ private Optional getAnnotatedMethodInfo(TypeElement typeElement, } } - return Optional.empty(); + TypeMirror superclass = typeElement.getSuperclass(); + if (superclass.getKind() != TypeKind.DECLARED) { + return Optional.empty(); + } + Element superclassElement = ((DeclaredType) superclass).asElement(); + + return getAnnotatedMethodInfo( + superclassElement instanceof TypeElement + ? (TypeElement) superclassElement + : null, + annotationType + ); } private PreviewConstructor getPreviewConstructor(TypeElement typeElement) { diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java index d82f2ba3a..8d0d2d7f4 100644 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java @@ -16,27 +16,64 @@ package io.flamingock.internal.common.core.preview.builder; import io.flamingock.api.annotations.Change; +import io.flamingock.common.test.processor.AnnotatedElementProcessor; +import io.flamingock.common.test.processor.JavacHelper; +import io.flamingock.internal.common.core.change.RecoveryDescriptor; 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.change.RecoveryDescriptor; +import org.junit.jupiter.api.DisplayName; 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.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; -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 io.flamingock.common.test.processor.JavacHelper.getSourcePath; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class CodePreviewChangeBuilderTest { + private final CodePreviewChangeBuilder codePreviewChangeBuilder = CodePreviewChangeBuilder.instance(); + private final JavacHelper javacHelper = new JavacHelper(); + + @Test + @DisplayName("inherits apply and rollback methods from its template superclass") + void inheritsApplyAndRollbackMethodsFromTemplateSuperclass() throws IOException, URISyntaxException { + // Arrange + Path sourcePath = getSourcePath(getClass(), "/changes/_0001__ChangeUnderTest.java"); + String expectedRollbackPreviewMethodName = "rollback"; + String expectedApplyPreviewMethodName = "apply"; + AtomicInteger changeCount = new AtomicInteger(0); + + // Act + javacHelper.compile( + sourcePath, + new AnnotatedElementProcessor(Change.class, element -> { + changeCount.incrementAndGet(); + codePreviewChangeBuilder.setTypeElement(element); + }) + ); + + // Assert + assertEquals(1, changeCount.get()); + + CodePreviewChange previewChange = codePreviewChangeBuilder.build(); + assertNotNull(previewChange.getApplyPreviewMethod()); + assertEquals(expectedApplyPreviewMethodName, previewChange.getApplyPreviewMethod().getName()); + assertNotNull(previewChange.getRollbackPreviewMethod()); + assertEquals(expectedRollbackPreviewMethodName, previewChange.getRollbackPreviewMethod().getName()); + } + @Test void shouldBuildNullSourceFileForCodeChanges() { CodePreviewChange preview = CodePreviewChangeBuilder.instance() diff --git a/core/flamingock-core-commons/src/test/resources/changes/_0001__ChangeUnderTest.java b/core/flamingock-core-commons/src/test/resources/changes/_0001__ChangeUnderTest.java new file mode 100644 index 000000000..f9ccf963d --- /dev/null +++ b/core/flamingock-core-commons/src/test/resources/changes/_0001__ChangeUnderTest.java @@ -0,0 +1,35 @@ +/* + * 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 test; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; + +abstract class ChangeTemplate { + @Apply + public void apply(String value) { + } + + @Rollback + public void rollback(String value) { + } +} + + +@Change(id = "change-under-test", author = "author") +class _0001__ChangeUnderTest extends ChangeTemplate { +} diff --git a/legacy/mongock-support/build.gradle.kts b/legacy/mongock-support/build.gradle.kts index c0bbd9c00..e409fa3e1 100644 --- a/legacy/mongock-support/build.gradle.kts +++ b/legacy/mongock-support/build.gradle.kts @@ -1,12 +1,7 @@ dependencies { api(project(":core:flamingock-core")) compileOnlyApi("javax.inject:javax.inject:1") + testImplementation(project(":utils:test-util")) } description = "Provides a compatibility layer for Mongock-based applications, including drop-in annotations and audit store migration utilities to Flamingock." - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } -} 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 5eed74b1b..cc2134d20 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 @@ -17,12 +17,12 @@ import com.github.cloudyrock.mongock.ChangeLog; import com.github.cloudyrock.mongock.ChangeSet; +import io.flamingock.internal.common.core.change.RecoveryDescriptor; +import io.flamingock.internal.common.core.change.TargetSystemDescriptor; 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.change.RecoveryDescriptor; -import io.flamingock.internal.common.core.change.TargetSystemDescriptor; import io.flamingock.internal.common.core.util.TypeElementNameUtils; import io.flamingock.internal.util.ReflectionUtil; import io.mongock.api.annotations.BeforeExecution; @@ -38,6 +38,8 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import java.lang.annotation.Annotation; import java.util.ArrayList; @@ -167,6 +169,9 @@ private void validateChangeUnitAnnotation(ChangeUnit changeUnitAnnotation) { private Optional getAnnotatedMethodInfo(TypeElement typeElement, Class annotationType) { + if (typeElement == null) { + return Optional.empty(); + } for (Element enclosedElement : typeElement.getEnclosedElements()) { if (enclosedElement.getKind() == ElementKind.METHOD && enclosedElement.getAnnotation(annotationType) != null) { @@ -176,7 +181,18 @@ private Optional getAnnotatedMethodInfo(TypeElement typeElement, } } - return Optional.empty(); + TypeMirror superclass = typeElement.getSuperclass(); + if (superclass.getKind() != TypeKind.DECLARED) { + return Optional.empty(); + } + Element superclassElement = ((DeclaredType) superclass).asElement(); + + return getAnnotatedMethodInfo( + superclassElement instanceof TypeElement + ? (TypeElement) superclassElement + : null, + annotationType + ); } private PreviewMethod previewMethodFromExecutableElement(ExecutableElement element) { diff --git a/legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java b/legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java new file mode 100644 index 000000000..cca838740 --- /dev/null +++ b/legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java @@ -0,0 +1,74 @@ +/* + * 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.support.mongock.internal.preview.builder; + +import io.flamingock.common.test.processor.AnnotatedElementProcessor; +import io.flamingock.common.test.processor.JavacHelper; +import io.flamingock.internal.common.core.preview.CodePreviewChange; +import io.mongock.api.annotations.ChangeUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static io.flamingock.common.test.processor.JavacHelper.getSourcePath; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class MongockCodePreviewChangeHelperTest { + + private final MongockCodePreviewChangeHelper mongockCodePreviewChangeHelper = new MongockCodePreviewChangeHelper(); + private final JavacHelper javacHelper = new JavacHelper(); + + @Test + @DisplayName("inherits execution and rollback methods from its template superclass") + void inheritsExecutionAndRollbackMethodsFromTemplateSuperclass() throws IOException, URISyntaxException { + // Arrange + Path sourcePath = getSourcePath(getClass(), "/changes/ChangeUnitUnderTest.java"); + String expectedRollbackPreviewMethodName = "rollback"; + String expectedExecutePreviewMethodName = "execute"; + List> actualChanges = Collections.synchronizedList(new ArrayList<>()); + + // Act + javacHelper.compile( + sourcePath, + new AnnotatedElementProcessor(ChangeUnit.class, element -> { + List changes = mongockCodePreviewChangeHelper.getCodePreviewChanges(element, "mongock"); + actualChanges.add(changes); + }) + ); + + // Assert + assertEquals(1, actualChanges.size()); + List changes = actualChanges.get(0); + + assertEquals(1, changes.size()); + CodePreviewChange change = changes.get(0); + + assertNotNull(change.getApplyPreviewMethod()); + String actualExecutePreviewMethodName = change.getApplyPreviewMethod().getName(); + assertEquals(expectedExecutePreviewMethodName, actualExecutePreviewMethodName); + + assertNotNull(change.getRollbackPreviewMethod()); + String actualRollbackPreviewMethodName = change.getRollbackPreviewMethod().getName(); + assertEquals(expectedRollbackPreviewMethodName, actualRollbackPreviewMethodName); + } +} diff --git a/legacy/mongock-support/src/test/resources/changes/ChangeUnitUnderTest.java b/legacy/mongock-support/src/test/resources/changes/ChangeUnitUnderTest.java new file mode 100644 index 000000000..52ea94ed2 --- /dev/null +++ b/legacy/mongock-support/src/test/resources/changes/ChangeUnitUnderTest.java @@ -0,0 +1,34 @@ +/* + * 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 test; + +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; +import io.mongock.api.annotations.RollbackExecution; + +abstract class ChangeUnitTemplate { + @Execution + public void execute(String value) { + } + + @RollbackExecution + public void rollback(String value) { + } +} + +@ChangeUnit(id = "inherited", order = "001") +class ChangeUnitUnderTest extends ChangeUnitTemplate { +} diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/processor/AnnotatedElementProcessor.java b/utils/test-util/src/main/java/io/flamingock/common/test/processor/AnnotatedElementProcessor.java new file mode 100644 index 000000000..3073abd80 --- /dev/null +++ b/utils/test-util/src/main/java/io/flamingock/common/test/processor/AnnotatedElementProcessor.java @@ -0,0 +1,62 @@ +/* + * 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.common.test.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Set; +import java.util.function.Consumer; + +/** + * A simple annotation processor that collects elements annotated with a specific annotation and passes them to a provided consumer. + */ +public class AnnotatedElementProcessor extends AbstractProcessor { + private final Class annotationClass; + private final Consumer elementConsumer; + + /** + * Creates a new AnnotatedElementProcessor for the specified annotation class and element consumer. + * @param annotationClass the annotation class to look for + * @param elementConsumer the consumer to process the annotated elements + */ + public AnnotatedElementProcessor(Class annotationClass, Consumer elementConsumer) { + this.annotationClass = annotationClass; + this.elementConsumer = elementConsumer; + } + + @Override + public Set getSupportedAnnotationTypes() { + return Collections.singleton(annotationClass.getCanonicalName()); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnvironment) { + for (Element element : roundEnvironment.getElementsAnnotatedWith(annotationClass)) { + elementConsumer.accept((TypeElement) element); + } + return false; + } +} diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/processor/JavacHelper.java b/utils/test-util/src/main/java/io/flamingock/common/test/processor/JavacHelper.java new file mode 100644 index 000000000..21f30c535 --- /dev/null +++ b/utils/test-util/src/main/java/io/flamingock/common/test/processor/JavacHelper.java @@ -0,0 +1,95 @@ +/* + * 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.common.test.processor; + +import javax.annotation.processing.Processor; +import javax.tools.*; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A helper class for compiling Java source files with annotation processors during tests. + */ +public class JavacHelper { + private final JavaCompiler compiler; + + /** + * Initializes a new instance of the JavacHelper class and retrieves the system Java compiler. + * Throws an assertion error if the system Java compiler is not available (e.g., when running on a JRE instead of a JDK). + */ + public JavacHelper() { + this.compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(this.compiler, "Tests must run on a JDK with a system Java compiler"); + } + + /** + * Compiles the Java source file at the specified path using the provided annotation processors. + * + * @param sourcePath the path to the Java source file to compile + * @param processors the annotation processors to use during compilation + * @throws IOException if an I/O error occurs during compilation + */ + public void compile(Path sourcePath, Processor... processors) throws IOException { + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + try (StandardJavaFileManager fileManager = + compiler.getStandardFileManager(diagnostics, null, null)) { + fileManager.setLocation(StandardLocation.CLASS_PATH, currentClasspath()); + + JavaCompiler.CompilationTask task = compiler.getTask( + null, + fileManager, + diagnostics, + Collections.singletonList("-proc:only"), + null, + fileManager.getJavaFileObjects(sourcePath.toFile()) + ); + task.setProcessors(Arrays.asList(processors)); + assertTrue(task.call(), () -> diagnostics.getDiagnostics().toString()); + } + } + + /** + * Retrieves the source path of a resource relative to the specified class. + * + * @param klass the class relative to which the resource path is specified + * @param resourcePath the resource path to retrieve + * @return the Path of the resource + * @throws URISyntaxException if the resource URL cannot be converted to a URI + * @throws AssertionError if the resource is not found + */ + public static Path getSourcePath(Class klass, String resourcePath) throws URISyntaxException { + URL sourceResource = klass.getResource(resourcePath); + assertNotNull(sourceResource, "Source resource not found: " + resourcePath); + return Paths.get(sourceResource.toURI()); + } + + private static List currentClasspath() { + return Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator)) + .map(File::new) + .collect(Collectors.toList()); + } +} From 59769ce5819a24a2b34e04be04a57528be648393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Berciano?= Date: Tue, 27 Oct 2026 16:53:44 +0100 Subject: [PATCH 2/3] fix(core): resolve inherited lifecycle methods at runtime and in native images --- .../loaded/CodeLoadedChangeBuilder.java | 27 +++ .../loaded/CodeLoadedChangeBuilderTest.java | 163 +++++++++++++++++- core/flamingock-graalvm/build.gradle.kts | 1 + .../graalvm/RegistrationFeature.java | 14 +- .../graalvm/RegistrationFeatureTest.java | 63 +++++++ 5 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 core/flamingock-graalvm/src/test/java/io/flamingock/graalvm/RegistrationFeatureTest.java diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java index ef2dea64f..1fa749264 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java @@ -284,9 +284,36 @@ private Optional getRollbackMethodFromPreview(CodePreviewChange preview) private Method getMethodFromNameAndParameters(String sourceClassName, String methodName, List methodParametersTypesNames) { Class sourceClass = getClassForName(sourceClassName); + // Match the preview signature at every level so unrelated overloads cannot + // hide the inherited lifecycle method. + for (Class candidate = sourceClass; candidate != null && candidate != Object.class; + candidate = candidate.getSuperclass()) { + for (Method method : candidate.getDeclaredMethods()) { + if (method.getName().equals(methodName) + && matchesPreviewParameters(method, methodParametersTypesNames)) { + return ReflectionUtil.getDeclaredMethodFromParameterTypeNames( + candidate, methodName, methodParametersTypesNames); + } + } + } return ReflectionUtil.getDeclaredMethodFromParameterTypeNames(sourceClass, methodName, methodParametersTypesNames); } + private boolean matchesPreviewParameters(Method method, List parameterNames) { + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != parameterNames.size()) { + return false; + } + for (int i = 0; i < parameterTypes.length; i++) { + // Canonical names cover source-style arrays; getName also accepts binary names. + if (!parameterNames.get(i).equals(parameterTypes[i].getCanonicalName()) + && !parameterNames.get(i).equals(parameterTypes[i].getName())) { + return false; + } + } + return true; + } + private Method getApplyMethodFromAnnotation(Class sourceClass) { Optional firstAnnotatedMethod = ReflectionUtil.findFirstAnnotatedMethod(sourceClass, Apply.class); return firstAnnotatedMethod diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java index f63446c4d..95b3ef58e 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java @@ -247,7 +247,163 @@ void shouldBuildFromAnnotatedClassCorrectlyWhenOrderInAnnotationNull() { assertNull(result.getSourceFile()); } + @Test + void shouldInvokeInheritedPreviewMethodsForBothEditions() throws Exception { + for (boolean legacy : new boolean[]{false, true}) { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + preview(InheritedChange.class.getName(), legacy, true)).build(); + InheritedChange instance = new InheritedChange(); + change.getApplyMethod().invoke(instance, "applied"); + assertEquals("applied", instance.effect); + change.getRollbackMethod().get().invoke(instance); + assertEquals("rolled back", instance.effect); + assertEquals(BaseChange.class, change.getApplyMethod().getDeclaringClass()); + } + } + + @Test + void shouldPreferDirectlyDeclaredPreviewMethods() throws Exception { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + preview(DirectChange.class.getName(), false, true)).build(); + DirectChange instance = new DirectChange(); + change.getApplyMethod().invoke(instance, "applied"); + assertEquals("direct applied", instance.effect); + change.getRollbackMethod().get().invoke(instance); + assertEquals("direct rollback", instance.effect); + assertEquals(DirectChange.class, change.getApplyMethod().getDeclaringClass()); + } + + @Test + void shouldSkipIntermediateOverloadsForInheritedPreviewSignatures() throws Exception { + for (boolean legacy : new boolean[]{false, true}) { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + signaturePreview(OverloadLeaf.class, "apply", "rollback", + Collections.singletonList(String.class.getName()), legacy)).build(); + OverloadLeaf instance = new OverloadLeaf(); + assertArrayEquals(new Class[]{String.class}, change.getApplyMethod().getParameterTypes()); + assertArrayEquals(new Class[]{String.class}, change.getRollbackMethod().get().getParameterTypes()); + change.getApplyMethod().invoke(instance, "applied"); + assertEquals("applied", instance.effect); + change.getRollbackMethod().get().invoke(instance, "rolled back"); + assertEquals("rolled back", instance.effect); + } + } + + @Test + void shouldResolveInheritedPreviewSignaturesWithConcreteOverloads() throws Exception { + for (boolean legacy : new boolean[]{false, true}) { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + signaturePreview(OverloadChild.class, "apply", "rollback", + Collections.singletonList(String.class.getName()), legacy)).build(); + OverloadChild instance = new OverloadChild(); + assertArrayEquals(new Class[]{String.class}, change.getApplyMethod().getParameterTypes()); + assertArrayEquals(new Class[]{String.class}, change.getRollbackMethod().get().getParameterTypes()); + change.getApplyMethod().invoke(instance, "applied"); + assertEquals("applied", instance.effect); + change.getRollbackMethod().get().invoke(instance, "rolled back"); + assertEquals("rolled back", instance.effect); + } + } + + @Test + void shouldResolveDirectPreviewSignaturesWithInheritedOverloads() throws Exception { + for (boolean legacy : new boolean[]{false, true}) { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + signaturePreview(OverloadChild.class, "apply", "rollback", + Collections.singletonList(Integer.class.getName()), legacy)).build(); + OverloadChild instance = new OverloadChild(); + assertArrayEquals(new Class[]{Integer.class}, change.getApplyMethod().getParameterTypes()); + assertArrayEquals(new Class[]{Integer.class}, change.getRollbackMethod().get().getParameterTypes()); + change.getApplyMethod().invoke(instance, 1); + assertEquals("integer apply", instance.effect); + change.getRollbackMethod().get().invoke(instance, 1); + assertEquals("integer rollback", instance.effect); + } + } + + @Test + void shouldMatchInheritedPrimitiveAndArraySignatures() throws Exception { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + signaturePreview(OverloadLeaf.class, "applyArrays", "rollbackArrays", + java.util.Arrays.asList("int", "int[]", "java.lang.String[]"), false)).build(); + OverloadLeaf instance = new OverloadLeaf(); + Class[] expected = {int.class, int[].class, String[].class}; + assertArrayEquals(expected, change.getApplyMethod().getParameterTypes()); + assertArrayEquals(expected, change.getRollbackMethod().get().getParameterTypes()); + change.getApplyMethod().invoke(instance, 7, new int[]{8}, new String[]{"apply"}); + assertEquals("apply78", instance.effect); + change.getRollbackMethod().get().invoke(instance, 9, new int[]{10}, new String[]{"rollback"}); + assertEquals("rollback910", instance.effect); + } + + public static class OverloadBase { + String effect; + + @Apply + public void apply(String value) { effect = value; } + + @io.flamingock.api.annotations.Rollback + public void rollback(String value) { effect = value; } + + public void applyArrays(int value, int[] numbers, String[] text) { + effect = text[0] + value + numbers[0]; + } + + public void rollbackArrays(int value, int[] numbers, String[] text) { + effect = text[0] + value + numbers[0]; + } + } + + public static class OverloadChild extends OverloadBase { + public void apply(Integer value) { effect = "integer apply"; } + public void rollback(Integer value) { effect = "integer rollback"; } + public void applyArrays(Integer value, Integer[] numbers, Object[] text) { } + public void rollbackArrays(Integer value, Integer[] numbers, Object[] text) { } + } + + public static class OverloadLeaf extends OverloadChild { } + + private CodePreviewChange signaturePreview(Class source, String apply, String rollback, + java.util.List parameters, boolean legacy) { + return new CodePreviewChange("signature-change", "001", "flamingock", source.getName(), null, + PreviewConstructor.getDefault(), new PreviewMethod(apply, parameters), + new PreviewMethod(rollback, parameters), false, true, false, null, + RecoveryDescriptor.getDefault(), legacy); + } + + public static class BaseChange { + String effect; + + public void apply(String value) { + effect = value; + } + + public void rollback() { + effect = "rolled back"; + } + } + + public static class MiddleChange extends BaseChange { } + + public static class InheritedChange extends MiddleChange { } + + public static class DirectChange extends MiddleChange { + @Override + public void apply(String value) { + effect = "direct " + value; + } + + @Override + public void rollback() { + effect = "direct rollback"; + } + } + private CodePreviewChange preview(String sourceClassPath) { + return preview(sourceClassPath, false, false); + } + + private CodePreviewChange preview(String sourceClassPath, boolean legacy, boolean rollback) { return new CodePreviewChange( "nested-change", "001", @@ -255,14 +411,15 @@ private CodePreviewChange preview(String sourceClassPath) { sourceClassPath, null, PreviewConstructor.getDefault(), - new PreviewMethod("apply", Collections.emptyList()), - null, + new PreviewMethod("apply", rollback + ? Collections.singletonList(String.class.getName()) : Collections.emptyList()), + rollback ? new PreviewMethod("rollback", Collections.emptyList()) : null, false, true, false, null, RecoveryDescriptor.getDefault(), - false); + legacy); } static class Group { diff --git a/core/flamingock-graalvm/build.gradle.kts b/core/flamingock-graalvm/build.gradle.kts index 38dd2f4a0..ef827a063 100644 --- a/core/flamingock-graalvm/build.gradle.kts +++ b/core/flamingock-graalvm/build.gradle.kts @@ -5,6 +5,7 @@ dependencies { // GraalVM SDK for native image support — compileOnly because it's only needed at // native-image build time, never at JVM runtime. compileOnlyApi("org.graalvm.sdk:graal-sdk:22.3.0") + testImplementation("org.graalvm.sdk:graal-sdk:22.3.0") } description = "GraalVM native image support and configuration for Flamingock applications" 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 4daa5525f..a02d36730 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 @@ -127,12 +127,24 @@ private static void registerProviderInfo() { logger.startRegistrationProcess("user classes"); providers.stream() .flatMap(p -> p.reflectionClasses().stream()) - .forEach(RegistrationFeature::registerClassForReflection); + .forEach(RegistrationFeature::registerUserClassForReflection); logger.completedRegistrationProcess("user classes"); logger.completedRegistrationProcess("provider info"); } + private static void registerUserClassForReflection(String className) { + try { + // Runtime preview lookup also visits ancestors of generated user classes. + for (Class clazz = getClass(className); clazz != null && clazz != Object.class; + clazz = clazz.getSuperclass()) { + registerClassForReflection(clazz); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + private static void registerClassForReflection(String className) { try { registerClassForReflection(getClass(className)); diff --git a/core/flamingock-graalvm/src/test/java/io/flamingock/graalvm/RegistrationFeatureTest.java b/core/flamingock-graalvm/src/test/java/io/flamingock/graalvm/RegistrationFeatureTest.java new file mode 100644 index 000000000..8a0b7f174 --- /dev/null +++ b/core/flamingock-graalvm/src/test/java/io/flamingock/graalvm/RegistrationFeatureTest.java @@ -0,0 +1,63 @@ +/* + * 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.graalvm; + +import org.graalvm.nativeimage.hosted.RuntimeReflection; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mockStatic; + +class RegistrationFeatureTest { + + private static boolean initialized; + + @Test + void shouldRegisterUserHierarchyWithoutInitialization() throws Exception { + try (MockedStatic reflection = mockStatic(RuntimeReflection.class)) { + Method register = RegistrationFeature.class.getDeclaredMethod("registerUserClassForReflection", String.class); + register.setAccessible(true); + register.invoke(null, ConcreteChange.class.getName()); + + for (Class type : new Class[]{ConcreteChange.class, MiddleChange.class, BaseChange.class}) { + reflection.verify(() -> RuntimeReflection.register(type)); + reflection.verify(() -> RuntimeReflection.register(type.getDeclaredMethods())); + } + reflection.verify(() -> RuntimeReflection.register(Object.class), org.mockito.Mockito.never()); + assertFalse(initialized); + } + } + + static class BaseChange { + public void apply() { } + public void rollback() { } + } + + static class MiddleChange extends BaseChange { + public void middle() { } + } + + static class ConcreteChange extends MiddleChange { + static { + initialized = true; + } + + public void concrete() { } + } +} From effb98c616e24f28d4187a608058aba97257233c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Berciano?= Date: Mon, 2 Nov 2026 15:07:54 +0100 Subject: [PATCH 3/3] refactor(core): delegate inherited method lookup to general-util 1.7.0 --- build.gradle.kts | 2 +- .../loaded/CodeLoadedChangeBuilder.java | 30 +------- .../loaded/CodeLoadedChangeBuilderTest.java | 69 ++----------------- 3 files changed, 7 insertions(+), 94 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index f5e411b56..afb2f323d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { val declaredVersion = "1.5.0-SNAPSHOT" version = VersionManager.resolveVersion(declaredVersion, project.hasProperty("release")) - extra["generalUtilVersion"] = "1.6.0" + extra["generalUtilVersion"] = "1.7.0" extra["templateApiVersion"] = "1.3.4" extra["coreApiVersion"] = "1.3.3" extra["sqlVersion"] = "1.3.2" diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java index 1fa749264..ec66efc8c 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java @@ -284,34 +284,8 @@ private Optional getRollbackMethodFromPreview(CodePreviewChange preview) private Method getMethodFromNameAndParameters(String sourceClassName, String methodName, List methodParametersTypesNames) { Class sourceClass = getClassForName(sourceClassName); - // Match the preview signature at every level so unrelated overloads cannot - // hide the inherited lifecycle method. - for (Class candidate = sourceClass; candidate != null && candidate != Object.class; - candidate = candidate.getSuperclass()) { - for (Method method : candidate.getDeclaredMethods()) { - if (method.getName().equals(methodName) - && matchesPreviewParameters(method, methodParametersTypesNames)) { - return ReflectionUtil.getDeclaredMethodFromParameterTypeNames( - candidate, methodName, methodParametersTypesNames); - } - } - } - return ReflectionUtil.getDeclaredMethodFromParameterTypeNames(sourceClass, methodName, methodParametersTypesNames); - } - - private boolean matchesPreviewParameters(Method method, List parameterNames) { - Class[] parameterTypes = method.getParameterTypes(); - if (parameterTypes.length != parameterNames.size()) { - return false; - } - for (int i = 0; i < parameterTypes.length; i++) { - // Canonical names cover source-style arrays; getName also accepts binary names. - if (!parameterNames.get(i).equals(parameterTypes[i].getCanonicalName()) - && !parameterNames.get(i).equals(parameterTypes[i].getName())) { - return false; - } - } - return true; + return ReflectionUtil.getMethodFromParameterTypeNames( + sourceClass, methodName, methodParametersTypesNames, true); } private Method getApplyMethodFromAnnotation(Class sourceClass) { diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java index 95b3ef58e..38a4e177d 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java @@ -273,60 +273,12 @@ void shouldPreferDirectlyDeclaredPreviewMethods() throws Exception { assertEquals(DirectChange.class, change.getApplyMethod().getDeclaringClass()); } - @Test - void shouldSkipIntermediateOverloadsForInheritedPreviewSignatures() throws Exception { - for (boolean legacy : new boolean[]{false, true}) { - CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( - signaturePreview(OverloadLeaf.class, "apply", "rollback", - Collections.singletonList(String.class.getName()), legacy)).build(); - OverloadLeaf instance = new OverloadLeaf(); - assertArrayEquals(new Class[]{String.class}, change.getApplyMethod().getParameterTypes()); - assertArrayEquals(new Class[]{String.class}, change.getRollbackMethod().get().getParameterTypes()); - change.getApplyMethod().invoke(instance, "applied"); - assertEquals("applied", instance.effect); - change.getRollbackMethod().get().invoke(instance, "rolled back"); - assertEquals("rolled back", instance.effect); - } - } - - @Test - void shouldResolveInheritedPreviewSignaturesWithConcreteOverloads() throws Exception { - for (boolean legacy : new boolean[]{false, true}) { - CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( - signaturePreview(OverloadChild.class, "apply", "rollback", - Collections.singletonList(String.class.getName()), legacy)).build(); - OverloadChild instance = new OverloadChild(); - assertArrayEquals(new Class[]{String.class}, change.getApplyMethod().getParameterTypes()); - assertArrayEquals(new Class[]{String.class}, change.getRollbackMethod().get().getParameterTypes()); - change.getApplyMethod().invoke(instance, "applied"); - assertEquals("applied", instance.effect); - change.getRollbackMethod().get().invoke(instance, "rolled back"); - assertEquals("rolled back", instance.effect); - } - } - - @Test - void shouldResolveDirectPreviewSignaturesWithInheritedOverloads() throws Exception { - for (boolean legacy : new boolean[]{false, true}) { - CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( - signaturePreview(OverloadChild.class, "apply", "rollback", - Collections.singletonList(Integer.class.getName()), legacy)).build(); - OverloadChild instance = new OverloadChild(); - assertArrayEquals(new Class[]{Integer.class}, change.getApplyMethod().getParameterTypes()); - assertArrayEquals(new Class[]{Integer.class}, change.getRollbackMethod().get().getParameterTypes()); - change.getApplyMethod().invoke(instance, 1); - assertEquals("integer apply", instance.effect); - change.getRollbackMethod().get().invoke(instance, 1); - assertEquals("integer rollback", instance.effect); - } - } - @Test void shouldMatchInheritedPrimitiveAndArraySignatures() throws Exception { CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( - signaturePreview(OverloadLeaf.class, "applyArrays", "rollbackArrays", + signaturePreview(InheritedArrayChange.class, "applyArrays", "rollbackArrays", java.util.Arrays.asList("int", "int[]", "java.lang.String[]"), false)).build(); - OverloadLeaf instance = new OverloadLeaf(); + InheritedArrayChange instance = new InheritedArrayChange(); Class[] expected = {int.class, int[].class, String[].class}; assertArrayEquals(expected, change.getApplyMethod().getParameterTypes()); assertArrayEquals(expected, change.getRollbackMethod().get().getParameterTypes()); @@ -336,15 +288,9 @@ void shouldMatchInheritedPrimitiveAndArraySignatures() throws Exception { assertEquals("rollback910", instance.effect); } - public static class OverloadBase { + public static class ArrayBaseChange { String effect; - @Apply - public void apply(String value) { effect = value; } - - @io.flamingock.api.annotations.Rollback - public void rollback(String value) { effect = value; } - public void applyArrays(int value, int[] numbers, String[] text) { effect = text[0] + value + numbers[0]; } @@ -354,14 +300,7 @@ public void rollbackArrays(int value, int[] numbers, String[] text) { } } - public static class OverloadChild extends OverloadBase { - public void apply(Integer value) { effect = "integer apply"; } - public void rollback(Integer value) { effect = "integer rollback"; } - public void applyArrays(Integer value, Integer[] numbers, Object[] text) { } - public void rollbackArrays(Integer value, Integer[] numbers, Object[] text) { } - } - - public static class OverloadLeaf extends OverloadChild { } + public static class InheritedArrayChange extends ArrayBaseChange { } private CodePreviewChange signaturePreview(Class source, String apply, String rollback, java.util.List parameters, boolean legacy) {