Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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;
}
};
}
}
Loading
Loading