From 1ad63f3b42d06cf8ad1b8d1a2db508557995e133 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Tue, 19 May 2026 11:55:58 +0200 Subject: [PATCH 1/2] feat(migration): preserve AF4 MessageOriginProvider metadata key names in AF5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AF5 renamed MessageOriginProvider's default metadata keys: - AF4 `traceId` (propagated originating-message id) → AF5 `correlationId` - AF4 `correlationId` (current-message id, direct cause) → AF5 `causationId` A no-args `new MessageOriginProvider()` in AF5 silently emits metadata under the new key names, breaking downstream consumers that still read the old keys. Two new recipes address this: `MigrateMessageOriginProviderDefaultKeys` (added to Axon4ToAxon5Messaging): - Replaces every no-args `new MessageOriginProvider()` with `new MessageOriginProvider("traceId", "correlationId")` to preserve the AF4-compatible key names. Idempotent; explicit-arg forms are left alone. `AddMessageOriginProviderSpringBeanConfiguration` (added to Axon4ToAxon5SpringBootExtension): - In Spring Boot apps, creates (or updates) a `CorrelationDataProviderConfiguration` `@Configuration` class with a `@Bean` returning the AF4-compatible provider. - Only triggers on a no-args constructor; beans that already carry explicit args are left completely untouched (developer's intentional choice). - Generates the config class in the `@SpringBootApplication` root package when the class does not yet exist; adds the `@Bean` method to an existing class. --- ...OriginProviderSpringBeanConfiguration.java | 290 +++++++++++++++++ ...grateMessageOriginProviderDefaultKeys.java | 146 +++++++++ .../axon4-to-axon5-extension-spring.yml | 11 + .../rewrite/axon4-to-axon5-messaging.yml | 11 + ...inProviderSpringBeanConfigurationTest.java | 303 ++++++++++++++++++ ...eMessageOriginProviderDefaultKeysTest.java | 170 ++++++++++ 6 files changed, 931 insertions(+) create mode 100644 migration/src/main/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfiguration.java create mode 100644 migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java create mode 100644 migration/src/test/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfigurationTest.java create mode 100644 migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysTest.java diff --git a/migration/src/main/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfiguration.java b/migration/src/main/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfiguration.java new file mode 100644 index 0000000000..b0994802c8 --- /dev/null +++ b/migration/src/main/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfiguration.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2010-2026. Axon Framework + * + * 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 org.axonframework.migration; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.ScanningRecipe; +import org.openrewrite.SourceFile; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.java.tree.TypeUtils; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * In Spring Boot applications that use {@code MessageOriginProvider}, creates (or updates) a + * {@code CorrelationDataProviderConfiguration} {@code @Configuration} class that registers the + * provider as a Spring bean with Axon Framework 4-compatible metadata key names. + *

+ * Spring Boot applications typically rely on bean discovery to register + * {@link org.axonframework.messaging.core.correlation.CorrelationDataProvider} implementations. + * After the AF4 → AF5 migration the default constructor of {@code MessageOriginProvider} writes + * metadata under new key names: AF4's {@code "traceId"} (originating-message id, propagated) + * became AF5's default {@code "correlationId"}, and AF4's {@code "correlationId"} (current-message + * id, the direct cause) became AF5's default {@code "causationId"}. This recipe wires the provider + * as a Spring bean with explicit {@code correlationKey="traceId"} / + * {@code causationKey="correlationId"}, preserving the AF4-compatible key names in message + * metadata so existing consumers are not broken. + *

+ * Behavior: + *

+ *

