Skip to content

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
We edit the pom and add the dependencies suggested by the website. Our pom will look like this:

<?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();
    }
}
getDescription and getDisplayName are methods from the rewrite core used to find the recipe and are also used for logging/reporting the recipe execution.

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)

LST_Diagram.webp

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
We initialize the directory as a git repo to see changes more easily:
git init
git add -A
git commit -m "initial import"

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);
        }
    }
}
As we see in the method declaration, its execution checks if comments exist to add them in case they didn’t exist. Let’s create a test Java class in the project to see how it behaves:

package com.openrewrite.demo.testing;

public class Ship {

}
The expected result is that a comment will only be added to the Ship class, since App and AppTest already had comments from the project creation.

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
We save:

git add -A
git commit -m "Added class comments

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
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.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] ------------------------------------------------------------------------
git diff shows us the changes.

We save:

git add -A
git commit -m "Added compilation Unit comments"

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;
}
And now we execute our recipe:

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.
we see the diff
 public class Ship {

+    //comment
+    private String propertyA;
 }

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

mvn clean install
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
Preparation We create a Vehicle class, just for the purpose of having Ship extend from it. It will look like this: ```java package com.openrewrite.demo.testing;

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

mvn clean install

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

public class Vehicle {

    protected String mayusculas(String key){
        return key.toUpperCase();
    }

}

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));
        }

    }
}
As we can see, we’ve used preconditions again and also the JavaTemplate for the method declaration.

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));
        }

    }
}
The execution looks 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.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);
        }

    }
}
and the other one

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:

mvn clean install

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’re going to deprecate dppwarePageBuilder, dppwarePageBuilderImpl.
@Slf4j
@Getter
@Deprecated
public class dppwarePageBuilderImpl implements dppwarePageBuilder {
...
@Deprecated
public interface dppwarePageBuilder {
...

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();
    }
}
and the other one
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

mvn clean install

We download the project

git clone git@gitlab.gcp.dppware.com:dppware-adhoc/back/testingautomatico/demotest-back-apirest-jpa.git
We see it uses version 4.2.1
$ 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:

  1. it will remove the library version, then add it in version 4.2.2

  2. And then we must write a Visitor that changes all variable declarations of type dppwarePageBuilder and changes them to dppwarePageBuilderPro

  3. 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.

  4. 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

mvn clean install

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