Modifying Methods with JavaTemplate
This tutorial will demonstrate how the JavaTemplate can be used to manipulate and change a method declaration using rewrite's refactoring capabilities. You will build a recipe that modifies the setCustomerInfo() method in the following class:
1
package com.yourorg;
2
3
public abstract class Customer {
4
private Date dateOfBirth;
5
private String firstName;
6
private String lastName;
7
8
public abstract void setCustomerInfo(String lastName);
9
}
Copied!
The recipe and associated visitor will:
    Define a method body and remove the abstract modifier
    Add two additional parameters to the method declaration
    Add two additional statements to the method declaration's body
The resulting refactored class will look like this:
1
package com.yourorg;
2
3
import java.util.Date;
4
5
public abstract class Customer {
6
private Date dateOfBirth;
7
private String firstName;
8
private String lastName;
9
10
public void setCustomerInfo(Date dateOfBirth, String firstName, String lastName){
11
this.dateOfBirth = dateOfBirth;
12
this.firstName = firstName;
13
this.lastName = lastName;
14
}
15
}
Copied!
This guide assumes you've already set up your Recipe Development Environment.

Define a Recipe & Visitor

Create a new class that extends org.openrewrite.Recipe. This recipe will act as a wrapper around a visitor we will build to target the method com.yourorg.Customer.setCustomerInfo()and this recipe does not require any additional configuration properties. The visitor is defined as an inner class to the recipe because it is not intended to be used outside the context of the recipe.
1
package org.openrewrite.samples;
2
3
import org.openrewrite.ExecutionContext;
4
import org.openrewrite.Recipe;
5
import org.openrewrite.TreeVisitor;
6
import org.openrewrite.java.JavaIsoVisitor;
7
import org.openrewrite.java.MethodMatcher;
8
import org.openrewrite.java.tree.J;
9
import org.openrewrite.java.tree.J.MethodDeclaration;
10
11
public class ExpandCustomerInfo extends Recipe {
12
13
@Override
14
public String getDisplayName() {
15
return "Expand Customer Info";
16
}
17
18
@Override
19
public String getDescription() {
20
return "Expand the `CustomerInfo` class with new fields.";
21
}
22
23
// OpenRewrite provides a managed environment in which it discovers, instantiates, and wires configuration into Recipes.
24
// This recipe has no configuration and delegates to its visitor when it is run.
25
@Override
26
protected JavaIsoVisitor<ExecutionContext> getVisitor() {
27
return new ExpandCustomerInfoVisitor();
28
}
29
30
private class ExpandCustomerInfoVisitor extends JavaIsoVisitor<ExecutionContext> {
31
32
// Used to identify the method declaration that will be refactored
33
private final MethodMatcher methodMatcher = new MethodMatcher("com.yourorg.Customer setCustomerInfo(String)");
34
35
@Override
36
public MethodDeclaration visitMethodDeclaration(MethodDeclaration m, ExecutionContext c) {
37
if (!methodMatcher.matches(m.getType())) {
38
return m;
39
}
40
//TODO - Implement Refactoring operations on the matching "setCustomerInfo()" method declaration.
41
42
return m;
43
}
44
}
45
}
Copied!

Add a Method Body to setCustomerInfo()

Within the ExpandCustomerInfoVisitor, we will add logic to remove the abstract modifier from the method and use the JavaTemplate to add a method body to declaration. This is accomplished by defining a JavaTemplate that represents the method body as an instance variable within the ExpandCustomerInfoVisitor:
1
private class ExpandCustomerInfoVisitor extends JavaIsoVisitor<ExecutionContext> {
2
...
3
// Template used to add a method body to "setCustomerInfo()" method declaration.
4
private final JavaTemplate addMethodBodyTemplate = JavaTemplate.builder(this::getCursor, "")
5
.build();
6
...
7
}
Copied!
When using a template to replace a method's body, the template must include the open and closing curly braces.
The method is then replaced with a copy that removes the abstract modifier and the template is then used within the visitMethodDeclaration() method to replace the method body:
1
public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, P p) {
2
...
3
// Remove the abstract modifier from the method.
4
m = m.withModifiers(m.getModifiers().stream()
5
.filter(mod -> mod.getType() != Type.Abstract)
6
.collect(Collectors.toList()));
7
8
// Add a method body
9
m = m.withTemplate(addMethodBodyTemplate, m.getCoordinates().replaceBody());
10
11
return m;
12
}
Copied!

