diff --git a/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java new file mode 100644 index 000000000..5ba018dc8 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextProperty.java @@ -0,0 +1,123 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.openrewrite.java.spring.boot3; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.spring.AddSpringProperty; +import org.openrewrite.java.tree.J; +import org.openrewrite.properties.tree.Properties; +import org.openrewrite.yaml.tree.Yaml; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.openrewrite.Preconditions.and; + +public class MigrateHooksToReactorContextProperty extends ScanningRecipe { + @Override + public String getDisplayName() { + return "Use `spring.reactor.context-propagation` property"; + } + + @Override + public String getDescription() { + return "Replace `Hooks.enableAutomaticContextPropagation()` with `spring.reactor.context-propagation=true`."; + } + + @Override + public AtomicBoolean getInitialValue(ExecutionContext ctx) { + return new AtomicBoolean(false); + } + + private static final String SPRING_BOOT_APPLICATION_FQN = "org.springframework.boot.autoconfigure.SpringBootApplication"; + private static final String HOOKS_TYPE = "reactor.core.publisher.Hooks"; + private static final MethodMatcher HOOKS_MATCHER = new MethodMatcher("reactor.core.publisher.Hooks enableAutomaticContextPropagation()"); + + @Override + public TreeVisitor getScanner(AtomicBoolean foundHooksInSpringApp) { + return Preconditions.check( + and( + new UsesType<>(SPRING_BOOT_APPLICATION_FQN, true), + new UsesType<>(HOOKS_TYPE, true) + ), + new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + if (HOOKS_MATCHER.matches(mi)) { + foundHooksInSpringApp.set(true); + } + + return mi; + } + } + ); + } + + @Override + public TreeVisitor getVisitor(AtomicBoolean foundHooksInSpringApp) { + if (!foundHooksInSpringApp.get()) { + return TreeVisitor.noop(); + } + + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (tree instanceof J.CompilationUnit) { + return new HooksRemovalVisitor().visitNonNull(tree, ctx); + } + + if (isApplicationProperties(tree)) { + return addSpringProperty(ctx, tree, "spring.reactor.context-propagation", "true"); + } + + return tree; + } + }; + } + + private static boolean isApplicationProperties(@Nullable Tree tree) { + return (tree instanceof Properties.File && + "application.properties".equals(((Properties.File) tree).getSourcePath().getFileName().toString())) || + (tree instanceof Yaml.Documents && + ((Yaml.Documents) tree).getSourcePath().getFileName().toString().matches("application\\.ya*ml")); + } + + private static Tree addSpringProperty(ExecutionContext ctx, Tree properties, String property, String value) { + return new AddSpringProperty(property, value, null, null) + .getVisitor() + .visitNonNull(properties, ctx); + } + + private static class HooksRemovalVisitor extends JavaIsoVisitor { + @Override + public J.@Nullable MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + + // Only remove if we're in a @SpringBootApplication class + if (HOOKS_MATCHER.matches(mi)) { + maybeRemoveImport(HOOKS_TYPE); + return null; + } + + return mi; + } + } +} diff --git a/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java new file mode 100644 index 000000000..9b1916af5 --- /dev/null +++ b/src/testWithSpringBoot_3_2/java/org/openrewrite/java/spring/boot3/MigrateHooksToReactorContextPropertyTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.openrewrite.java.spring.boot3; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.properties.Assertions.properties; + +class MigrateHooksToReactorContextPropertyTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateHooksToReactorContextProperty()); + } + + @DocumentExample + @Test + void replaceMethodCallWithProperty() { + rewriteRun( + spec -> spec.recipe(new MigrateHooksToReactorContextProperty()), + java( + """ + package org.springframework.boot.autoconfigure; + public @interface SpringBootApplication {} + """ + ), + java( + """ + package org.springframework.boot; + public class SpringApplication { + public static void run(Class cls, String[] args) {} + } + """ + ), + java( + """ + package reactor.core.publisher; + public class Hooks { + public static void enableAutomaticContextPropagation() {} + } + """ + ), + java( + """ + import reactor.core.publisher.Hooks; + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + public class MyApplication { + public static void main(String[] args) { + Hooks.enableAutomaticContextPropagation(); + SpringApplication.run(MyApplication.class, args); + } + } + """, + """ + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + public class MyApplication { + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + } + """ + ), + properties( + "", + "spring.reactor.context-propagation=true", + spec -> spec.path("application.properties") + ) + ); + } +}