Saltar a contenido

Aprendiendo a escribir nuestrar propias recetas

Es necesario que aprendamos a escribir nuestras recetas, porque como hemos visto a pesar de que hay muchas publicas, hay veces que no encajan en lo nuestro, o simplemente queremos hacer algun tipo de tareas especificas de nuestro proyecto.

Como hemos visto anteriormente, las recetas se distribuyen en artefactos maven.

Para crear nuestras recetas, necesitamos crear un proyecto maven y importar el core de open-rewrite, extender sus clases y escribir nuestras ejecuciones.

Desde openRewrite nos ofrecen un “starter” con todas las dependencias necesarias para empezar a escribir nuestras recetas: https://docs.openrewrite.org/authoring-recipes/recipe-development-environment

Desde ahi tenemos la opcion con gradle, pero vamos a ver como hacerlo con maven

Creamos un proyecto maven

mvn -B archetype:generate -DgroupId=com.openrewrite.demo.recipe -DartifactId=dppware-recipe -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4
Editamos el pom y añadimos las dependencias que nos sugiere la web. nos quedara el pom asi:

<?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>

Compilamos para ver qeu todo va bien y empezamos a escribir nuestra primera receta:

Escribiendo Recetas

Una receta es una clase JAva que extiende de la clase abstracta org.openrewrite.Recipe. Creamos una clase llamada FirstRecipe, extendemos Recipe e implementamos 3 metodos basicos:

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 y getDisplayName son metodos del core de rewrite usados para encontrar la receta y tambien se usan para el log/report de la ejecucion de la receta.

getVisitor es el corazon de la ejecucion de una receta: Como vimos anteriormente el arbol LST esta formado por nodos, estos nodos pueden ser de tipo method, variableDeclaration, Class, CompilationUnit … En la web oficial tenemos todos los elementos: [https://docs.openrewrite.org/concepts-explanations/lst-examples] (https://docs.openrewrite.org/concepts-explanations/lst-examples)

LST_Diagram.webp

El patron visitor , visitará todos los nodos que se construyan en el arbol LST y para cada uno aplicará las ejecuciones que definamos en nuestro visitor.

Vamos a crear un visitor propio en vez de devolver el super.getVisitor():

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor() {
        // TODO Auto-generated method stub
        return new JavaVisitor<ExecutionContext>() {
        };
    }

Antes de escribir la implementacion debemos saber que tipo de elemento del arbol LST queremos modificar (es decir si queremos cambiar una variable, una deficion de metodo, una clase porque vamos a añadir una nueva variable, etc…)

En caso de querer modificar elementos Java usaremos JavaVisitor Asi que vemos que JavaVisitor nos ofrece varios metodos que podemos implementar de manera sencilla para manipular elementos java:

@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..
        };

USANDO NUESTRAS RECETAS

Preparacion

Vamos a crear un proyecto sencillo maven que haga de conejillo de indias para nuestras recetas:

Vamos a un directorio vacio y ejecutamos el archetype de maven para crear un proyecto sencillo:

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
Inicializamos el directorio como repo git para poder ver los cambios de manera mas sencilla:
git init
git add -A
git commit -m "initial import"

RECETA 1: AddCommentToClassDeclaration

Queremos añadir unos comentarios a todas las clases del proyecto.

Nuestro visitor esta focalizado en visitar las declaraciones de clase que existan en nuestro proyecto, asi que debemos implementar el metodo visitClassDeclaration de nuestro JavaVisitor.

Creamos una receta llamada AddCommentToClassDeclaration que quedara así:

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);
        }
    }
}
Como vemos en la declaracion del metodo, su ejecucion comprueba si existen comentarios para añadirlos en caso de que no existiera. Vamos a crear una clase java de prueba en el proyecto para ver como se comporta:

package com.openrewrite.demo.testing;

public class Ship {

}
El resultado esperado es que solo se añada comentario a la clase Ship, ya que la App y AppTest ya tenian comentarios de la creacion del proyecto.

Compilamos nuestro modulo de recetas:

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