Add Parameters to setCustomerInfo()

Next, we will use JavaTemplate to add two additional parameters to the method declaration. Here we use an interpolation signifier #{} to leave a space for the existing argument so that it is preserved when we replace the method's argument list.
1
private class ExpandCustomerInfoVisitor extends JavaIsoVisitor<ExecutionContext> {
2
...
3
// Template used to insert two additional parameters into the "setCustomerInfo()" method declaration.
4
private final JavaTemplate addMethodParametersTemplate = JavaTemplate.builder(this::getCursor, "Date dateOfBirth, String firstName, #{}")
5
.imports("java.util.Date")
6
.build();
7
...
8
}
Copied!
NOTE: Because a new type, java.util.Date, is being introduced to the template, the type must be added when building the template. This ensures that any generated elements will have the correct type attribution.
The template is used to replace the method declaration using withTemplate(). Our code must also ensure that java.util.Date is added as an import to the compilation unit.
1
public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, P p) {
2
...
3
// Add two parameters to the method declaration by inserting them in from of the first argument.
4
m = m.withTemplate(addMethodParametersTemplate,
5
m.getCoordinates().replaceParameters(),
6
m.getParameters().get(0));
7
8
// Add an import for java.util.Date to this compilation unit's list of imports.
9
maybeAddImport("java.util.Date");
10
11
return m;
12
}
Copied!

Add Additional Statements to setCustomerInfo()

We'll use another template to add the assignment statements to the method body.
1
private class ExpandCustomerInfoVisitor extends JavaIsoVisitor<ExecutionContext> {
2
...
3
// Template used to add initializing statements to the method body
4
private final JavaTemplate addStatementsTemplate = JavaTemplate.builder(this::getCursor,
5
"this.dateOfBirth = dateOfBirth;\n" +
6
"this.firstName = firstName;\n" +
7
"this.lastName = lastName;\n")
8
.build();
9
...
10
}
Copied!
And this template is used to insert those statements into the method declaration's body:
1
public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, P p) {
2
...
3
// Safe to assert since we just added a body to the method
4
assert m.getBody() != null;
5
6
// Add the assignment statements to the method body
7
m = m.withTemplate(addStatementsTemplate, m.getBody().getCoordinates().lastStatement());
8
9
return m;
10
}
Copied!

Testing The Recipe

