openrewrite / rewrite

Automated mass refactoring of source code.
https://docs.openrewrite.org
Apache License 2.0
2.23k stars 332 forks source link

[Recipe] AddComponentAnnotationForAllInjected #3361

Closed jdelobel closed 1 year ago

jdelobel commented 1 year ago

Sorry for the recipe's name. I have not found better :)

What problem are you trying to solve?

I have to migrate Jboss/JEE Java 8 application to Springboot 2.7.x. running (and compiling) on java 17 I have to create serevals recipes but for this issue i propose to add Spring @Component annotation on each class that uses @Inject or @Path in the code. I have no idea how to test this recipe with RewriteTest.

What precondition(s) should be checked before applying this recipe?

Describe the situation before applying the recipe

class A {
    void foo(String bar) {
         @Inject
         MyClass myClass

    }
}

class MyClass {

}

Describe the situation after applying the recipe

class A {
    void foo(String bar) {
         @Inject
         MyClass myClass

    }
}

@Component
class MyClass {

}

Additionnaly I would be very happy if you explain to me how to create a unit test for this recipe :)

Have you considered any alternatives or workarounds?

No

Any additional context

N/A

Are you interested in contributing this recipe to OpenRewrite?

Yes

Here is my recipe:

import org.openrewrite.ExecutionContext;
import org.openrewrite.ScanningRecipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.search.FindAnnotations;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.Statement;

import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;

public class AddComponentAnnotationForAllInjected extends ScanningRecipe<Set<JavaType>> {

    @Override
    public String getDisplayName() {
        return "Add `@Component` for all `@Inject` collaborators and all class annoted with @Path";
    }

    @Override
    public String getDescription() {
        return "Add `@Component` for all `@Inject` collaborators and all class annoted with @Path.";
    }

    private static final String at = "@";
    private static final String COMPONENT_FULLY_QUALIFIED_TYPE_NAME = "org.springframework.stereotype.Component";

    private static final String INJECT_FULLY_QUALIFIED_TYPE_NAME = "javax.inject.Inject";

    private static final String PATH_FULLY_QUALIFIED_TYPE_NAME = "javax.ws.rs.Path";

    @Override
    public Set<JavaType> getInitialValue(ExecutionContext ctx) {
        return new HashSet<>();
    }

    @Override
    public TreeVisitor<?, ExecutionContext> getScanner(Set<JavaType> injected) {
// Step 1: find all the injected fields

        return new JavaIsoVisitor<ExecutionContext>() {

            /**
             * Add all type Injected at Variable level into `injected` list
             */
            @Override
            public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations variableDecl, ExecutionContext ctx) {
                J.VariableDeclarations vd = super.visitVariableDeclarations(variableDecl, ctx);
                if (!FindAnnotations.find(variableDecl, at + INJECT_FULLY_QUALIFIED_TYPE_NAME).isEmpty()) {
                    injected.add(variableDecl.getType());
                }
                return vd;
            }

            /**
             * Add all type Injected in Contructor parameters `injected` list
             */
            @Override
            public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl, ExecutionContext ctx) {
                J.MethodDeclaration md = super.visitMethodDeclaration(methodDecl, ctx);
                if (md.isConstructor() && !FindAnnotations.find(md, at + INJECT_FULLY_QUALIFIED_TYPE_NAME).isEmpty()) {
                    for (Statement statement : md.getParameters()) {
                        if (statement instanceof J.VariableDeclarations) {
                            J.VariableDeclarations parameter = (J.VariableDeclarations) statement;
                            injected.add(parameter.getType());
                        }

                    }
                }
                return md;
            }
        };
    }

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor(Set<JavaType> injected) {
        // Step 2: use the found types to ensure @Component is on their declarations
        return new JavaIsoVisitor<ExecutionContext>() {
            @Override
            public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
                J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
                if (isComponentToAdd(cd, injected)) {
                    maybeAddImport(COMPONENT_FULLY_QUALIFIED_TYPE_NAME);
                    return JavaTemplate.builder("@" + getComponentAnnotation()).javaParser(
                                    JavaParser.fromJavaVersion().classpath("javax.inject")
                                            .dependsOn("package " + getComponentPackage() + "; public @interface " + getComponentAnnotation() + " {}")
                            )
                            .imports(COMPONENT_FULLY_QUALIFIED_TYPE_NAME)
                            .build().apply(getCursor(), classDecl.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)));

                }
                return cd;
            }
        };
    }

    /**
     * This method determine if the @Component annotation has to be added or not.
     *
     * @param classDecl the class to test
     * @param injected  list of all Injected classes, Interfaces
     * @return true if injected contains classDecl matches, false otherwise
     */
    private boolean isComponentToAdd(J.ClassDeclaration classDecl, Set<JavaType> injected) {

        boolean isClass = classDecl.getKind().name().equals(JavaType.FullyQualified.Kind.Class.toString());
        boolean isClassInjected = isClass && injected.contains(classDecl.getType());
        boolean isInterFaceInjected = isClass && isInterfaceInjected(classDecl, injected);

        return FindAnnotations.find(classDecl, at + COMPONENT_FULLY_QUALIFIED_TYPE_NAME).isEmpty()
                && (isClassInjected
                || isInterFaceInjected
                || !FindAnnotations.find(classDecl, at + PATH_FULLY_QUALIFIED_TYPE_NAME).isEmpty());
    }

    /**
     * This method determine if the @Component annotation has to be added or not for Implemented Interface.
     *
     * @param classDecl the class to test
     * @param injected  list of all Injected classes
     * @return true if injected contains an anImplemented Interface matches, false otherwise
     */
    private boolean isInterfaceInjected(J.ClassDeclaration classDecl, Set<JavaType> injected) {
        return classDecl.getImplements() != null
                && classDecl.getImplements().stream().anyMatch(anImplement -> injected.contains(anImplement.getType()));
    }

    /**
     * Get Component annotation
     *
     * @return
     */

    private static String getComponentAnnotation() {
        return COMPONENT_FULLY_QUALIFIED_TYPE_NAME.substring(COMPONENT_FULLY_QUALIFIED_TYPE_NAME.lastIndexOf('.') + 1);
    }

    /**
     * Get Component package
     *
     * @return
     */
    private static String getComponentPackage() {
        return COMPONENT_FULLY_QUALIFIED_TYPE_NAME.substring(0, COMPONENT_FULLY_QUALIFIED_TYPE_NAME.lastIndexOf('.'));
    }