Vamos al proyecto y ejecutamos el plugin de openrewrite usando las coordenadas de nuestro artefacto y el nombre de la receta. Nos quedara asi:

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] ------------------------------------------------------------------------

Vemos que ha habido cambios, hacemos el 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
Guardamos:

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

RECETA 2: AddCommentToCompilationUnit

Escribimos otra receta que su target de Visitor son las compilationUnit (no es la clase, una unidad de compilacion engloba definicion de paqueteria, dependencias de import, etc..). Vamos a añadir una licencia dppware a todas las clases. Nos quedara asi:

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 = "Quien tuviere narices de copiar este codigo sera castigado con 3 flexiones y 2 cafes de la maquina del pasillo";

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

Recompilamos para empaquetar nuestras recetas y ejecutamos indicando el nombre de la receta com.openrewrite.demo.recipe.AddCommentToCompilationUnit

Compilamos nuestro modulo de recetas:

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
Vamos al proyecto y ejecutamos el plugin de openrewrite usando las coordenadas de nuestro artefacto y el nombre de la receta. Nos quedara asi:
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 nos indica los cambios.

Guardamos:

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

RECETA 3: AddCommentToClassVariable

Hay que tener en cuenta que una declaracion de una varible no es solo a nivel de clase, puedes definir una variable como signatura de un metodo, o tambien dentro de un metodo, o dentro de un bucle, etc…

Asi que esta receta requiere un poco mas de trasteo con el API del manejo de LST.

La receta nos quedara asi:

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

    }
}

compilamos nuestro artefacto de recetas
```sh
mvn clean install

Como añade comentario a una variable propertyA , vamos a añadir una variable a la clase Ship.java del proyecto donde estamos relaizando las ejecuciones

/*Quien tuviere narices de copiar este codigo sera castigado con 3 flexiones y 2 cafes de la maquina del pasillo*/
package com.openrewrite.demo.testing;


/*This is AddCommentToClassDeclaration*/
public class Ship {

    private String propertyA;
}
Y ahora ejecutamos nuestra receta:

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.
vemos el diff
 public class Ship {

+    //comment
+    private String propertyA;
 }

RECETA 4: AddClassVariable

Vamos a definir una nueva variable en una clase. Debemos usar el Visitor de CLassDeclaration, ya que es a nivel de clase donde vamos a meter la variable.

Nuestra receta quedara asi:

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

    }
}

Vemos que para cosas complejas podemos hacer uso de JavaTemplate. Esta utilidad nos permite escribir bloques java de manera mas sencilla como si fueran templates. En este ejemplo ademas vamos a crear una variable de tipo Random con lo que vamos a necesitar añadir ese import.

compilamos nuestro recetario

mvn clean install
y ejecutamos en el proyecto:

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.

 ```

 Como vemos nos ha añadido la declaracion de la variable a todas las clases del proyecto. En muchos casos queremos establecer una condicion para la ejecucion de nuestro visitor.

 # RECETA 4: AddClassVariable with PRECONDITIONS

 Las precondiciones son clases java, que filtran antes de la ejecucion del visitor. 
 Vamos a deshacer los cambios ejecutados anteriormente y vamos crear la variable solo si la clase extiende de una determinada clase padre.
```sh
git stash
Preparacion Creamos una clase Vehicle, solo con el proposito de que Ship extienda de ella. Nos quedara asi: ```java package com.openrewrite.demo.testing;