After following the steps above, the final recipe will look like the following:
1
package org.openrewrite.samples;
2
3
import java.util.stream.Collectors;
4
5
import org.openrewrite.ExecutionContext;
6
import org.openrewrite.Recipe;
7
import org.openrewrite.java.JavaIsoVisitor;
8
import org.openrewrite.java.JavaTemplate;
9
import org.openrewrite.java.MethodMatcher;
10
import org.openrewrite.java.tree.J;
11
import org.openrewrite.java.tree.J.MethodDeclaration;
12
import org.openrewrite.java.tree.J.Modifier.Type;
13
14
public class ExpandCustomerInfo extends Recipe {
15
@Override
16
public String getDisplayName() {
17
return "Expand Customer Info";
18
}
19
20
@Override
21
public String getDescription() {
22
return "Expand the `CustomerInfo` class with new fields.";
23
}
24
25
// OpenRewrite provides a managed environment in which it discovers, instantiates, and wires configuration into Recipes.
26
// This recipe has no configuration and when it is executed, it will delegate to its visitor.
27
@Override
28
protected ExpandCustomerInfoVisitor getVisitor() {
29
return new ExpandCustomerInfoVisitor();
30
}
31
32
private class ExpandCustomerInfoVisitor extends JavaIsoVisitor<ExecutionContext> {
33
34
// Used to identify the method declaration that will be refactored
35
private final MethodMatcher methodMatcher = new MethodMatcher("com.yourorg.Customer setCustomerInfo(String)");
36
37
// Template used to add a method body to "setCustomerInfo()" method declaration.
38
private final JavaTemplate addMethodBodyTemplate = JavaTemplate.builder(this::getCursor, "")
39
.build();
40
41
// Template used to insert two additional parameters into the "setCustomerInfo()" method declaration.
42
private final JavaTemplate addMethodParametersTemplate = JavaTemplate.builder(this::getCursor, "Date dateOfBirth, String firstName, #{}")
43
.imports("java.util.Date")
44
.build();
45
46
// Template used to add initializing statements to the method body
47
private final JavaTemplate addStatementsTemplate = JavaTemplate.builder(this::getCursor,
48
"this.dateOfBirth = dateOfBirth;\n" +
49
"this.firstName = firstName;\n" +
50
"this.lastName = lastName;\n")
51
.build();
52
53
@Override
54
public MethodDeclaration visitMethodDeclaration(MethodDeclaration m, ExecutionContext c) {
55
if (!methodMatcher.matches(m.getType())) {
56
return m;
57
}
58
// Remove the abstract modifier from the method.
59
m = m.withModifiers(m.getModifiers().stream()
60
.filter(mod -> mod.getType() != Type.Abstract)
61
.collect(Collectors.toList()));
62
63
// Add a method body
64
m = m.withTemplate(addMethodBodyTemplate, m.getCoordinates().replaceBody());
65
66
// Add two parameters to the method declaration by inserting them in from of the first argument.
67
m = m.withTemplate(addMethodParametersTemplate,
68
m.getCoordinates().replaceParameters(),
69
m.getParameters().get(0));
70
71
// Add an import for java.util.Date to this compilation unit's list of imports.
72
maybeAddImport("java.util.Date");
73
74
// Safe to assert since we just added a body to the method
75
assert m.getBody() != null;
76
77
// Add the assignment statements to the method body
78
m = m.withTemplate(addStatementsTemplate, m.getBody().getCoordinates().lastStatement());
79
80
return m;
81
}
82
}
83
}
Copied!
OpenRewrite provides testing infrastructure for recipes via the rewrite-test module. To create automated tests of this recipe we use the Kotlin language, mostly for convenient access to multi-line Strings, with JUnit 5. To assert that the recipe makes the expected changes to our test class, we will create a new test class that implements the JavaRecipeTest interface and useassertChanged ensure the recipe is making the expected changes to the test class.
1
package org.openrewrite.samples;
2
3
import org.openrewrite.java.JavaParser
4
import org.junit.jupiter.api.Test
5
import org.openrewrite.java.JavaRecipeTest
6
7
class ExpandCustomerInfoTest : JavaRecipeTest {
8
9
override val recipe = ExpandCustomerInfo()
10
11
@Test
12
fun testExapandCustomerInfo() = assertChanged(
13
before = """
14
package com.yourorg;
15
16
public abstract class Customer {
17
private Date dateOfBirth;
18
private String firstName;
19
private String lastName;
20
21
public abstract void setCustomerInfo(String lastName);
22
}
23
""",
24
after = """
25
package com.yourorg;
26
27
import java.util.Date;
28
29
public abstract class Customer {
30
private Date dateOfBirth;
31
private String firstName;
32
private String lastName;
33
34
public void setCustomerInfo(Date dateOfBirth, String firstName, String lastName){
35
this.dateOfBirth = dateOfBirth;
36
this.firstName = firstName;
37
this.lastName = lastName;
38
}
39
}
40
"""
41
)
42
}
Copied!
Last modified 2mo ago