Search code examples
javaopenrewrite

How to apply a recipe on a specific method parameter


In my recipe I select the method parameters which have both the NotNull and RequestParam annotations and I want to apply the OpenRewrite recipe AddOrUpdateAnnotationAttribute on these method parameters to set the required attribute to true of the RequestParam annotation.

I'm struggling how to apply a recipe on a specific piece of code, not on the complete Java class. Does somebody has an example?

An example of the source code before applying my recipe:

import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.constraints.NotNull;

class ControllerClass {
    public String sayHello (
        @NotNull @RequestParam(value = "name") String name,
        @RequestParam(value = "lang") String lang
    ) {
       return "Hello";
    }
}

The expected source code after applying my recipe:

import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.constraints.NotNull;

class ControllerClass {
    public String sayHello (
        @NotNull @RequestParam(required = true, value = "name") String name,
        @RequestParam(value = "lang") String lang
    ) {
       return "Hello";
    }
}

Only the first parameter name needs to be adopted as the second parameter has no NotNull annotation.

My (simplified) recipe:

public class MandatoryRequestParameter extends Recipe {

    @Override
    public @NotNull String getDisplayName() {
        return "Make RequestParam mandatory";
    }
    
    @Override
    protected @NotNull JavaIsoVisitor<ExecutionContext> getVisitor() {
        return new MandatoryRequestParameterVisitor();
    }
    
    public class MandatoryRequestParameterVisitor extends JavaIsoVisitor<ExecutionContext> {
        @Override
        public @NotNull J.MethodDeclaration visitMethodDeclaration(@NotNull J.MethodDeclaration methodDeclaration, @NotNull ExecutionContext executionContext) {
            J.MethodDeclaration methodDecl = super.visitMethodDeclaration(methodDeclaration, executionContext);
    
            return methodDeclaration.withParameters(ListUtils.map(methodDecl.getParameters(), (i, p) -> makeRequestParamMandatory(p, executionContext)));
        }
    
        private Statement makeRequestParamMandatory(Statement statement, ExecutionContext executionContext) {
            if (!(statement instanceof J.VariableDeclarations methodParameterDeclaration) || methodParameterDeclaration.getLeadingAnnotations().size() < 2) {
                return statement;
            }
    
            AddOrUpdateAnnotationAttribute addOrUpdateAnnotationAttribute = new AddOrUpdateAnnotationAttribute(
                    "org.springframework.web.bind.annotation.RequestParam", "required", "true", false
            );
    
            return (Statement) methodParameterDeclaration.acceptJava(addOrUpdateAnnotationAttribute.getVisitor(), executionContext);
        }
    }

}

When I execute my recipe, I got following error so my implementation is not the correct way of applying a recipe.

org.openrewrite.UncaughtVisitorException: java.lang.IllegalStateException: Expected to find a matching parent for Cursor{Annotation-\>root}
at org.openrewrite.TreeVisitor.visit(TreeVisitor.java:253)
at org.openrewrite.TreeVisitor.visit(TreeVisitor.java:145)
at org.openrewrite.java.JavaTemplate.withTemplate(JavaTemplate.java:520)
at org.openrewrite.java.JavaTemplate.withTemplate(JavaTemplate.java:42)
at org.openrewrite.java.tree.J.withTemplate(J.java:87)
at org.openrewrite.java.AddOrUpdateAnnotationAttribute$1.visitAnnotation(AddOrUpdateAnnotationAttribute.java:144)
at org.openrewrite.java.AddOrUpdateAnnotationAttribute$1.visitAnnotation(AddOrUpdateAnnotationAttribute.java:78)
at org.openrewrite.java.tree.J$Annotation.acceptJava(J.java:220)
at org.openrewrite.java.tree.J.accept(J.java:60)
at org.openrewrite.TreeVisitor.visit(TreeVisitor.java:206)
at org.openrewrite.TreeVisitor.visitAndCast(TreeVisitor.java:285)
at org.openrewrite.java.JavaVisitor.lambda$visitVariableDeclarations$23(JavaVisitor.java:873)
at org.openrewrite.internal.ListUtils.lambda$map$0(ListUtils.java:141)
at org.openrewrite.internal.ListUtils.map(ListUtils.java:123)
at org.openrewrite.internal.ListUtils.map(ListUtils.java:141)
at org.openrewrite.java.JavaVisitor.visitVariableDeclarations(JavaVisitor.java:873)
at org.openrewrite.java.JavaIsoVisitor.visitVariableDeclarations(JavaIsoVisitor.java:240)
at org.openrewrite.java.JavaIsoVisitor.visitVariableDeclarations(JavaIsoVisitor.java:31)
at org.openrewrite.java.tree.J$VariableDeclarations.acceptJava(J.java:5149)
at org.springframework.sbm.jee.jaxrs.recipes.MandatoryRequestParameter$MandatoryRequestParameterVisitor.makeRequestParamMandatory(MandatoryRequestParameter.java:45)
at org.springframework.sbm.jee.jaxrs.recipes.MandatoryRequestParameter$MandatoryRequestParameterVisitor.lambda$visitMethodDeclaration$0(MandatoryRequestParameter.java:33)

