Learning to Write Our Own Recipes
We need to learn to write our own recipes, because as we’ve seen, despite there being many public ones, sometimes they don’t fit our needs, or we simply want to perform some specific tasks for our project.
As we’ve seen before, recipes are distributed as Maven artifacts.
To create our recipes, we need to create a Maven project and import the OpenRewrite core, extend its classes and write our executions.
OpenRewrite offers us a “starter” with all the necessary dependencies to start writing our recipes: https://docs.openrewrite.org/authoring-recipes/recipe-development-environment
From there we have the option with Gradle, but let’s see how to do it with Maven.
We create a Maven project:
mvn -B archetype:generate -DgroupId=com.openrewrite.demo.recipe -DartifactId=dppware-recipe -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.openrewrite.demo.recipe</groupId>
<artifactId>dppware-recipe</artifactId>
<version>1.0-SNAPSHOT</version>
<name>dppware-recipe</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.testSource>17</maven.compiler.testSource>
<maven.compiler.testTarget>17</maven.compiler.testTarget>
<lombok.version>1.18.28</lombok.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-recipe-bom</artifactId>
<version>2.0.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- rewrite-java dependencies only necessary for Java Recipe development -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java-17</artifactId>
<scope>runtime</scope>
</dependency>
<!-- rewrite-maven dependency only necessary for Maven Recipe development -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-maven</artifactId>
<scope>compile</scope>
</dependency>
<!-- rewrite-yaml dependency only necessary for Yaml Recipe development -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-yaml</artifactId>
<scope>compile</scope>
</dependency>
<!-- rewrite-properties dependency only necessary for Properties Recipe development -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-properties</artifactId>
<scope>compile</scope>
</dependency>
<!-- rewrite-xml dependency only necessary for XML Recipe development -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-xml</artifactId>
<scope>compile</scope>
</dependency>
<!-- lombok is optional, but recommended for authoring recipes -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!-- For authoring tests for any kind of Recipe -->
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M9</version>
</plugin>
</plugins>
</build>
</project>
We compile to verify that everything works correctly and start writing our first recipe:
Writing Recipes
A recipe is a Java class that extends the abstract class org.openrewrite.Recipe. We create a class called FirstRecipe, extend Recipe and implement 3 basic methods:
package com.openrewrite.demo.recipe;
import org.openrewrite.Recipe;
public class FirstRecipe extends Recipe{
@Override
public String getDescription() {
// TODO Auto-generated method stub
return null;
}
@Override
public String getDisplayName() {
// TODO Auto-generated method stub
return null;
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// TODO Auto-generated method stub
return super.getVisitor();
}
}
getVisitor is the heart of a recipe’s execution: As we saw earlier, the LST tree is formed by nodes, these nodes can be of type method, variableDeclaration, Class, CompilationUnit… On the official website we have all the elements: [https://docs.openrewrite.org/concepts-explanations/lst-examples] (https://docs.openrewrite.org/concepts-explanations/lst-examples)

The visitor pattern will visit all the nodes that are constructed in the LST tree and for each one will apply the executions we define in our visitor.
Let’s create our own visitor instead of returning super.getVisitor():
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// TODO Auto-generated method stub
return new JavaVisitor<ExecutionContext>() {
};
}
Before writing the implementation, we need to know what type of LST tree element we want to modify (i.e., if we want to change a variable, a method definition, a class because we’re going to add a new variable, etc…)
In case we want to modify Java elements, we’ll use JavaVisitor. So we see that JavaVisitor offers us several methods that we can implement simply to manipulate Java elements:
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// TODO Auto-generated method stub
return new JavaVisitor<ExecutionContext>() {
@Override
public J visitMethodDeclaration(MethodDeclaration method, ExecutionContext p) {
// TODO Auto-generated method stub
return super.visitMethodDeclaration(method, p);
}
@Override
public J visitMethodInvocation(MethodInvocation method, ExecutionContext p) {
// TODO Auto-generated method stub
return super.visitMethodInvocation(method, p);
}
@Override
public J visitVariableDeclarations(VariableDeclarations multiVariable, ExecutionContext p) {
// TODO Auto-generated method stub
return super.visitVariableDeclarations(multiVariable, p);
}
@Override
public J visitClassDeclaration(ClassDeclaration classDecl, ExecutionContext p) {
// TODO Auto-generated method stub
return super.visitClassDeclaration(classDecl, p);
}
@Override
public J visitImport(Import import_, ExecutionContext p) {
// TODO Auto-generated method stub
return super.visitImport(import_, p);
}
...etc..
};
USING OUR RECIPES
Preparation
Let’s create a simple Maven project that will serve as a guinea pig for our recipes:
Let’s go to an empty directory and execute the Maven archetype to create a simple project:
cd /tmp
mvn -B archetype:generate -DgroupId=com.openrewrite.demo.testing -DartifactId=testing-recipe-project -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4
cd testing-recipe-project
RECIPE 1: AddCommentToClassDeclaration
We want to add comments to all classes in the project.
Our visitor is focused on visiting class declarations that exist in our project, so we must implement the visitClassDeclaration method of our JavaVisitor.
We create a recipe called AddCommentToClassDeclaration that will look like this:
package com.openrewrite.demo.recipe;
import java.util.Collections;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J.ClassDeclaration;
import org.openrewrite.java.tree.TextComment;
import org.openrewrite.marker.Markers;
public class AddCommentToClassDeclaration extends Recipe {
private final String COMMENT = "This is AddCommentToClassDeclaration";
@Override
public String getDescription() {
return "AddCommentToClassDeclaration";
}
@Override
public String getDisplayName() {
return "AddCommentToClassDeclaration";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddComment();
}
public class AddComment extends JavaIsoVisitor<ExecutionContext> {
@Override
public ClassDeclaration visitClassDeclaration(ClassDeclaration classDecl, ExecutionContext p) {
if (classDecl.getComments().isEmpty()) { //Idempotent execution
classDecl = classDecl.withComments(Collections.singletonList(
new TextComment(true, COMMENT, "\n", Markers.EMPTY)
));
}
return super.visitClassDeclaration(classDecl, p);
}
}
}
We compile our recipe module:
mvn clean install
[INFO] Installing /home/dpena/development/workspaces/dppware/gitlab/dppware-adhoc/back/pocs/openrewrite/dppware-recipe/pom.xml to /home/dpena/.m2/repository/com/dppware/recipe/dppware-recipe/1.0-SNAPSHOT/dppware-recipe-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
We go to the project and execute the openrewrite plugin using the coordinates of our artifact and the name of the recipe. It will look like this:
cd /tmp/testing-recipe-project
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.recipe.AddCommentToClassDeclaration
[INFO] --- rewrite-maven-plugin:5.3.1:run (default-cli) @ testing-recipe-project ---
[INFO] Using active recipe(s) [com.openrewrite.demo.recipe.AddCommentToClassDeclaration]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [testing-recipe-project] Resolving Poms...
[INFO] Project [testing-recipe-project] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to src/main/java/com/dppware/testing/Ship.java by:
[WARNING] com.openrewrite.demo.recipe.AddCommentToClassDeclaration
[WARNING] Please review and commit the results.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
We see there have been changes, let’s do the diff:
git diff
diff --git a/src/main/java/com/dppware/testing/Ship.java b/src/main/java/com/dppware/testing/Ship.java
index e8cfb8f..a321053 100644
--- a/src/main/java/com/dppware/testing/Ship.java
+++ b/src/main/java/com/dppware/testing/Ship.java
@@ -1,6 +1,7 @@
package com.openrewrite.demo.testing;
+/*This is AddCommentToClassDeclaration*/
public class Ship {
}
diff --git a/target/classes/com/dppware/testing/Ship.class b/target/classes/com/dppware/testing/Ship.class
RECIPE 2: AddCommentToCompilationUnit
We write another recipe whose Visitor target is the compilationUnit (it’s not the class, a compilation unit encompasses package definition, import dependencies, etc.). We’re going to add a dppware license to all classes. It will look like this:
package com.openrewrite.demo.recipe;
import java.util.Collections;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J.CompilationUnit;
import org.openrewrite.java.tree.TextComment;
import org.openrewrite.marker.Markers;
public class AddCommentToCompilationUnit extends Recipe {
private final String COMMENT = "Whoever has the guts to copy this code will be punished with 3 push-ups and 2 coffees from the hallway machine";
@Override
public String getDescription() {
return "AddCommentToCompilationUnit";
}
@Override
public String getDisplayName() {
return "AddCommentToCompilationUnit";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddComment();
}
public class AddComment extends JavaIsoVisitor<ExecutionContext> {
@Override
public CompilationUnit visitCompilationUnit(CompilationUnit cu, ExecutionContext p) {
if (cu.getComments().isEmpty()) { //Idempotent execution
cu = cu.withComments(Collections.singletonList(
new TextComment(true, COMMENT, "\n", Markers.EMPTY)
));
}
return super.visitCompilationUnit(cu, p);
}
}
}
We recompile to package our recipes and execute indicating the recipe name com.openrewrite.demo.recipe.AddCommentToCompilationUnit
We compile our recipe module:
mvn clean install
[INFO] Installing /home/dpena/development/workspaces/dppware/gitlab/dppware-adhoc/back/pocs/openrewrite/dppware-recipe/pom.xml to /home/dpena/.m2/repository/com/dppware/recipe/dppware-recipe/1.0-SNAPSHOT/dppware-recipe-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
cd /tmp/testing-recipe-project
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.recipe.AddCommentToCompilationUnit
[INFO] Using active recipe(s) [com.openrewrite.demo.recipe.AddCommentToCompilationUnit]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [testing-recipe-project] Resolving Poms...
[INFO] Project [testing-recipe-project] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to src/main/java/com/dppware/testing/Ship.java by:
[WARNING] com.openrewrite.demo.recipe.AddCommentToCompilationUnit
[WARNING] Changes have been made to src/main/java/com/dppware/testing/App.java by:
[WARNING] com.openrewrite.demo.recipe.AddCommentToCompilationUnit
[WARNING] Changes have been made to src/test/java/com/dppware/testing/AppTest.java by:
[WARNING] com.openrewrite.demo.recipe.AddCommentToCompilationUnit
[WARNING] Please review and commit the results.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.669 s
[INFO] Finished at: 2023-07-19T12:59:11+02:00
[INFO] ------------------------------------------------------------------------
We save:
RECIPE 3: AddCommentToClassVariable
Keep in mind that a variable declaration is not only at the class level, you can define a variable as a method signature, or also inside a method, or inside a loop, etc…
So this recipe requires a bit more work with the LST handling API.
The recipe will look like this:
package com.openrewrite.demo.recipe;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.TextComment;
import org.openrewrite.marker.Markers;
public class AddCommentToClassVariable extends Recipe {
private final String COMMENT = "This is AddCommentToVariable";
@Override
public String getDescription() {
return "AddCommentToClassVariable";
}
@Override
public String getDisplayName() {
return "AddCommentToClassVariable";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddComment();
}
public class AddComment extends JavaIsoVisitor<ExecutionContext> {
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext executionContext) {
multiVariable = super.visitVariableDeclarations(multiVariable, executionContext);
if(isSingleVarDeclaration(multiVariable) && isNotMethodArgumentDeclarationVar(getCursor())){
if (multiVariable.getComments().size() == 0) { //Idempotent
multiVariable = multiVariable.withComments(ListUtils.concat(
multiVariable.getComments(),
new TextComment(false, "comment", "\n"+multiVariable.getPrefix().getIndent(), Markers.EMPTY)
));
}
}
return multiVariable;
}
/**
* Does not affect to multiple same type declaration: String variableA,variableB
*/
private boolean isSingleVarDeclaration(J.VariableDeclarations multiVariable) {
return 1 == multiVariable.getVariables().size();
}
/**
* Returns true if is a
* @param nv
* @return
*/
private boolean isNotMethodArgumentDeclarationVar(Cursor cursor) {
return cursor.getParent().firstEnclosing(J.MethodDeclaration.class) == null;
}
}
}
we compile our recipe artifact
```sh
mvn clean install
Since it adds a comment to a variable propertyA, let’s add a variable to the Ship.java class of the project where we’re performing the executions
/*Whoever has the guts to copy this code will be punished with 3 push-ups and 2 coffees from the hallway machine*/
package com.openrewrite.demo.testing;
/*This is AddCommentToClassDeclaration*/
public class Ship {
private String propertyA;
}
cd /tmp/testing-recipe-project
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.recipe.AddCommentToClassVariable
[INFO] Running recipe(s)...
[WARNING] Changes have been made to src/main/java/com/dppware/testing/Ship.java by:
[WARNING] com.openrewrite.demo.recipe.AddCommentToClassVariable
[WARNING] Please review and commit the results.
RECIPE 4: AddClassVariable
Let’s define a new variable in a class. We must use the ClassDeclaration Visitor, since it’s at the class level where we’re going to add the variable.
Our recipe will look like this:
package com.openrewrite.demo.recipe;
import static java.util.Collections.emptyList;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Statement;
public class AddClassVariable extends Recipe {
private String variableAnnotations = "@Deprecated";
private String variableModifiers = "public final";
private String variableRawStatement = "Random variableA";
private String variableInitialization = " new Random()";
@Override
public String getDescription() {
return "Add Class Variable";
}
@Override
public String getDisplayName() {
return "Add Class Variable";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddClassRandomVariableDeclaration();
}
public class AddClassRandomVariableDeclaration extends JavaIsoVisitor<ExecutionContext> {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext p) {
J.ClassDeclaration c = classDecl;
if(!classVarAlreadyExists(c, variableRawStatement)) { //Idempotent
JavaTemplate varDeclarationTemplate = JavaTemplate
.builder("#{} \n #{} #{} = #{};")
.contextSensitive()
.build();
Statement addVariableStatement = maybeAutoFormat(c, c.withBody(
varDeclarationTemplate.apply(
new Cursor(getCursor(), c.getBody().withStatements(emptyList())),
c.getBody().getCoordinates().lastStatement(),
variableAnnotations,
variableModifiers ,
variableRawStatement,
variableInitialization
)), p).getBody().getStatements().get(0);
c = c.withBody(c.getBody().withStatements(ListUtils.concat(c.getBody().getStatements(), addVariableStatement)));
maybeAddImport("java.util.Random",null, false); //OnlyIfReferenced=false, because at this point is not evaluated that the new var exists, so is not referenced already
}
return c;
}
/**
* Returns true if exist a class Var Declaration that name match
* @param classDecl
* @return
*/
private boolean classVarAlreadyExists(J.ClassDeclaration classDecl, String typeAndNameVar) {
return classDecl.getBody().getStatements().stream()
.filter(statement -> statement instanceof J.VariableDeclarations)
.map(J.VariableDeclarations.class::cast)
.anyMatch(varDeclaration -> varDeclaration.toString().contains(typeAndNameVar));
}
}
}
We see that for complex things we can use JavaTemplate. This utility allows us to write Java blocks more easily as if they were templates. In this example, we’re also going to create a variable of type Random, so we’ll need to add that import.
we compile our recipe collection
and execute in the project:cd /tmp/testing-recipe-project
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.recipe.AddClassVariable
[INFO] Using active recipe(s) [com.openrewrite.demo.recipe.AddClassVariable]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [testing-recipe-project] Resolving Poms...
[INFO] Project [testing-recipe-project] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to src/main/java/com/dppware/testing/Ship.java by:
[WARNING] com.openrewrite.demo.recipe.AddClassVariable
[WARNING] Changes have been made to src/main/java/com/dppware/testing/App.java by:
[WARNING] com.openrewrite.demo.recipe.AddClassVariable
[WARNING] Changes have been made to src/test/java/com/dppware/testing/AppTest.java by:
[WARNING] com.openrewrite.demo.recipe.AddClassVariable
[WARNING] Please review and commit the results.
```
As we see, it has added the variable declaration to all classes in the project. In many cases we want to establish a condition for the execution of our visitor.
# RECIPE 4: AddClassVariable with PRECONDITIONS
Preconditions are Java classes that filter before the execution of the visitor.
Let's undo the changes executed previously and we're going to create the variable only if the class extends from a certain parent class.
```sh
git stash
public class Vehicle {
}
```java
/*Whoever has the guts to copy this code will be punished with 3 push-ups and 2 coffees from the hallway machine*/
package com.openrewrite.demo.testing;
/*This is AddCommentToClassDeclaration*/
public class Ship extends Vehicle{
//comment
private String propertyA;
}
We create the Precondition in a generic way, that is, in the constructor we can indicate which class we expect it to extend for the condition to be met
package com.openrewrite.demo.recipe;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J.ClassDeclaration;
import org.openrewrite.marker.SearchResult;
import lombok.EqualsAndHashCode;
import lombok.Value;
@Value
@EqualsAndHashCode(callSuper = false)
public class PreconditionClassExtends<P> extends JavaIsoVisitor<P> {
/**
* Target extends className
*/
String extendsClass ;
public PreconditionClassExtends(String extendsClass) {
this.extendsClass = extendsClass;
}
/**
* We only declare the visitor where we expect to find the condition
*/
@Override
public ClassDeclaration visitClassDeclaration(ClassDeclaration classDecl, P p) {
if(classDecl.getExtends()!=null && classDecl.getExtends().getType().toString().equals(extendsClass)) {
return SearchResult.found(classDecl);
}
return super.visitClassDeclaration(classDecl, p);
}
}
and now we modify our recipe, changing the getVisitor method so that if the condition that it extends from Vehicle is met, it returns the execution of the visitor.
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.or(
new PreconditionClassExtends<>("com.openrewrite.demo.testing.Vehicle")),
new AddClassRandomVariableDeclaration());
}
Let’s compile our recipe collection
and execute it on the project:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.recipe.AddClassVariable
We see that only the Ship class has been affected
# RECIPE 5: AddClassMethod with PRECONDITIONS
We’re going to create a method using a precondition of whether the method doesn’t exist in the class. That is, if the class already has it, it won’t add it.
We prepare the project code by adding a method called mayusculas to the Vehicle.java class
We prepare a precondition that can be reused and where we can specify the method name:
package com.openrewrite.demo.recipe;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J.ClassDeclaration;
import org.openrewrite.java.tree.J.MethodDeclaration;
import org.openrewrite.marker.SearchResult;
public class PreconditionHasMethodWithName<P> extends JavaIsoVisitor<P> {
/**
* Target extends className
*/
String methodName ;
public PreconditionHasMethodWithName(String methodName) {
this.methodName = methodName;
}
/**
* We only declare the visitor where we expect to find the condition
*/
public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, P p) {
if(method.getSimpleName().equals(methodName)) {
return SearchResult.found(method);
}
return super.visitMethodDeclaration(method, p);
};
}
We’ll ask if the class declares a method called mayusculas and if so, we’ll add another method that will use it.
And the recipe looks like this:
package com.openrewrite.demo.recipe;
import static java.util.Collections.emptyList;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Statement;
public class AddClassMethod extends Recipe {
private String methodName = "concatenateWithMe";
private final String methodTemplate = "public String #{}(#{} , #{}) {return this.mayusculas(#{} + #{});}";
@Override
public String getDescription() {
return "Add Class Method";
}
@Override
public String getDisplayName() {
return "Add Class Method";
}
/**
* Main execution with Matchers Preconditions
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.or(
new PreconditionHasMethodWithName<>("mayusculas")),
new AddClassMethodVisitor());
}
public class AddClassMethodVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext p) {
J.ClassDeclaration c = classDecl;
if(!classMethodAlreadyExists(c, methodName)) { //Idempotent
JavaTemplate varDeclarationTemplate = JavaTemplate
.builder(methodTemplate)
.contextSensitive()
.build();
Statement addVariableStatement = maybeAutoFormat(c, c.withBody(
varDeclarationTemplate.apply(
new Cursor(getCursor(), c.getBody().withStatements(emptyList())),
c.getBody().getCoordinates().lastStatement(),
methodName,
"String arg1" ,
"String arg2",
"arg1",
"arg2"
)), p).getBody().getStatements().get(0);
c = c.withBody(c.getBody().withStatements(ListUtils.concat(c.getBody().getStatements(), addVariableStatement)));
}
return c;
}
/**
* Returns true if exist a class Var method that name match
* @param classDecl
* @return
*/
private boolean classMethodAlreadyExists(J.ClassDeclaration classDecl, String typeAndNameMethod) {
return classDecl.getBody().getStatements().stream()
.filter(statement -> statement instanceof J.MethodDeclaration)
.map(J.MethodDeclaration.class::cast)
.anyMatch(varDeclaration -> varDeclaration.toString().contains(typeAndNameMethod));
}
}
}
cd /tmp/testing-recipe-project
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.recipe.AddClassMethod
[WARNING] Changes have been made to src/main/java/com/dppware/testing/Vehicle.java by:
[WARNING] com.openrewrite.demo.recipe.AddClassMethod
[WARNING] Please review and commit the results.
```
We see that it has added the method to the Vehicle class which is the one that declares the method.
```diff
public class Vehicle {
protected String mayusculas(String key){
return key.toUpperCase();
}
+ public String concatenateWithMe(String arg1, String arg2) {
+ return this.mayusculas(arg1 + arg2);
+ }
}
```
# RECIPE 6: Add an execution line inside a method
We're going to look for a method that if it exists, we'll change the first line to be an execution we want. In this case, a new of a Ship object.
For simplicity, we're going to use the PreconditionHasMethodWithName precondition.
If the class has a method named main, we're going to modify the content by adding a statement to the execution that instantiates a Ship.
Our recipe looks like this **AddStatementToMethod**:
```java
package com.openrewrite.demo.recipe;
import static java.util.Collections.emptyList;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Statement;
import org.openrewrite.java.tree.J.MethodDeclaration;
public class AddStatementToMethod extends Recipe {
private final String statementTemplate = "Ship ship = new Ship()";
@Override
public String getDescription() {
return "AddStatementToMethod";
}
@Override
public String getDisplayName() {
return "AddStatementToMethod";
}
/**
* Main execution with Matchers Preconditions
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.or(
new PreconditionHasMethodWithName<>("main")),
new AddStatementToMethodVisitor());
}
public class AddStatementToMethodVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, ExecutionContext p) {
J.MethodDeclaration m = super.visitMethodDeclaration(method, p);
if(!invocationStatementExist(m, statementTemplate)) { //Idempotent
JavaTemplate invocationStatementTemplate = JavaTemplate
.builder(statementTemplate)
.contextSensitive()
.build();
Statement addMethodInvocationStatement = maybeAutoFormat(m, m.withBody(
invocationStatementTemplate.apply(
new Cursor(getCursor(), m.getBody().withStatements(emptyList())),
m.getBody().getCoordinates().lastStatement()
)), p).getBody().getStatements().get(0);
m = m.withBody(m.getBody().withStatements(ListUtils.concat(m.getBody().getStatements(), addMethodInvocationStatement)));
maybeAddImport("com.openrewrite.demo.testing.Ship",null, false); //OnlyIfReferenced=false, because at this point is not evaluated that the new var exists, so is not referenced already
}
return m;
}
/**
* Returns true if exist the invocation
* @param classDecl
* @return
*/
private boolean invocationStatementExist(MethodDeclaration method, String invocation) {
return method.getBody().getStatements().stream()
.anyMatch(stm -> stm.toString().contains(invocation));
}
}
}
cd /tmp/testing-recipe-project
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.recipe.AddStatementToMethod
[INFO] Project [testing-recipe-project] Resolving Poms...
[INFO] Project [testing-recipe-project] Parsing source files
[INFO] Running recipe(s)...
[WARNING] Changes have been made to src/main/java/com/dppware/testing/App.java by:
[WARNING] com.openrewrite.demo.recipe.AddStatementToMethod
[WARNING] Please review and commit the results.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
```
We see the diff and verify that it found the main method and included the new Ship.
```diff
@@ -10,5 +10,6 @@ public class App
public static void main( String[] args )
{
System.out.println( "Hello World!" );
+ Ship ship = new Ship();
}
}
# RECIPE 7: Create a new parameterized constructor
This recipe will create a constructor with 2 parameters. These 2 parameters must be defined at the class level, so we can assign them in the constructor. So this recipe will be a bit more complex.
We’re going to change the Ship constructor to require 2 arguments. But if we do that, the new we’re doing in the App.java#main class will fail because it will need to provide those arguments.
This recipe requires 2 executions (recipes)
The first recipe AddConstructorParametrizedToClass will add the fields and create the constructor The second recipe UpdateConstructorInvocation will look for invocations to the Ship constructor and change them to add the new arguments we’re going to pass as arguments in the recipe
They will look like this:
package com.openrewrite.demo.recipe;
import static java.util.Collections.emptyList;
import org.openrewrite.Cursor;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.Statement;
public class AddConstructorParametrizedToClass extends Recipe {
private String arg1 = "private String argumentA";
private String argB = "private String argumentB";
private final String constructorTemplate = "public Ship (#{} , #{}) {this.#{} = #{}; \n this.#{} = #{};}";
@Override
public String getDescription() {
return "AddConstructorParametrizedToClass";
}
@Override
public String getDisplayName() {
return "AddConstructorParametrizedToClass";
}
/**
* Main execution with Matchers Preconditions
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddClassParametrizedConstructorVisitor();
}
public class AddClassParametrizedConstructorVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext p) {
J.ClassDeclaration c = classDecl;
if(classDecl.getName().getSimpleName().equals("Ship")) {
c = createPropertyA("private String argumentA", c, p);
c = createPropertyA("private String argumentB", c, p);
c = createConstructor(c,p);
}
return c;
}
private J.ClassDeclaration createPropertyA(String template, J.ClassDeclaration c, ExecutionContext p){
if(!classVarAlreadyExists(c, template)) {
JavaTemplate varATemplate = JavaTemplate.builder(template).contextSensitive().build();
Statement addVarAStatement = maybeAutoFormat(c,
c.withBody( varATemplate.apply(new Cursor(getCursor(), c.getBody().withStatements(emptyList())),c.getBody().getCoordinates().lastStatement())), p).getBody().getStatements().get(0);
c = c.withBody(c.getBody().withStatements(ListUtils.concat(c.getBody().getStatements(), addVarAStatement)));
}
return c;
}
private J.ClassDeclaration createConstructor(J.ClassDeclaration c, ExecutionContext p){
if(!classMethodAlreadyExists(c, "nolose")) {
JavaTemplate constructorDeclarationTemplate = JavaTemplate.builder(constructorTemplate).contextSensitive().build();
Statement addParametrizedConstructorStatement = maybeAutoFormat(c, c.withBody(
constructorDeclarationTemplate.apply(
new Cursor(getCursor(), c.getBody().withStatements(emptyList())),
c.getBody().getCoordinates().lastStatement(),
"String argumentA" ,
"String argumentB",
"argumentA","argumentA",
"argumentB","argumentB"
)), p).getBody().getStatements().get(0);
c = c.withBody(c.getBody().withStatements(ListUtils.concat(c.getBody().getStatements(), addParametrizedConstructorStatement)));
}
return c;
}
/**
* Returns true if exist a class Var Declaration that name match
* @param classDecl
* @return
*/
private boolean classVarAlreadyExists(J.ClassDeclaration classDecl, String typeAndNameMethod) {
return classDecl.getBody().getStatements().stream()
.filter(statement -> statement instanceof J.VariableDeclarations)
.map(J.VariableDeclarations.class::cast)
.anyMatch(varDeclaration -> varDeclaration.toString().contains(typeAndNameMethod));
}
/**
* Returns true if exist a class Var method that name match
* @param classDecl
* @return
*/
private boolean classMethodAlreadyExists(J.ClassDeclaration classDecl, String typeAndNameMethod) {
return classDecl.getBody().getStatements().stream()
.filter(statement -> statement instanceof J.MethodDeclaration)
.map(J.MethodDeclaration.class::cast)
.anyMatch(method -> method.isConstructor() && method.getParameters().size()==2);
}
}
}
package com.openrewrite.demo.recipe;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Option;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J.NewClass;
import org.openrewrite.java.tree.TypeUtils;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.Value;
@EqualsAndHashCode(callSuper = true)
@Getter
@Setter
public class UpdateConstructorInvocation extends Recipe {
@Option(displayName = "Target Constructor name",
description = "Target Constructor name.",
example = "com.dppware.Ship")
String constructorName ;
@Option(displayName = "Target Constructor first argument",
description = "Target Constructor first argument",
example = "hola")
String firstArgValue ;
@Option(displayName = "Target Constructor second argument",
description = "Target Constructor second argument",
example = "adios")
String secondArgValue ;
@Override
public String getDescription() {
return "Update invocations to constructor";
}
@Override
public String getDisplayName() {
return "Unpdate invocations to constructor";
}
/**
* Main execution with Matchers Preconditions
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddClassParametrizedConstructorVisitor();
}
public class AddClassParametrizedConstructorVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public NewClass visitNewClass(NewClass newClass, ExecutionContext p) {
NewClass newInvocation = super.visitNewClass(newClass, p);
if(TypeUtils.isOfClassType(newInvocation.getType(), constructorName)) {
return JavaTemplate.builder("new Ship(\""+firstArgValue+"\" , \""+secondArgValue+"\" ); " )
.build()
.apply(updateCursor(newInvocation), newInvocation.getCoordinates().replace());
}
return newClass;
}
}
}
We compile the recipe collection:
The execution of the 2 recipes we want to execute at once, that is, in a complete transaction, since if we launch the first one, we won’t be able to launch the second because we’ll leave the code in an unstable state that doesn’t compile.
To execute both, we create the configuration from a yaml file, as we’ve done before. It will look like this: recipe_change_constructor.yaml
type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.ChangeConstructorExample
displayName: Change Ship constructor and invocations
recipeList:
- com.openrewrite.demo.recipe.AddConstructorParametrizedToClass
- com.openrewrite.demo.recipe.UpdateConstructorInvocation:
constructorName: com.openrewrite.demo.testing.Ship
firstArgValue: hola
secondArgValue: adios
and the execution indicating the configuration file will be like this:
cd /tmp/testing-recipe-project
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.ChangeConstructorExample \
-Drewrite.configLocation=/home/dpena/development/workspaces/dppware/gitlab/dppware-adhoc/back/pocs/openrewrite/recipes/recipe_change_constructor.yaml
We analyze the diff and see that everything has changed
public static void main( String[] args )
{
System.out.println( "Hello World!" );
- Ship ship = new Ship();
+ Ship ship = new Ship("hola", "adios" );
}
}
diff --git a/src/main/java/com/dppware/testing/Ship.java b/src/main/java/com/dppware/testing/Ship.java
index 702591d..ff4e5b0 100644
--- a/src/main/java/com/dppware/testing/Ship.java
+++ b/src/main/java/com/dppware/testing/Ship.java
@@ -11,4 +11,10 @@ public class Ship extends Vehicle{
private String propertyA;
@Deprecated
public final Random variableA = new Random();
+ private String argumentA;
+ private String argumentB;
+ public Ship(String argumentA, String argumentB) {
+ this.argumentA = argumentA;
+ this.argumentB = argumentB;
+ }
# RECIPE 8: Real example in dppware multi-module
We’re going to make a change in a library (fwkcna-lib-common-repository), we’re going to deprecate dppwarePageBuilder and we’re going to create a new one called dppwarePageBuilderPro and dppwarePageBuilderImplPro
git clone git@gitlab.gcp.dppware.com:dppware-adhoc/back/lib/fwkcna-lib-repository-common.git
cd fwkcna-lib-repository-common
git checkout -b feat/Changes
WE DON’T DELETE THEM, for the behavior I’ll comment on later about maybeDeleteImport
And we’re going to create a new package com.openrewrite.demo.framework.cna.lib.repository.builders.pro and we’re going to create the new classes that will replace them, so we create these 2 (which are really the same as the old ones but with changed names and references)
I create the 2 classes that will replace them in the builders.pro package
package com.openrewrite.demo.framework.cna.lib.repository.builders.pro;
import org.springframework.data.domain.Pageable;
public interface dppwarePageBuilderPro {
/**
* Thread-safe obtain builder instance
* @return Builder
*/
Builder builder();
public static interface Builder{
public Builder page(int pageNumber) ;
public Builder pageSize(int pageSize);
public Builder sort(String sort);
public Pageable build();
}
}
package com.openrewrite.demo.framework.cna.lib.repository.builders.pro;
import java.util.Arrays;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import com.openrewrite.demo.framework.cna.commons.exception.dppwareRuntimeException;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.logstash.logback.util.StringUtils;
/**
* Class creates page structures using <link>org.springframework.data.domain.PageRequest</link> for a given pagination
* parameters.
*/
@Slf4j
@Getter
public class dppwarePageBuilderImplPro implements dppwarePageBuilderPro {
private static final String VALIDATION_ERROR = "DB_VALIDATION_ERROR";
private static final Integer DEFAULT_INDEXED_MINIMUM_PAGE_INDEX = 0;
private static final Integer ONE_INDEXED_MINIMUM_PAGE_INDEX = 1;
private static final Integer MINIMUM_PAGE_SIZE = 1;
private static final Character ASCENDING = '+';
private static final Character DESCENDING = '-';
public static final int MINIMUM_SORT_LENGTH = 2;
private final boolean isOneIndexedPageParameters;
private final Integer maxPageSize;
private final Integer defaultPageSize;
private Integer pageNumber;
private Integer pageSize;
private Sort sort;
public dppwarePageBuilderImplPro(
boolean isOneIndexedPageParameters,
Integer defaultPageSize,
Integer maxPageSize
) {
this(
isOneIndexedPageParameters,
defaultPageSize,
maxPageSize,
Sort.unsorted()
);
}
public dppwarePageBuilderImplPro(
boolean isOneIndexedPageParameters,
Integer defaultPageSize,
Integer maxPageSize, Sort sort
) {
this.isOneIndexedPageParameters = isOneIndexedPageParameters;
this.defaultPageSize = defaultPageSize;
this.maxPageSize = maxPageSize;
this.pageNumber =
(isOneIndexedPageParameters) ? ONE_INDEXED_MINIMUM_PAGE_INDEX : DEFAULT_INDEXED_MINIMUM_PAGE_INDEX;
this.pageSize = defaultPageSize;
this.sort = sort!=null? sort:Sort.unsorted();
}
private dppwarePageBuilderPro page(Integer page) {
this.validatePageIndex(page);
this.pageNumber = (isOneIndexedPageParameters) ? page - 1 : page;
return this;
}
private dppwarePageBuilderPro pageSize(Integer pageSize) {
this.validatePageSize(pageSize);
this.pageSize = pageSize;
return this;
}
private dppwarePageBuilderPro sort(String sort) {
if (StringUtils.isEmpty(sort)) {
this.sort = Sort.unsorted();
} else {
final List<String> sortList = Arrays.asList(sort.split(","));
var sorted = sortList.stream().map(this::parseSortField).reduce(Sort::and);
this.sort = sorted.orElseGet(Sort::unsorted);
}
return this;
}
private Sort parseSortField(String sort) throws dppwareRuntimeException {
if (sort.length() < MINIMUM_SORT_LENGTH) {
throw createdppwareException(
String.format("There is a validation error in field %s (sort). Field not has enough length", sort));
}
final var direction = sort.charAt(0);
final var field = sort.substring(1);
if (direction == ASCENDING) {
return Sort.by(field).ascending();
} else if (direction == DESCENDING) {
return Sort.by(field).descending();
} else {
throw createdppwareException(
String.format("There is a validation error in field %s (sort). Sort field must starts by + or -",
sort));
}
}
private dppwareRuntimeException createdppwareException(String message) {
return new dppwareRuntimeException(
message,
VALIDATION_ERROR
);
}
// @Override
private Pageable build() {
return PageRequest.of(this.pageNumber, this.pageSize, this.sort);
}
private void validatePageIndex(Integer page) {
if (isOneIndexedPageParameters && page < ONE_INDEXED_MINIMUM_PAGE_INDEX) {
throw createdppwareException(
String.format("Page cannot be less than %s", ONE_INDEXED_MINIMUM_PAGE_INDEX)
);
}
if (!isOneIndexedPageParameters && page < DEFAULT_INDEXED_MINIMUM_PAGE_INDEX) {
throw createdppwareException(
String.format("Page cannot be less than %s", DEFAULT_INDEXED_MINIMUM_PAGE_INDEX)
);
}
}
private void validatePageSize(Integer size) {
if (size > maxPageSize) {
throw createdppwareException(
String.format("The requested page size cannot greater than %s", maxPageSize)
);
}
if (size < MINIMUM_PAGE_SIZE) {
throw createdppwareException(
String.format("The requested page size cannot less than %s", MINIMUM_PAGE_SIZE)
);
}
}
public static class Builder implements dppwarePageBuilderPro.Builder{
private Integer pageNumber;
private Integer pageSize;
private Sort sort;
dppwarePageBuilderImplPro instance;
public Builder(dppwarePageBuilderImplPro seed) {
super();
instance = new dppwarePageBuilderImplPro(seed.isOneIndexedPageParameters, seed.defaultPageSize, seed.maxPageSize, seed.sort);
}
public Builder page(int pageNumber) {
instance.page(pageNumber);
return this;
}
public Builder pageSize(int pageSize) {
instance.pageSize(pageSize);
return this;
}
public Builder sort(String sort) {
instance.sort(sort);
return this;
}
public Pageable build() {
return instance.build();
}
}
@Override
public Builder builder() {
return new dppwarePageBuilderImplPro.Builder(this) ;
}
}
I upgrade the Maven version to the next one
<groupId>com.openrewrite.demo.framework.cna</groupId>
<artifactId>fwkcna-lib-repository-common</artifactId>
<version>4.2.2</version>
By making this change I upgrade the library version I compile to make it available
We download the project
git clone git@gitlab.gcp.dppware.com:dppware-adhoc/back/testingautomatico/demotest-back-apirest-jpa.git
$ mvn dependency:tree |grep repository-common
[INFO] | +- com.openrewrite.demo.framework.cna:fwkcna-lib-repository-common:jar:4.2.1:compile
[INFO] | | +- com.openrewrite.demo.framework.cna:fwkcna-lib-repository-common:jar:4.2.1:compile
Our recipe will look like this, in order:
-
it will remove the library version, then add it in version 4.2.2
-
And then we must write a Visitor that changes all variable declarations of type dppwarePageBuilder and changes them to dppwarePageBuilderPro
-
And we also see in the ExampleRepositoryAdapterTest.java test that some New are being done manually of the deprecated classes. So we also have to write a recipe that explores the NewClass being done related to dppwarePageBuilder and changes them to the new dppwarePageBuilderPro.
-
and finally we’ll add a comment to the pom.xml to indicate that the library has been changed deliberately
We create the file recipe_dppwarepage_changes.yaml
type: specs.openrewrite.org/v1beta/recipe
name: com.openrewrite.demo.dppwarePageDeprecation
displayName: Update from MercadoPageBuilder to dppwarePageBuilderMolon
recipeList:
- org.openrewrite.maven.RemoveDependency:
groupId: com.openrewrite.demo.framework.cna
artifactId: fwkcna-lib-repository-common
scope: runtime
- org.openrewrite.maven.AddDependency:
groupId: com.openrewrite.demo.framework.cna
artifactId: fwkcna-lib-repository-common
version: 4.2.2
scope: compile
onlyIfUsing: com.openrewrite.demo.framework.cna.lib.repository.builders.*
type: jar
classifier: ''
optional: null
acceptTransitive: false
- com.openrewrite.demo.recipe.MigrateVarDeclarationsTodppwarePageBuilderToPro
- com.openrewrite.demo.recipe.MigrateNewClassInvocationsTodppwarePageBuilderToPro
- org.openrewrite.maven.AddCommentToMavenDependency:
xPath: /project/dependencies/dependency
groupId: com.openrewrite.demo.framework.cna
artifactId: fwkcna-lib-repository-common
commentText: EL molon mola mas y por eso lo hemos metido, vas a flipar colega
The recipe com.openrewrite.demo.recipe.MigrateVarDeclarationsTodppwarePageBuilderToPro will look like this
package com.openrewrite.demo.recipe;
import java.util.stream.Collectors;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.J.VariableDeclarations;
import org.openrewrite.java.tree.TypeUtils;
public class MigrateVarDeclarationsTodppwarePageBuilderToPro extends Recipe {
private final String Target_Class = "com.openrewrite.demo.framework.cna.lib.repository.builders.dppwarePageBuilder";
private final String Accepted_Target_Class= "com.openrewrite.demo.framework.cna.lib.repository.builders.pro.dppwarePageBuilderPro";
@Override
public String getDescription() {
return "MigrateTodppwarePageBuilderToPro";
}
@Override
public String getDisplayName() {
return "MigrateTodppwarePageBuilderToPro";
}
/**
* Main execution with Matchers Preconditions
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new FindVarDeclarationAndUpdateVisitor();
}
public class FindVarDeclarationAndUpdateVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public VariableDeclarations visitVariableDeclarations(VariableDeclarations multiVariable, ExecutionContext p) {
J.VariableDeclarations variableDeclarations = super.visitVariableDeclarations(multiVariable, p);
if (TypeUtils.isOfClassType(variableDeclarations.getType(), Target_Class)) { //Better this way, you don't need it in the classpath if it was a proprietary class
J.VariableDeclarations.NamedVariable nv = variableDeclarations.getVariables().get(0);
String finalVarVariableTemplateString = "dppwarePageBuilderPro #{} ;";
finalVarVariableTemplateString = String.format("%s %s %s", maintainVariableAnnotationsIfExists(variableDeclarations),
maintainVariableModifiersIfExists(variableDeclarations),
finalVarVariableTemplateString) ;
Object[] args = new Object[]{ nv.getSimpleName()}; //Maintain original Declaration variable name
variableDeclarations = JavaTemplate.builder(finalVarVariableTemplateString)
.contextSensitive()
.imports(Accepted_Target_Class)
.build()
.apply(updateCursor(variableDeclarations), variableDeclarations.getCoordinates().replace(), args);
//It's necessary to change the import
maybeAddImport(Accepted_Target_Class, null, false);
maybeRemoveImport(Target_Class);
}
return variableDeclarations;
}
private String maintainVariableAnnotationsIfExists(J.VariableDeclarations variableDeclarations) {
if(variableDeclarations.getAllAnnotations().size()>0) {
String annotationDeclarations = variableDeclarations.getAllAnnotations().stream().map(Object::toString)
.collect(Collectors.joining("\n "));
return annotationDeclarations;
}
return "";
}
private String maintainVariableModifiersIfExists(J.VariableDeclarations variableDeclarations) {
if(variableDeclarations.getModifiers().size()>0) {
String varModifiers = variableDeclarations.getModifiers().stream().map(Object::toString)
.collect(Collectors.joining(" "));
return varModifiers;
}
return " ";
}
}
}
and the recipe that explores calls to constructors will look like this:
package com.openrewrite.demo.recipe;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J.NewClass;
import org.openrewrite.java.tree.TypeUtils;
public class MigrateNewClassInvocationsTodppwarePageBuilderToPro extends Recipe {
private final String Target_Class = "com.openrewrite.demo.framework.cna.lib.repository.builders.dppwarePageBuilderImpl.Builder";
private final String Accepted_Target_Class= "com.openrewrite.demo.framework.cna.lib.repository.builders.pro.dppwarePageBuilderImplPro";
@Override
public String getDescription() {
return "MigrateTodppwarePageBuilderToPro";
}
@Override
public String getDisplayName() {
return "MigrateTodppwarePageBuilderToPro";
}
/**
* Main execution with Matchers Preconditions
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new AddClassParametrizedConstructorVisitor();
}
public class AddClassParametrizedConstructorVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public NewClass visitNewClass(NewClass newClass, ExecutionContext p) {
NewClass newInvocation = super.visitNewClass(newClass, p);
if(TypeUtils.isOfClassType(newInvocation.getType(), Target_Class)) {
maybeAddImport(Accepted_Target_Class, null, false);
return JavaTemplate.builder("new dppwarePageBuilderImplPro.Builder(new dppwarePageBuilderImplPro(true, 1, 10 )); " )
.build()
.apply(updateCursor(newInvocation), newInvocation.getCoordinates().replace());
}
return newClass;
}
}
}
We compile our recipe collection
and execute the recipe on the demotest project
In multi-module projects it’s necessary to specify the full Path of the configLocation, since as it executes for each one the relative path doesn’t match
cd demotest-back-apirest-jpa
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=com.openrewrite.demo.recipe:dppware-recipe:1.0-SNAPSHOT \
-Drewrite.activeRecipes=com.openrewrite.demo.dppwarePageDeprecation \
-Drewrite.configLocation=/home/dpena/tmp/KATA/openrewrite/recipes/recipe_dppwarepage_changes.yaml
[WARNING] There were problems parsing boot/charts/env/ocp/values-dev.yaml
[INFO] Running recipe(s)...
[WARNING] Changes have been made to repository-sql/pom.xml by:
[WARNING] com.openrewrite.demo.dppwarePageDeprecation
[WARNING] org.openrewrite.maven.AddDependency: {groupId=com.openrewrite.demo.framework.cna, artifactId=fwkcna-lib-repository-common, version=4.2.2, scope=compile, onlyIfUsing=com.openrewrite.demo.framework.cna.lib.repository.builders.*, type=jar, classifier=, acceptTransitive=false}
[WARNING] org.openrewrite.maven.AddCommentToMavenDependency: {xPath=/project/dependencies/dependency, groupId=com.openrewrite.demo.framework.cna, artifactId=fwkcna-lib-repository-common, commentText=EL molon mola mas y por eso lo hemos metido, vas a flipar colega}
[WARNING] org.openrewrite.maven.RemoveDependency: {groupId=com.openrewrite.demo.framework.cna, artifactId=fwkcna-lib-repository-common, scope=runtime}
[WARNING] Changes have been made to repository-sql/src/main/java/com/dppware/framework/cna/demo/api/rest/test/repositories/adapters/ExampleRepositoryAdapter.java by:
[WARNING] com.openrewrite.demo.dppwarePageDeprecation
[WARNING] com.openrewrite.demo.recipe.MigrateTodppwarePageBuilderToPro
[WARNING] Changes have been made to repository-sql/src/test/java/com/dppware/framework/cna/demo/api/rest/test/repositories/adapters/ExampleRepositoryAdapterTest.java by:
[WARNING] com.openrewrite.demo.dppwarePageDeprecation
[WARNING] com.openrewrite.demo.recipe.MigrateTodppwarePageBuilderToPro
[WARNING] Please review and commit the results.
We see that it has recursively processed all the project modules and applied the visitors where they applied
If we see the final result with the diff we see the changes and verify that it has replaced the variable type with the new dppwareBuilderPro both in class declarations and in invocations to its constructor.
MaybeDeleteImport: this method from Recipes deletes the import if it detects that it’s not used in the class. But in this execution, when it built the tree it was being used and therefore even if we specify it, it won’t delete it. We need a second execution indicating for example https://docs.openrewrite.org/recipes/java/removeunusedimports, when it re-analyzes the project before executing the removeunusedimports it will see that it’s not being used and it will delete it.
We execute:
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.activeRecipes=org.openrewrite.java.RemoveUnusedImports
and we see that the execution has removed the imports of the deprecated ones, because they’re no longer referenced
-import com.openrewrite.demo.framework.cna.lib.repository.builders.dppwarePageBuilder;
-import com.openrewrite.demo.framework.cna.lib.repository.builders.dppwarePageBuilderImpl;
-
This is a clear example that code modifications should be done in small steps, that is, first deprecate, offer the alternative and finally be able to remove in the next minor/major version