Skip to content

Commit 2f64138

Browse files
authored
fix: handle ClassNotFoundException with nested/inner change classes (#891)
Refs #889
1 parent 20d580d commit 2f64138

File tree

14 files changed

+464
-45
lines changed

14 files changed

+464
-45
lines changed

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/ChangeOrderExtractor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ public final class ChangeOrderExtractor {
2929
// Captures ORDER before double underscore separator
3030
private static final Pattern SIMPLE_FILE_ORDER_REGEX_PATTERN = Pattern.compile("^_(.+?)__(.+)$");
3131

32-
// For class names: must have _order__ at the beginning of the class name after package
33-
// (e.g., com.mycompany.mypackage._002__MyChange or com.mycompany.OuterClass$_V1_2_3__InnerChange)
32+
// For class names: must have _order__ at the beginning of the class name, after package, or after enclosing class
33+
// (e.g., _001__MyChange, com.mycompany.mypackage._002__MyChange or com.mycompany.OuterClass$_V1_2_3__InnerChange)
3434
// Captures ORDER before double underscore separator
35-
private static final Pattern FILE_WITH_PACKAGE_ORDER_REGEX_PATTERN = Pattern.compile("[.$]_(.+?)__(.+)$");
35+
private static final Pattern FILE_WITH_PACKAGE_ORDER_REGEX_PATTERN = Pattern.compile("(?:^|[.$])_(.+?)__(.+)$");
3636

3737
private ChangeOrderExtractor() {
3838
}

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/CodePreviewChange.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ public CodePreviewChange(String id,
5050
this.previewConstructor = previewConstructor;
5151
this.applyPreviewMethod = applyPreviewMethod;
5252
this.rollbackPreviewMethod = rollbackPreviewMethod;
53-
this.sourcePackage = sourceClassPath.substring(0, sourceClassPath.lastIndexOf("."));
53+
this.sourcePackage = extractSourcePackage(sourceClassPath);
54+
}
55+
56+
private String extractSourcePackage(String sourceClassPath) {
57+
int packageEndIndex = sourceClassPath != null ? sourceClassPath.lastIndexOf(".") : -1;
58+
return packageEndIndex > 0 ? sourceClassPath.substring(0, packageEndIndex) : "";
5459
}
5560

5661
public PreviewConstructor getPreviewConstructor() { return previewConstructor; }

core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilder.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.flamingock.internal.common.core.preview.PreviewMethod;
2929
import io.flamingock.internal.common.core.task.RecoveryDescriptor;
3030
import io.flamingock.internal.common.core.task.TargetSystemDescriptor;
31+
import io.flamingock.internal.common.core.util.TypeElementNameUtils;
3132
import io.flamingock.internal.util.ReflectionUtil;
3233
import org.jetbrains.annotations.NotNull;
3334

@@ -132,8 +133,9 @@ CodePreviewTaskBuilder setTypeElement(TypeElement typeElement) {
132133
Recovery recoveryAnnotation = typeElement.getAnnotation(Recovery.class);
133134

134135
if(changeAnnotation != null) {
136+
validateNestedChangeClass(typeElement);
135137
String changeId = changeAnnotation.id();
136-
String classPath = typeElement.getQualifiedName().toString();
138+
String classPath = TypeElementNameUtils.getBinaryName(typeElement);
137139
String order = ChangeOrderExtractor.extractOrderFromClassName(changeId, classPath);
138140
setId(changeId);
139141
setOrder(order);
@@ -157,6 +159,14 @@ CodePreviewTaskBuilder setTypeElement(TypeElement typeElement) {
157159
return this;
158160
}
159161

162+
private void validateNestedChangeClass(TypeElement typeElement) {
163+
if (TypeElementNameUtils.isNonStaticNestedType(typeElement)) {
164+
throw new FlamingockException(
165+
"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.",
166+
TypeElementNameUtils.getBinaryName(typeElement));
167+
}
168+
}
169+
160170

161171

162172
@Override
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.common.core.util;
17+
18+
import javax.lang.model.element.Element;
19+
import javax.lang.model.element.ElementKind;
20+
import javax.lang.model.element.Modifier;
21+
import javax.lang.model.element.TypeElement;
22+
23+
public final class TypeElementNameUtils {
24+
25+
private TypeElementNameUtils() {
26+
}
27+
28+
public static String getBinaryName(TypeElement typeElement) {
29+
Element enclosingElement = typeElement.getEnclosingElement();
30+
if (enclosingElement instanceof TypeElement && isTypeElement(enclosingElement)) {
31+
return getBinaryName((TypeElement) enclosingElement) + "$" + typeElement.getSimpleName();
32+
}
33+
return typeElement.getQualifiedName().toString();
34+
}
35+
36+
private static boolean isTypeElement(Element element) {
37+
ElementKind kind = element.getKind();
38+
return kind == ElementKind.CLASS
39+
|| kind == ElementKind.INTERFACE
40+
|| kind == ElementKind.ENUM
41+
|| kind == ElementKind.ANNOTATION_TYPE;
42+
}
43+
44+
public static boolean isNonStaticNestedType(TypeElement typeElement) {
45+
return typeElement.getEnclosingElement() instanceof TypeElement
46+
&& !typeElement.getModifiers().contains(Modifier.STATIC);
47+
}
48+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2026 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.internal.common.core.preview;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
class ChangeOrderExtractorTest {
23+
24+
@Test
25+
void shouldExtractOrderFromDefaultPackageClassName() {
26+
String order = ChangeOrderExtractor.extractOrderFromClassName("dummy-change", "_0001__DummyChange");
27+
28+
assertEquals("0001", order);
29+
}
30+
31+
@Test
32+
void shouldExtractOrderFromPackagedClassName() {
33+
String order = ChangeOrderExtractor.extractOrderFromClassName("dummy-change", "io.flamingock._0002__DummyChange");
34+
35+
assertEquals("0002", order);
36+
}
37+
38+
@Test
39+
void shouldExtractOrderFromNestedBinaryClassName() {
40+
String order = ChangeOrderExtractor.extractOrderFromClassName("dummy-change", "io.flamingock.ChangeGroup$_0003__DummyChange");
41+
42+
assertEquals("0003", order);
43+
}
44+
}

core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewTaskBuilderTest.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,25 @@
1515
*/
1616
package io.flamingock.internal.common.core.preview.builder;
1717

18+
import io.flamingock.api.annotations.Change;
19+
import io.flamingock.internal.common.core.error.FlamingockException;
1820
import io.flamingock.internal.common.core.preview.CodePreviewChange;
1921
import io.flamingock.internal.common.core.preview.PreviewConstructor;
2022
import io.flamingock.internal.common.core.preview.PreviewMethod;
2123
import io.flamingock.internal.common.core.task.RecoveryDescriptor;
2224
import org.junit.jupiter.api.Test;
2325

26+
import javax.lang.model.element.Element;
27+
import javax.lang.model.element.ElementKind;
28+
import javax.lang.model.element.Name;
29+
import javax.lang.model.element.TypeElement;
2430
import java.util.Collections;
2531

32+
import static org.junit.jupiter.api.Assertions.assertEquals;
2633
import static org.junit.jupiter.api.Assertions.assertNull;
34+
import static org.junit.jupiter.api.Assertions.assertThrows;
35+
import static org.mockito.Mockito.mock;
36+
import static org.mockito.Mockito.when;
2737

2838
class CodePreviewTaskBuilderTest {
2939

@@ -45,4 +55,101 @@ void shouldBuildNullSourceFileForCodeChanges() {
4555

4656
assertNull(preview.getSourceFile());
4757
}
58+
59+
@Test
60+
void shouldExtractSourcePackageFromNestedBinaryClassName() {
61+
CodePreviewChange preview = buildPreview("io.flamingock.ChangeGroup$_001__NestedChange");
62+
63+
assertEquals("io.flamingock", preview.getSourcePackage());
64+
}
65+
66+
@Test
67+
void shouldSetEmptySourcePackageWhenSourceClassIsNotDeclaredInPackage() {
68+
CodePreviewChange preview = buildPreview("_001__DefaultPackageChange");
69+
70+
assertEquals("", preview.getSourcePackage());
71+
}
72+
73+
@Test
74+
void shouldThrowWhenChangeIsNonStaticNestedClass() {
75+
TypeElement group = mockTypeElement("io.flamingock.GroupClass", "GroupClass", packageElement());
76+
TypeElement change = mockTypeElement("io.flamingock.GroupClass._1001__InnerChange", "_1001__InnerChange", group);
77+
Change changeAnnotation = changeAnnotation();
78+
when(change.getAnnotation(Change.class)).thenReturn(changeAnnotation);
79+
80+
FlamingockException exception = assertThrows(
81+
FlamingockException.class,
82+
() -> CodePreviewTaskBuilder.instance(change).build());
83+
84+
assertEquals(
85+
"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.",
86+
exception.getMessage());
87+
}
88+
89+
private CodePreviewChange buildPreview(String sourceClassPath) {
90+
return CodePreviewTaskBuilder.instance()
91+
.setId("test-id")
92+
.setOrder("001")
93+
.setAuthor("author")
94+
.setSourceClassPath(sourceClassPath)
95+
.setConstructor(PreviewConstructor.getDefault())
96+
.setApplyMethod(new PreviewMethod("apply", Collections.emptyList()))
97+
.setRollbackMethod(null)
98+
.setRunAlways(false)
99+
.setTransactionalFlag(true)
100+
.setSystem(false)
101+
.setRecovery(RecoveryDescriptor.getDefault())
102+
.build();
103+
}
104+
105+
private TypeElement mockTypeElement(String qualifiedName, String simpleName, Element enclosingElement) {
106+
TypeElement typeElement = mock(TypeElement.class);
107+
when(typeElement.getKind()).thenReturn(ElementKind.CLASS);
108+
when(typeElement.getQualifiedName()).thenReturn(name(qualifiedName));
109+
when(typeElement.getSimpleName()).thenReturn(name(simpleName));
110+
when(typeElement.getEnclosingElement()).thenReturn(enclosingElement);
111+
when(typeElement.getModifiers()).thenReturn(Collections.emptySet());
112+
return typeElement;
113+
}
114+
115+
private Element packageElement() {
116+
Element element = mock(Element.class);
117+
when(element.getKind()).thenReturn(ElementKind.PACKAGE);
118+
return element;
119+
}
120+
121+
private Change changeAnnotation() {
122+
Change change = mock(Change.class);
123+
when(change.id()).thenReturn("inner-change");
124+
return change;
125+
}
126+
127+
private Name name(String value) {
128+
return new Name() {
129+
@Override
130+
public boolean contentEquals(CharSequence cs) {
131+
return value.contentEquals(cs);
132+
}
133+
134+
@Override
135+
public int length() {
136+
return value.length();
137+
}
138+
139+
@Override
140+
public char charAt(int index) {
141+
return value.charAt(index);
142+
}
143+
144+
@Override
145+
public CharSequence subSequence(int start, int end) {
146+
return value.subSequence(start, end);
147+
}
148+
149+
@Override
150+
public String toString() {
151+
return value;
152+
}
153+
};
154+
}
48155
}

0 commit comments

Comments
 (0)