Solution

  • You can get the results you are looking for by using a declarative recipe. You can create a rewrite.yml file in the root of a project and use the rewrite's Maven or Gradle build plugin to apply that recipe.

        type: specs.openrewrite.org/v1beta/recipe
        name: org.example.MandatoryRequestParameter
        displayName: Make Spring `RequestParam` mandatory
        description: Add `required` attribute to `RequestParam` and set the value to `true`.
        recipeList:
          - org.openrewrite.java.AddOrUpdateAnnotationAttribute:
              annotationType: org.springframework.web.bind.annotation.RequestParam
              attributeName: required
              attributeValue: "true"
    

    If using Maven, you can activate that recipe by adding the plugin to your pom.xml:

    <plugin>
      <groupId>org.openrewrite.maven</groupId>
      <artifactId>rewrite-maven-plugin</artifactId>
      <version>4.38.0</version>
      <configuration>
        <activeRecipes>
          <recipe>org.example.MandatoryRequestParameter</recipe>
        </activeRecipes>
      </configuration>
    </plugin>
    

    See https://docs.openrewrite.org/getting-started/getting-started for more details on how to use the build plugins.

    However, if you want to restrict the change to only specific parameters, while still using the above recipe, you can write an imperative recipe.

    public class MandatoryRequestParameter extends Recipe {
    
        private static final String REQUEST_PARAM_FQ_NAME = "org.springframework.web.bind.annotation.RequestParam";
    
        @Override
        public @NotNull String getDisplayName() {
            return "Make Spring `RequestParam` mandatory";
        }
    
        @Override
        public String getDescription() {
            return "Add `required` attribute to `RequestParam` and set the value to `true`.";
        }
    
        @Override
        protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
            // This optimization means that your visitor will only run if the source file
            // has a reference to the annotation.
            return new UsesType<>(REQUEST_PARAM_FQ_NAME);
        }
    
        @Override
        protected @NotNull JavaVisitor<ExecutionContext> getVisitor() {
    
            JavaIsoVisitor addAttributeVisitor = new AddOrUpdateAnnotationAttribute(
                    REQUEST_PARAM_FQ_NAME, "required", "true", false
            ).getVisitor();
    
    
            return new JavaIsoVisitor<ExecutionContext>() {
                @Override
                public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
                    J.Annotation a = super.visitAnnotation(annotation, ctx);
    
                    if (!TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME)) {
                        return a;
                    }
    
                    // The visitor provides a cursor via the `getCusor()` method, and we can use that to navigate
                    // up to the parent. So, in this case, when we visit the annotation, we can navigate to the
                    // variable declaration upon which it is defined
                    J.VariableDeclarations variableDeclaration = getCursor().getParent().getValue();
    
                    // This is demonstrating two different ways we might restrict the change: 
                    // - If the parameter's type is a java.lang.Number. Note, this will change all parameters that are
                    //   subtypes of java.lang.Number
                    // - If the parameter name is equal to "fred"
                    JavaType paramType = variableDeclaration.getType();
                    if (TypeUtils.isAssignableTo("java.lang.Number", paramType) ||
                        variableDeclaration.getVariables().get(0).getSimpleName().equals("fred")) {
                        // If there is a match, we simple delegate to nested visitor.
                        return (J.Annotation) addAttributeVisitor.visit(a, ctx, getCursor());
                    }
                    return a;
                }
    
            };
        }
        }
    }
    

    Here is an example using OpenRewrite's test harness:

        @Test
        void requiredRequestParam() {
    
            rewriteRun(
                    spec -> spec
                            .recipe(new MandatoryRequestParameter())
                            .parser(JavaParser.fromJavaVersion().classpath("spring-web")),
                    java(
                            """
                              import org.springframework.web.bind.annotation.RequestParam;
                              
                              class ControllerClass {
                                public String sayHello (
                                  @RequestParam(value = "fred") String fred,
                                  @RequestParam(value = "lang") String lang,
                                  @RequestParam(value = "aNumber") Long aNumber
                                ) {
                                  return "Hello";
                                }
                              }
                            """,
                            """
                              import org.springframework.web.bind.annotation.RequestParam;
                              
                              class ControllerClass {
                                public String sayHello (
                                  @RequestParam(required = true, value = "fred") String fred,
                                  @RequestParam(value = "lang") String lang,
                                  @RequestParam(required = true, value = "aNumber") Long aNumber
                                ) {
                                  return "Hello";
                                }
                              }
                            """
                    )
            );
        }
    

    Hopefully, this helps!