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)
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!