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
+ * In AF4, {@code MessageOriginProvider} put two keys into message metadata:
+ *
+ * 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, ExecutionContext> getVisitor() {
+ return new JavaIsoVisitor<>() {
+
+ @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
+ * 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/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")
+ }
+ }
+ """
+ )
+ );
+ }
+}
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):
+ *
+ *
+ * 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.
+ *
+ *
+ */
+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");
+ }
+ }
+ """
+ )
+ );
+ }
+}