public class Vehicle {

}

 ```java
/*Quien tuviere narices de copiar este codigo sera castigado con 3 flexiones y 2 cafes de la maquina del pasillo*/
package com.openrewrite.demo.testing;


/*This is AddCommentToClassDeclaration*/
public class Ship extends Vehicle{

    //comment
    private String propertyA;
}

Creamos la Precondition de manera generica, es decir, en el constructor podemos indicar que clase es la que esperamos que extienda para cumplirse la condicion

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;
    }
    /**
     * Solo declaramos el visitor donde esperamos encontrar la condicion
     */
    @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);
    }
}

y ahora modificamos nuestra receta, cambiando el metodo getVisitor para que si se da la condicion de que extiende de Vehicle devuelva la ejecucion del visitor.

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor() {
        return Preconditions.check(
                Preconditions.or(
                        new PreconditionClassExtends<>("com.openrewrite.demo.testing.Vehicle")),
                new AddClassRandomVariableDeclaration());

    }

Vamos a compilar nuestro recetario

mvn clean install

y lo ejecutamos sobre el proyecto:

 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

Vemos que solo se ha visto afectada la clase Ship

# RECETA 5: AddClassMethod with PRECONDITIONS

Vamos a crear un metodo usando una precondicion de si el metodo no existe en la clase. Es decir si la clase ya lo tiene no lo mete.

Preparamos el codigo del proyecto añadiendo un metodo llamado mayusculas a la clase Vehicle.java

public class Vehicle {

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

}

Preparamos una precondition que se pueda reutilizar y que podamos decir el nombre del metodo :

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;
    }
    /**
     * Solo declaramos el visitor donde esperamos encontrar la condicion
     */
    public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, P p) {
        if(method.getSimpleName().equals(methodName)) {
            return SearchResult.found(method);
        }
        return super.visitMethodDeclaration(method, p);
    };

}

Preguntaremos si la clase declara un metodo llamado mayusculas y si es asi metemos otro metodo que hara uso de él.

Y la receta nos queda asi:

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

    }
}
Como podemos ver, hemos vuelto a hacer uso de las preconditions y tambien del JavaTemplate para la declaracion del metodo.

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.

 ```

 Vemos que nos ha añadido el metodo en la clase Vehicle que es la que declara el metodo.
 ```diff
  public class Vehicle {

     protected String mayusculas(String key){
         return key.toUpperCase();
     }
+    public String concatenateWithMe(String arg1, String arg2) {
+        return this.mayusculas(arg1 + arg2);
+    }
 }
 ```

 # RECETA 6: Añadir una linea de ejecucion dentro de un metodo

Vamos a buscar un metodo que si existe, la primera linea vamos a cambiarla para que sea una ejecucion que nosotros queramos. En este caso un new de un objeto Ship.

Por simpleza vamos a usar la precondition PreconditionHasMethodWithName. 

Si la clase tiene un metodo con nombre main, pues vamos a modificar el contenido añadiendo una sentencia a la ejecucion que instancie un Ship.