timtebeek commented 1 year ago

Hi @jdelobel ! It sounds like what you're looking to do is very similar to what @m-brophy is working on in https://github.com/openrewrite/rewrite/pull/3337 and the replacement likely to come in rewrite-migrate-java, with the minor change that he's adding a different annotation. There was also a bit of back and forth on our Slack with some additional fixes to his tests that could help you see the recommended test pattern. You might want to reach out here or on our Slack to learn from each other too.

Separately there's also a collection of JEE to Spring migration recipes over at Spring Boot Migrator. That could be interesting for you as well, with potentially some work you can lift over. @fabapp2 would be to one to reach out to there.

Does that help get you past the initial hurdle in continuing your recipe development?

jdelobel commented 1 year ago

Hi @jdelobel ! It sounds like what you're looking to do is very similar to what @m-brophy is working on in #3337 and the replacement likely to come in rewrite-migrate-java, with the minor change that he's adding a different annotation. There was also a bit of back and forth on our Slack with some additional fixes to his tests that could help you see the recommended test pattern. You might want to reach out here or on our Slack to learn from each other too.

Separately there's also a collection of JEE to Spring migration recipes over at Spring Boot Migrator. That could be interesting for you as well, with potentially some work you can lift over. @fabapp2 would be to one to reach out to there.

Does that help get you past the initial hurdle in continuing your recipe development?

Hi @timtebeek ,

-Thanks a lot for all of your shared links!. I will check its works ASAP. @fabapp2 Do you intend to migrate to open-rewrite v8?

Unfortunately I'm on a company and the firewall blocks slack (and it's not very practical to rewrite the code snippets on the phone).

timtebeek commented 1 year ago

Sure! Since I don't want you missing out on unknown details in Slack, here's a quick copy of what was discussed: image

jdelobel commented 1 year ago

FYI, i did the test:

package com.myorg.spm.annotation.java.component;

import com.myorg.spm.java.annotation.component.AddComponentAnnotationForAllInjected;
import org.junit.jupiter.api.Test;
import org.openrewrite.java.JavaParser;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

public class AddComponentAnnotationForAllInjectedTest implements RewriteTest {

    @Override
    public void defaults(RecipeSpec spec) {
        spec.recipe(new AddComponentAnnotationForAllInjected()).parser(JavaParser.fromJavaVersion().classpath("javax.inject"));
    }

    @Test
    void addComponentWhenInjectAnnotationIsPresentAtVariableLevel() {
        rewriteRun(
                //language=java
                java("""
                            import javax.inject.*;
                            class Test {
                                @Inject
                                private MyComponent mc;
                            }
                        """),
                //language=java
                java("""
                            class MyComponent {}
                        """, """
                            import org.springframework.stereotype.Component;

                            @Component
                            class MyComponent {}
                        """));
    }

    @Test
    void addComponentWhenInjectAnnotationIsPresentAtConstructorLevel() {
        rewriteRun(
                //language=java
                java("""
                            import javax.inject.*;
                            class Test {

                                private MyComponent mc;

                                @Inject
                                public Test(private MyComponent mc){
                                this.mc = mc;
                                }
                            }
                        """),
                //language=java
                java("""
                            class MyComponent {}
                        """, """
                            import org.springframework.stereotype.Component;

                            @Component
                            class MyComponent {}
                        """));
    }

    @Test
    void addComponentOnCorrespondingClassesWhenInjectAnnotationIsPresentOnItsImplementedInterface() {
        rewriteRun(
                //language=java
                java("""
                        public interface TestInterface {}
                        """),

                //language=java
                java("""

                            import javax.inject.*;
                            class Test {

                                @Inject
                                private TestInterface mc;
                            }
                        """),

                //language=java
                java("""
                            class MyComponent implements TestInterface {}
                        """, """
                            import org.springframework.stereotype.Component;

                            @Component
                            class MyComponent implements TestInterface {}
                        """));
    }

    @Test
    void addComponentOnClassesThatImplementsDgtStartupListener() {
        rewriteRun(
                //language=java
                java("""
                            package com.myorg.fmk.base.deploiement;

                            public interface DgtStartupListener {}
                        """),

                //language=java
                java("""
                          import com.myorg.fmk.base.deploiement.DgtStartupListener;

                            class Test implements DgtStartupListener {}
                        """, """
                            import com.myorg.fmk.base.deploiement.DgtStartupListener;
                            import org.springframework.stereotype.Component;

                            @Component
                            class Test implements DgtStartupListener {}
                        """));
    }
}