+ * Intended to run as part of the {@code Axon4ToAxon5SpringBootExtension} recipe so that it has + * access to the Spring Boot classpath. Must run after {@code Axon4ToAxon5Messaging} so that + * package renames have already produced the AF5 FQNs in the imports. + * + * @author Mateusz Nowak + * @since 5.1.1 + */ +public class AddMessageOriginProviderSpringBeanConfiguration + extends ScanningRecipe { + + private static final String MOP_AF4 = + "org.axonframework.messaging.correlation.MessageOriginProvider"; + private static final String MOP_AF5 = + "org.axonframework.messaging.core.correlation.MessageOriginProvider"; + private static final String CDP_AF5 = + "org.axonframework.messaging.core.correlation.CorrelationDataProvider"; + private static final String SPRING_BOOT_APP_FQN = + "org.springframework.boot.autoconfigure.SpringBootApplication"; + private static final String BEAN_FQN = + "org.springframework.context.annotation.Bean"; + private static final String CONFIGURATION_FQN = + "org.springframework.context.annotation.Configuration"; + private static final String CONFIG_CLASS_NAME = "CorrelationDataProviderConfiguration"; + private static final String BEAN_METHOD_NAME = "messageOriginProvider"; + + public static class Accumulator { + + boolean messageOriginProviderUsed; + boolean configClassExists; + /** True when the {@code messageOriginProvider} @Bean already returns an explicit-args constructor. */ + boolean configClassHasCustomArgs; + String rootPackage; + } + + @Override + public String getDisplayName() { + return "Create CorrelationDataProviderConfiguration Spring bean for MessageOriginProvider"; + } + + @Override + public String getDescription() { + return "In Spring Boot applications using MessageOriginProvider, creates (or updates) a " + + "`CorrelationDataProviderConfiguration` @Configuration class with a @Bean method " + + "returning `new MessageOriginProvider(\"traceId\", \"correlationId\")` to register " + + "the provider as a Spring bean with Axon Framework 4-compatible metadata key names. " + + "AF5 renamed the keys: AF4's `traceId` (propagated originating-message id) became " + + "AF5's `correlationKey`, and AF4's `correlationId` (direct-cause id) became " + + "AF5's `causationKey`. This bean preserves the old key names in output metadata."; + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + return new JavaIsoVisitor<>() { + + @Override + public J.NewClass visitNewClass(J.NewClass nc, ExecutionContext ctx) { + if (isMessageOriginProviderNoArgs(nc)) { + acc.messageOriginProviderUsed = true; + } + return super.visitNewClass(nc, ctx); + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, + ExecutionContext ctx) { + if (CONFIG_CLASS_NAME.equals(cd.getSimpleName())) { + acc.configClassExists = true; + } + if (acc.rootPackage == null && isSpringBootApplication(cd)) { + if (cd.getType() != null) { + acc.rootPackage = cd.getType().getPackageName(); + } + } + return super.visitClassDeclaration(cd, ctx); + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, + ExecutionContext ctx) { + // Detect whether the existing @Bean method already uses explicit constructor args. + // If it does, the developer chose their own keys and we must not override them. + J.ClassDeclaration enclosingClass = getCursor().firstEnclosing(J.ClassDeclaration.class); + if (enclosingClass != null + && CONFIG_CLASS_NAME.equals(enclosingClass.getSimpleName()) + && BEAN_METHOD_NAME.equals(method.getName().getSimpleName()) + && method.getBody() != null) { + for (Statement stmt : method.getBody().getStatements()) { + if (stmt instanceof J.Return ret + && ret.getExpression() instanceof J.NewClass nc + && isMessageOriginProviderClass(nc) + && !isNoArgs(nc)) { + acc.configClassHasCustomArgs = true; + } + } + } + return super.visitMethodDeclaration(method, ctx); + } + + private boolean isMessageOriginProviderClass(J.NewClass nc) { + if (TypeUtils.isOfClassType(nc.getType(), MOP_AF4) + || TypeUtils.isOfClassType(nc.getType(), MOP_AF5)) { + return true; + } + if (nc.getClazz() instanceof J.Identifier) { + return "MessageOriginProvider".equals( + ((J.Identifier) nc.getClazz()).getSimpleName()); + } + return false; + } + + private boolean isNoArgs(J.NewClass nc) { + List args = nc.getArguments(); + return args == null + || args.isEmpty() + || (args.size() == 1 && args.get(0) instanceof J.Empty); + } + + private boolean isMessageOriginProviderNoArgs(J.NewClass nc) { + return isNoArgs(nc) && isMessageOriginProviderClass(nc); + } + + private boolean isSpringBootApplication(J.ClassDeclaration cd) { + for (J.Annotation ann : cd.getLeadingAnnotations()) { + if (TypeUtils.isOfClassType(ann.getType(), SPRING_BOOT_APP_FQN)) { + return true; + } + if (ann.getAnnotationType() instanceof J.Identifier) { + String simpleName = ((J.Identifier) ann.getAnnotationType()).getSimpleName(); + if ("SpringBootApplication".equals(simpleName)) { + return true; + } + } + } + return false; + } + }; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + if (!acc.messageOriginProviderUsed || !acc.configClassExists + || acc.configClassHasCustomArgs) { + // configClassHasCustomArgs: the @Bean already returns an explicit-args constructor — + // the developer intentionally chose their own key names, so we must not override them. + return TreeVisitor.noop(); + } + return new JavaIsoVisitor<>() { + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, + ExecutionContext ctx) { + J.ClassDeclaration result = super.visitClassDeclaration(cd, ctx); + if (!CONFIG_CLASS_NAME.equals(cd.getSimpleName())) { + return result; + } + if (hasBeanMethod(result)) { + // Method already exists (with no-args constructor). + // MigrateMessageOriginProviderDefaultKeys handles updating the args. + return result; + } + result = JavaTemplate.builder( + "@Bean\n" + + "public CorrelationDataProvider messageOriginProvider() {\n" + + " return new MessageOriginProvider(\"traceId\", \"correlationId\");\n" + + "}\n") + .imports(CDP_AF5, MOP_AF5, BEAN_FQN) + .javaParser(JavaParser.fromJavaVersion().classpath(JavaParser.runtimeClasspath())) + .build() + .apply(getCursor(), result.getBody().getCoordinates().lastStatement()); + maybeAddImport(CDP_AF5, null, false); + maybeAddImport(MOP_AF5, null, false); + maybeAddImport(BEAN_FQN, null, false); + return result; + } + + private boolean hasBeanMethod(J.ClassDeclaration cd) { + for (Statement stmt : cd.getBody().getStatements()) { + if (stmt instanceof J.MethodDeclaration method) { + if (BEAN_METHOD_NAME.equals(method.getName().getSimpleName())) { + return true; + } + } + } + return false; + } + }; + } + + @Override + public Collection generate(Accumulator acc, + Collection generatedInThisCycle, + ExecutionContext ctx) { + if (!acc.messageOriginProviderUsed || acc.configClassExists || acc.rootPackage == null) { + return Collections.emptyList(); + } + String pkg = acc.rootPackage; + String source = buildConfigClassSource(pkg); + Path sourcePath = Paths.get( + "src/main/java/" + pkg.replace('.', '/') + "/" + CONFIG_CLASS_NAME + ".java"); + List result = new ArrayList<>(); + JavaParser.fromJavaVersion() + .build() + .parse(source) + .forEach(sf -> result.add(sf.withSourcePath(sourcePath))); + return result; + } + + private static String buildConfigClassSource(String pkg) { + return "package " + pkg + ";\n\n" + + "import " + CDP_AF5 + ";\n" + + "import " + MOP_AF5 + ";\n" + + "import " + BEAN_FQN + ";\n" + + "import " + CONFIGURATION_FQN + ";\n\n" + + "@Configuration\n" + + "public class " + CONFIG_CLASS_NAME + " {\n\n" + + " @Bean\n" + + " public CorrelationDataProvider messageOriginProvider() {\n" + + " return new MessageOriginProvider(\"traceId\", \"correlationId\");\n" + + " }\n" + + "}\n"; + } +} diff --git a/migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java b/migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java new file mode 100644 index 0000000000..467deb3a19 --- /dev/null +++ b/migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2010-2026. Axon Framework + * + * 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 org.axonframework.migration; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.SourceFile; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.marker.Markers; + +import java.util.ArrayList; +import java.util.List; + +/** + * Replaces no-args {@code new MessageOriginProvider()} with + * {@code new MessageOriginProvider("traceId", "correlationId")} to preserve the Axon Framework 4 + * default metadata key names after migrating to AF5. + *

+ * In AF4, {@code MessageOriginProvider} put two keys into message metadata: + *

+ * AF5 renamed the keys to better reflect their semantics: + * + * A no-args {@code new MessageOriginProvider()} in AF5 therefore writes metadata under the + * new key names. Downstream systems that still read the old AF4 keys + * ({@code "correlationId"} and {@code "traceId"}) would silently stop receiving those values. + * This recipe pins the constructor to + * {@code new MessageOriginProvider("traceId", "correlationId")} so that the provider continues + * to write metadata under the AF4-compatible key names. + *

+ * Idempotent — constructor calls that already supply explicit arguments are left unchanged. + * Handles both the AF4 FQN ({@code org.axonframework.messaging.correlation.MessageOriginProvider}) + * and the AF5 FQN ({@code org.axonframework.messaging.core.correlation.MessageOriginProvider}), + * so it is order-independent with respect to the package-rename recipe in + * {@code Axon4ToAxon5Messaging}. + * + * @author Mateusz Nowak + * @since 5.1.1 + */ +public class MigrateMessageOriginProviderDefaultKeys extends Recipe { + + private static final String MOP_AF4 = + "org.axonframework.messaging.correlation.MessageOriginProvider"; + private static final String MOP_AF5 = + "org.axonframework.messaging.core.correlation.MessageOriginProvider"; + + @Override + public String getDisplayName() { + return "Pin MessageOriginProvider to Axon Framework 4 default metadata keys"; + } + + @Override + public String getDescription() { + return "Replaces no-args `new MessageOriginProvider()` with " + + "`new MessageOriginProvider(\"traceId\", \"correlationId\")` to preserve the " + + "Axon Framework 4 metadata key names. AF5 renamed the keys: AF4's `traceId` " + + "(originating message, propagated) became AF5's default `correlationId`, and " + + "AF4's `correlationId` (current message) became AF5's default `causationId`. " + + "This recipe pins the AF4-compatible key names so downstream consumers are not broken."; + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor<>() { + + @Override + public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { + return sourceFile instanceof J.CompilationUnit; + } + + @Override + public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) { + J.NewClass nc = super.visitNewClass(newClass, ctx); + if (!isMessageOriginProviderNoArgs(nc)) { + return nc; + } + // Replace the empty argument list with explicit AF4-compatible key names. + // AF5 constructor: MessageOriginProvider(String correlationKey, String causationKey) + // To emit AF4 key names in metadata we pass: + // correlationKey = "traceId" (AF4's "traceId" → now called "correlationKey" in AF5) + // causationKey = "correlationId" (AF4's "correlationId" → now called "causationKey" in AF5) + // The first argument gets no leading space; the second gets a single space so + // the output reads `new MessageOriginProvider("traceId", "correlationId")`. + J.Literal traceIdAsCorrelation = new J.Literal( + Tree.randomId(), Space.EMPTY, Markers.EMPTY, + "traceId", "\"traceId\"", null, JavaType.Primitive.String); + J.Literal correlationIdAsCausation = new J.Literal( + Tree.randomId(), Space.format(" "), Markers.EMPTY, + "correlationId", "\"correlationId\"", null, JavaType.Primitive.String); + List newArgs = new ArrayList<>(2); + newArgs.add(traceIdAsCorrelation); + newArgs.add(correlationIdAsCausation); + return nc.withArguments(newArgs); + } + + private boolean isMessageOriginProviderNoArgs(J.NewClass nc) { + List args = nc.getArguments(); + boolean noArgs = args == null + || args.isEmpty() + || (args.size() == 1 && args.get(0) instanceof J.Empty); + if (!noArgs) { + return false; + } + if (TypeUtils.isOfClassType(nc.getType(), MOP_AF4) + || TypeUtils.isOfClassType(nc.getType(), MOP_AF5)) { + return true; + } + // Fallback for sources whose imports haven't been resolved yet (e.g., when this + // recipe runs before the package-rename recipe within the same cycle). + if (nc.getClazz() instanceof J.Identifier) { + return "MessageOriginProvider".equals( + ((J.Identifier) nc.getClazz()).getSimpleName()); + } + return false; + } + }; + } +} diff --git a/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-extension-spring.yml b/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-extension-spring.yml index 2f02c4104f..355d92249a 100644 --- a/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-extension-spring.yml +++ b/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-extension-spring.yml @@ -137,6 +137,17 @@ recipeList: # developer and the next-running LLM-driven skill see it immediately. - org.axonframework.migration.AnnotateObsoleteSequencingPolicyProperty + # ── MessageOriginProvider: create Spring @Bean with AF4-compatible keys ─ + # Spring Boot applications rely on bean discovery to register + # CorrelationDataProvider implementations. This recipe creates (or updates) + # a `CorrelationDataProviderConfiguration` @Configuration class that exposes + # MessageOriginProvider as a bean with explicit "correlationId" / "traceId" + # keys, preserving the AF4-compatible key names that the surrounding system + # may already depend on. + # Runs after MigrateMessageOriginProviderDefaultKeys (in Axon4ToAxon5Messaging) + # which replaces no-args constructors; this recipe adds the Spring wiring on top. + - org.axonframework.migration.AddMessageOriginProviderSpringBeanConfiguration + --- type: specs.openrewrite.org/v1beta/recipe name: org.axonframework.migration.Axon4ToAxon5SpringBootActuatorExtension diff --git a/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-messaging.yml b/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-messaging.yml index 62a2788f9f..2ca4222764 100644 --- a/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-messaging.yml +++ b/migration/src/main/resources/META-INF/rewrite/axon4-to-axon5-messaging.yml @@ -361,3 +361,14 @@ recipeList: # `CommandGateway`. Anything outside the supported body shapes (single dispatch / try/catch with one # dispatch per branch) is left untouched so the build still surfaces those call sites for the human. - org.axonframework.migration.MigrateCommandGatewayInEventHandler + + # ── MessageOriginProvider: pin AF4-compatible default metadata keys ────── + # AF5 renamed MessageOriginProvider's default metadata keys: + # AF4 `traceId` (propagated originating-message id) → AF5 `correlationId` + # AF4 `correlationId` (current-message id, direct cause) → AF5 `causationId` + # A no-args `new MessageOriginProvider()` in AF5 therefore writes metadata + # under the NEW key names, silently breaking downstream consumers that read + # the old AF4 keys. This recipe replaces every no-args constructor call with + # `new MessageOriginProvider("traceId", "correlationId")` so the provider + # continues to emit metadata under the AF4-compatible key names. + - org.axonframework.migration.MigrateMessageOriginProviderDefaultKeys diff --git a/migration/src/test/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfigurationTest.java b/migration/src/test/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfigurationTest.java new file mode 100644 index 0000000000..483e50976f --- /dev/null +++ b/migration/src/test/java/org/axonframework/migration/AddMessageOriginProviderSpringBeanConfigurationTest.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2010-2026. Axon Framework + * + * 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 org.axonframework.migration; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; + +import static org.openrewrite.java.Assertions.java; + +/** + * Verifies the {@link AddMessageOriginProviderSpringBeanConfiguration} recipe creates (or updates) + * a {@code CorrelationDataProviderConfiguration} Spring {@code @Configuration} class. + *

+ * The recipe triggers only on a no-args {@code new MessageOriginProvider()} + * constructor call — explicit-arg forms such as + * {@code new MessageOriginProvider("myKey", "myOther")} are left untouched so that developers who + * intentionally chose custom key names are not overridden. + *

+ * The generated / injected bean uses {@code new MessageOriginProvider("traceId", "correlationId")}: + * {@code correlationKey="traceId"} preserves the AF4 trace-id key name, and + * {@code causationKey="correlationId"} preserves the AF4 correlation-id key name. + */ +class AddMessageOriginProviderSpringBeanConfigurationTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AddMessageOriginProviderSpringBeanConfiguration()) + .typeValidationOptions(TypeValidation.none()); + } + + @Test + void generatesConfigClassWhenNoneExistsInSpringBootApp() { + // when: @SpringBootApplication class present + no-args MessageOriginProvider usage + // then: a new CorrelationDataProviderConfiguration is generated in the root package. + // Note: JavaParser preserves the blank lines from the generated source template — + // blank line between `package` and imports, blank line inside the class body. + rewriteRun( + java( + """ + package com.example; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + class Application {} + """ + ), + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class SomeHandler { + private MessageOriginProvider provider = new MessageOriginProvider(); + } + """ + ), + java( + null, + """ + package com.example; + + import org.axonframework.messaging.core.correlation.CorrelationDataProvider; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class CorrelationDataProviderConfiguration { + + @Bean + public CorrelationDataProvider messageOriginProvider() { + return new MessageOriginProvider("traceId", "correlationId"); + } + } + """, + spec -> spec.path("src/main/java/com/example/CorrelationDataProviderConfiguration.java") + ) + ); + } + + @Test + void addsBeanToExistingConfigClassWhenBeanAbsent() { + // when: CorrelationDataProviderConfiguration exists but lacks the @Bean method + // then: the @Bean method is injected by JavaTemplate (no blank line after `package` + // because the original file didn't have one; no blank line inside {} because the + // original body was empty). + rewriteRun( + java( + """ + package com.example; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + class Application {} + """ + ), + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class SomeHandler { + private MessageOriginProvider provider = new MessageOriginProvider(); + } + """ + ), + java( + """ + package com.example; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class CorrelationDataProviderConfiguration { + } + """, + """ + package com.example; + import org.axonframework.messaging.core.correlation.CorrelationDataProvider; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class CorrelationDataProviderConfiguration { + @Bean + public CorrelationDataProvider messageOriginProvider() { + return new MessageOriginProvider("traceId", "correlationId"); + } + } + """ + ) + ); + } + + @Test + void leavesExistingBeanMethodUntouched() { + // Idempotency: if the @Bean already exists, re-running should produce no change. + rewriteRun( + java( + """ + package com.example; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + class Application {} + """ + ), + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class SomeHandler { + private MessageOriginProvider provider = new MessageOriginProvider(); + } + """ + ), + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.CorrelationDataProvider; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class CorrelationDataProviderConfiguration { + + @Bean + public CorrelationDataProvider messageOriginProvider() { + return new MessageOriginProvider("traceId", "correlationId"); + } + } + """ + ) + ); + } + + @Test + void doesNotTriggerOnExplicitConstructorArgs() { + // Developer used custom key names intentionally — recipe must NOT override them. + rewriteRun( + java( + """ + package com.example; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + class Application {} + """ + ), + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class SomeHandler { + private MessageOriginProvider provider = + new MessageOriginProvider("myCorrelation", "myCausation"); + } + """ + ) + // no generated file expected — explicit args prevent the trigger + ); + } + + @Test + void doesNotOverrideBeanWithExplicitArgs() { + // Developer already set custom key names in the @Bean — do not touch. + // The scanner detects explicit args in the return statement and sets the + // "do not override" flag so neither the visitor nor the generator runs. + rewriteRun( + java( + """ + package com.example; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + class Application {} + """ + ), + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class SomeHandler { + private MessageOriginProvider provider = new MessageOriginProvider(); + } + """ + ), + // config class exists with custom args in the @Bean → must remain unchanged + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.CorrelationDataProvider; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class CorrelationDataProviderConfiguration { + + @Bean + public CorrelationDataProvider messageOriginProvider() { + return new MessageOriginProvider("myCustomCorrelation", "myCustomCausation"); + } + } + """ + ) + ); + } + + @Test + void doesNothingWhenNoMessageOriginProviderUsage() { + // No MessageOriginProvider in the project → no config class generated. + rewriteRun( + java( + """ + package com.example; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + class Application {} + """ + ) + ); + } + + @Test + void doesNothingWithoutSpringBootApplication() { + // Non-Spring-Boot project: no @SpringBootApplication → can't determine package, + // so no file is generated even when no-args MessageOriginProvider is used. + rewriteRun( + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class SomeHandler { + private MessageOriginProvider provider = new MessageOriginProvider(); + } + """ + ) + ); + } +} diff --git a/migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysTest.java b/migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysTest.java new file mode 100644 index 0000000000..97d21293b0 --- /dev/null +++ b/migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2010-2026. Axon Framework + * + * 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 org.axonframework.migration; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; + +import static org.openrewrite.java.Assertions.java; + +/** + * Verifies the {@link MigrateMessageOriginProviderDefaultKeys} recipe replaces no-args + * {@code new MessageOriginProvider()} with {@code new MessageOriginProvider("traceId", "correlationId")} + * to preserve AF4-compatible metadata key names. + *

+ * Key mapping (AF4 → AF5 rename, what this recipe preserves): + *

+ */ +class MigrateMessageOriginProviderDefaultKeysTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateMessageOriginProviderDefaultKeys()) + .typeValidationOptions(TypeValidation.none()); + } + + @Test + void replacesNoArgsConstructorWithAf4Keys() { + rewriteRun( + java( + """ + package com.example; + import org.axonframework.messaging.correlation.MessageOriginProvider; + + class MyConfiguration { + MessageOriginProvider provider = new MessageOriginProvider(); + } + """, + """ + package com.example; + import org.axonframework.messaging.correlation.MessageOriginProvider; + + class MyConfiguration { + MessageOriginProvider provider = new MessageOriginProvider("traceId", "correlationId"); + } + """ + ) + ); + } + + @Test + void replacesNoArgsConstructorOnAf5FqnType() { + rewriteRun( + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class MyConfiguration { + MessageOriginProvider provider = new MessageOriginProvider(); + } + """, + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class MyConfiguration { + MessageOriginProvider provider = new MessageOriginProvider("traceId", "correlationId"); + } + """ + ) + ); + } + + @Test + void leavesConstructorWithExplicitArgsUntouched() { + rewriteRun( + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class MyConfiguration { + MessageOriginProvider provider = new MessageOriginProvider("myCorrelation", "myCausation"); + } + """ + ) + ); + } + + @Test + void leavesAlreadyMigratedConstructorUntouched() { + // Idempotency: if the recipe already ran, re-running should produce no change. + rewriteRun( + java( + """ + package com.example; + import org.axonframework.messaging.core.correlation.MessageOriginProvider; + + class MyConfiguration { + MessageOriginProvider provider = new MessageOriginProvider("traceId", "correlationId"); + } + """ + ) + ); + } + + @Test + void leavesOtherClassConstructorsUntouched() { + rewriteRun( + java( + """ + package com.example; + + class MyConfiguration { + Object obj = new Object(); + } + """ + ) + ); + } + + @Test + void replacesInMethodBody() { + rewriteRun( + java( + """ + package com.example; + import org.axonframework.messaging.correlation.MessageOriginProvider; + import org.axonframework.messaging.correlation.CorrelationDataProvider; + + class Config { + CorrelationDataProvider provider() { + return new MessageOriginProvider(); + } + } + """, + """ + package com.example; + import org.axonframework.messaging.correlation.MessageOriginProvider; + import org.axonframework.messaging.correlation.CorrelationDataProvider; + + class Config { + CorrelationDataProvider provider() { + return new MessageOriginProvider("traceId", "correlationId"); + } + } + """ + ) + ); + } +} From f2eea0a37fe63ce6789c1e4f33f04b9c9ca8d603 Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Mon, 1 Jun 2026 13:31:19 +0200 Subject: [PATCH 2/2] fix(migration): apply MessageOriginProvider key migration to Kotlin sources MigrateMessageOriginProviderDefaultKeys overrode isAcceptable to gate on J.CompilationUnit, which OpenRewrite uses only for Java sources. Kotlin sources parse into K.CompilationUnit, so every .kt file was rejected before the visitor ran and no-args `MessageOriginProvider()` beans kept emitting the new AF5 metadata key names. The default JavaIsoVisitor.isAcceptable already accepts JavaSourceFile, the shared supertype of both J.CompilationUnit and K.CompilationUnit, so the override was unnecessary and actively excluded Kotlin. Removing it lets the recipe rewrite resolved Kotlin constructor calls (parsed as J.NewClass once the type is on the classpath, as in a real migration run) the same way as Java. Adds MigrateMessageOriginProviderDefaultKeysKotlinTest mirroring the Spring @Bean shape, covering both the no-args rewrite and the explicit-args no-op. --- ...grateMessageOriginProviderDefaultKeys.java | 6 - ...geOriginProviderDefaultKeysKotlinTest.java | 117 ++++++++++++++++++ 2 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysKotlinTest.java diff --git a/migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java b/migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java index 467deb3a19..fd050add38 100644 --- a/migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java +++ b/migration/src/main/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeys.java @@ -18,7 +18,6 @@ import org.openrewrite.ExecutionContext; import org.openrewrite.Recipe; -import org.openrewrite.SourceFile; import org.openrewrite.Tree; import org.openrewrite.TreeVisitor; import org.openrewrite.java.JavaIsoVisitor; @@ -91,11 +90,6 @@ public String getDescription() { public TreeVisitor getVisitor() { return new JavaIsoVisitor<>() { - @Override - public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { - return sourceFile instanceof J.CompilationUnit; - } - @Override public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) { J.NewClass nc = super.visitNewClass(newClass, ctx); diff --git a/migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysKotlinTest.java b/migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysKotlinTest.java new file mode 100644 index 0000000000..3f7512b5c0 --- /dev/null +++ b/migration/src/test/java/org/axonframework/migration/MigrateMessageOriginProviderDefaultKeysKotlinTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2010-2026. Axon Framework + * + * 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 org.axonframework.migration; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; + +import static org.openrewrite.kotlin.Assertions.kotlin; + +/** + * Validates {@link MigrateMessageOriginProviderDefaultKeys} on Kotlin sources, mirroring the + * real-world Spring {@code @Bean} shape from the Cinema sample app. + *

+ * A type stub for {@code MessageOriginProvider} is supplied so the Kotlin parser resolves the + * call as a constructor invocation ({@code J.NewClass}). This mirrors a real migration run, where + * the project's compile classpath provides the Axon types — without resolution Kotlin parses + * {@code MessageOriginProvider()} as an ambiguous {@code J.MethodInvocation} instead. + */ +class MigrateMessageOriginProviderDefaultKeysKotlinTest implements RewriteTest { + + private static final String MESSAGE_ORIGIN_PROVIDER_STUB = + """ + package org.axonframework.messaging.core.correlation + interface CorrelationDataProvider + class MessageOriginProvider() : CorrelationDataProvider { + constructor(correlationKey: String, causationKey: String) : this() + } + """; + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateMessageOriginProviderDefaultKeys()) + .typeValidationOptions(TypeValidation.none()); + } + + @Test + void replacesNoArgsConstructorInKotlinSpringBean() { + rewriteRun( + kotlin(MESSAGE_ORIGIN_PROVIDER_STUB), + kotlin( + """ + package com.example + import org.axonframework.messaging.core.correlation.CorrelationDataProvider + import org.axonframework.messaging.core.correlation.MessageOriginProvider + import org.springframework.context.annotation.Bean + import org.springframework.context.annotation.Configuration + + @Configuration + internal class AxonFrameworkConfiguration { + + @Bean + fun messageOriginProvider(): CorrelationDataProvider { + return MessageOriginProvider() + } + } + """, + """ + package com.example + import org.axonframework.messaging.core.correlation.CorrelationDataProvider + import org.axonframework.messaging.core.correlation.MessageOriginProvider + import org.springframework.context.annotation.Bean + import org.springframework.context.annotation.Configuration + + @Configuration + internal class AxonFrameworkConfiguration { + + @Bean + fun messageOriginProvider(): CorrelationDataProvider { + return MessageOriginProvider("traceId", "correlationId") + } + } + """ + ) + ); + } + + @Test + void leavesConstructorWithExplicitArgsUntouched() { + rewriteRun( + kotlin(MESSAGE_ORIGIN_PROVIDER_STUB), + kotlin( + """ + package com.example + import org.axonframework.messaging.core.correlation.CorrelationDataProvider + import org.axonframework.messaging.core.correlation.MessageOriginProvider + import org.springframework.context.annotation.Bean + import org.springframework.context.annotation.Configuration + + @Configuration + internal class AxonFrameworkConfiguration { + + @Bean + fun messageOriginProvider(): CorrelationDataProvider { + return MessageOriginProvider("myCorrelation", "myCausation") + } + } + """ + ) + ); + } +}