Nuestra receta queda asi **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));
        }

    }
}
La ejecucion nos queda asi:

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

 ```
Vemos el diff y vemos que ha encontrado el metodo main y ha incluido el 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();
     }
 }

# RECETA 7: Crear un nuevo constructor parametrizado

Esta receta va a crear un constructor con 2 parametros. Esos 2 parametros deberemos definirlos a nivel de clase, para poder asignarlos en el constructor. Asi que esta receta va a ser un poco mas compleja.

Vamos a cambiar el constructor de Ship para que pida 2 argumentos. Pero si hacemos eso el new que estamos haciendo en la clase App.java#main va a fallar porque necesitará proporcionar esos argumentos.

Esta receta requiere de 2 ejecuciones (recetas)

La primera receta AddConstructorParametrizedToClass añadira los campos y creara el constructor La segunda receta UpdateConstructorInvocation, buscara las invocaciones al constructor de Ship y las cambiara para añadirle los nuevos argumentos que le vamos a pasar como argumentos en la receta

Nos quedaran asi:

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

    }
}
y la otra

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

}

Compilamos el recetario:

mvn clean install

La ejecucion de las 2 recetas queremos que se ejecute de una vez, es decir en una transaccion completa, ya que si lanzamos la primera, la segunda no la vamos a poder lanzar porque dejaremos el codigo en un estado inestable que no llega a compilar.

Para ejecutar las 2 creamos la configuracion a partir de un fichero yaml, como hemos hecho otras veces. Nos quedara asi: 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 

y la ejecucion indicando el fichero de configuracion sera asi:

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

Analizamos el diff y vemos que ha cambiado todo

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

# RECETA 8: Ejemplo real en multimodulo dppware

Vamos a hacer un cambio en una lib (fwkcna-lib-common-repository), vamos a deprecar dppwarePageBuilder y vamos a crear una nueva llamada dppwarePageBuilderPro y 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
Vamos a deprecar dppwarePageBuilder, dppwarePageBuilderImpl.
@Slf4j
@Getter
@Deprecated
public class dppwarePageBuilderImpl implements dppwarePageBuilder {
...
@Deprecated
public interface dppwarePageBuilder {
...

NO LAS BORRO, por el comportamiento que comentare despues del maybeDeleteImport

Y vamos a crea un paquete nuevo com.openrewrite.demo.framework.cna.lib.repository.builders.pro y vamos a crear las nuevas clases que la sustituiran asi que creamos estas 2 (que es realmente lo mismo que las antiguas pero cambiado los nombres y las referenncias)

Creo las 2 clases que las van a sustituir en el paquete builders.pro

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();
    }
}
y la otra
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) ;
    }

}

subo la version de maven a la siguiente

  <groupId>com.openrewrite.demo.framework.cna</groupId>
  <artifactId>fwkcna-lib-repository-common</artifactId>
  <version>4.2.2</version>

AL haer este cambio subo una vesion la lib Hago la compilacion para tenerla disponible

mvn clean install

Descargamos el proyecto

git clone git@gitlab.gcp.dppware.com:dppware-adhoc/back/testingautomatico/demotest-back-apirest-jpa.git
Vemos que usa la 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

Nuestra receta tendra esta pinta, por orden:

  1. quitara la version de la lib, luego la añadira en version 4.2.2

  2. Y despues debemos escribir un Visitor que cambiar todas las declaraciones de variable del tipo dppwarePageBuilder y cambiarlas por la dppwarePageBuilderPro

  3. Y tambien vemos en el test ExampleRepositoryAdapterTest.java que se estan haciendo unos New a mano del las clases deprecadas. Asi que tambien tenemos que escribir una receta que explore los NewClass que se estan haciendo relacionados con el dppwarePageBuilder y cambiarlos por el nuevo dppwarePageBuilderPro.

  4. y por utlimo añadiremos un comentario al pom.xml para indicar que se ha cambiado la lib a conciencia

Creamos el fichero 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

La receta com.openrewrite.demo.recipe.MigrateVarDeclarationsTodppwarePageBuilderToPro tendra este aspecto

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)) { //Mejor asi, no la necesitas en el classpath si fuera una clase propietaria 


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

                //Es necesario cambiar el 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 " ";
        }

    }
}

y la receta que explora la llamada a los consutrctores nos quedara asi:

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

}

Compilamos nuestro recetario

mvn clean install

y ejecutamos la receta sobre el proyecto demotest

En proyectos multimodulo es necesario especificar el Path completo del configLocation, ya que como lo ejecuta para cada uno la ruta relativa no le cuadra

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.

Vemos que recursivamente ha ido procesando todos los modulos de proyecto y aplicando los visitor donde tocaban

Si vemos el resutaldo final con el diff vemos los cambios y vemos que ha sustituido el tipo de la variable por el nuevo dppwareBuilderPro tanto en las declaraciones de clase como en las invocaciones a su constructor.

MaybeDeleteImport : este metodo de los Recipe borra el import si detecta que no se usa en la clase. Pero en esta ejecucion, cuando ha montado el arbol si que se estaba usando y por lo tanto aunque se lo especficiquemos no lo va a borrar. Necesitamos hacer una segunda ejecucion indicando por ejempo https://docs.openrewrite.org/recipes/java/removeunusedimports, cuando vuelva a analizar el proyecto antes de ejecutar el removeunusedimports verá que no se esta usando y si que lo borrara.

Ejecumamos:

mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.activeRecipes=org.openrewrite.java.RemoveUnusedImports

y vemos que la ejecucion ha quitado los imports de las deprecadas, porque ya no estan referenciadas

-import com.openrewrite.demo.framework.cna.lib.repository.builders.dppwarePageBuilder;
-import com.openrewrite.demo.framework.cna.lib.repository.builders.dppwarePageBuilderImpl;
-

Este es un claro ejemplo de que las modificaciones de codigo deben hacerse a cortos pasos, es decir, primero deprecar , ofrecer la alternativa y por ultimo ya poder quitar en la siguiente version minor/major