Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions core/flamingock-core-commons/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -195,6 +198,9 @@ private CodePreviewChange getCodePreviewChange() {

private Optional<PreviewMethod> getAnnotatedMethodInfo(TypeElement typeElement,
Class<? extends Annotation> annotationType) {
if (typeElement == null) {
return Optional.empty();
}
for (Element enclosedElement : typeElement.getEnclosedElements()) {
if (enclosedElement.getKind() == ElementKind.METHOD &&
enclosedElement.getAnnotation(annotationType) != null) {
Expand All @@ -208,7 +214,18 @@ private Optional<PreviewMethod> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,8 @@ private Optional<Method> getRollbackMethodFromPreview(CodePreviewChange preview)

private Method getMethodFromNameAndParameters(String sourceClassName, String methodName, List<String> methodParametersTypesNames) {
Class<?> sourceClass = getClassForName(sourceClassName);
return ReflectionUtil.getDeclaredMethodFromParameterTypeNames(sourceClass, methodName, methodParametersTypesNames);
return ReflectionUtil.getMethodFromParameterTypeNames(
sourceClass, methodName, methodParametersTypesNames, true);
}

private Method getApplyMethodFromAnnotation(Class<?> sourceClass) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,22 +247,118 @@ 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 shouldMatchInheritedPrimitiveAndArraySignatures() throws Exception {
CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview(
signaturePreview(InheritedArrayChange.class, "applyArrays", "rollbackArrays",
java.util.Arrays.asList("int", "int[]", "java.lang.String[]"), false)).build();
InheritedArrayChange instance = new InheritedArrayChange();
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 ArrayBaseChange {
String effect;

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 InheritedArrayChange extends ArrayBaseChange { }

private CodePreviewChange signaturePreview(Class<?> source, String apply, String rollback,
java.util.List<String> 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",
"flamingock",
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 {
Expand Down
1 change: 1 addition & 0 deletions core/flamingock-graalvm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RuntimeReflection> 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() { }
}
}
Loading
Loading