Develop your own migrations with Open Rewrite

Open Rewrite offers developers a variety of migrations for common libraries and frameworks. They significantly reduce the migration effort and make the application reproducible. But how can developers provide recipes for their own frameworks? This blog post provides an introduction to the development of open rewrite recipes.

The 3 types of recipes

In Open Rewrite, recipes are used to define migrations and execute them in a repeatable way. The basic concept was described in a previous article. There are three different abstractions with different levels of complexity: declarative YAML recipes, refaster templates and imperative recipes.

Declarative recipes are used to configure existing recipes and execute them together. In the first step, the desired recipe is identified in the Open Rewrite recipe catalogue and the fully qualified name is determined. This name corresponds to the class and package name and is specified directly under the heading on the documentation page. If recipes from your own catalogue are to be used, the qualifying name is also used in this case. For example, to remove an annotation, the recipe Remove annotation with the qualifying name org.openrewrite.java.RemoveAnnotation is suitable. We make the necessary configuration in a new YAML recipe.

	
		---
		type: specs.openrewrite.org/v1beta/recipe
		name: de.adesso.houskeeping.ExampleRecipe
		displayName: Example Recipe to replace Service Call
		recipeList:
		  - org.openrewrite.java.RemoveAnnotation:
		      annotationPattern: '@java.lang.SuppressWarnings("deprecation")'
	

The type identifies it as a recipe for open rewrite migrations and can be addressed via the qualifying name after name. The displayName is mainly used for the automatically generated recipe catalogue. The recipes to be executed are specified in the recipeList. The possible configurations are specified on the respective recipe pages. If the existing basic recipes in the recipe catalogue are not sufficient, new recipes must be written.

One possible example would be the replacement of a method call to a service. This is where the use of Refaster templates from the Error Prone Project comes in handy. Error Prone Refaster can be used to perform pattern-based refactorings. Open Rewrite supports the Refaster templates and thus offers a suitable abstraction for the customisation of method calls.

	
		package de.adesso.houskeeping
		@RecipeDescriptor(
		    name = "Migrate to modern Method",
		    description = "Migrate the old method to modern"
		)
		static class SwitchToModernMethod {
		    @BeforeTemplate
		    void old(MyService srv, String msg, int times) {
		        srv.oldMethode(times, msg);
		    }
		    @AfterTemplate
		    void modern(MyService srv, String msg, int times) {
		        srv.otherMethode(msg, times);
		    }
		}
	

A Refaster Template recipe is marked as a recipe by the @RecipeDesciptor and given a name for the documentation and a description. The code passage to be replaced is specified in the method labelled @BeforeTemplate. The service instance and the call parameters in the method are used according to type. The method marked with @AfterTemplate defines the target state and uses type safety to adjust the order of the parameters.

Refaster templates can only make changes within a code block. If further adjustments are necessary and these are not possible by combining existing recipes, an imperative recipe can be created. These imperative recipes extend the abstract class org.openrewrite.Recipe.

	
		public class TestRecipe extends org.openrewrite.Recipe {
		    @Override
		    public String getDisplayName() {
		        return "An example Recipe";
		    }
		    @Override
		    public String getDescription() {
		        return "This Recipe is an example for our Blog.";
		    }
		    @Override
		    public TreeVisitor<?, ExecutionContext> getVisitor() {
		        return super.getVisitor();
		    }
		}
	

The getDisplayName method returns the display name and getDescription returns the description for the automatically generated documentation. The getVisitor method returns an instance of a TreeVisitor. A TreeVisitor runs through the Lossless Semantic Tree (LST) and manipulates individual nodes in this tree. The LST is the representation of the entire source code within the Open Rewrite Tool. The LST is generated when Open Rewrite is started and is used after all recipes have been executed to convert the changes back into source code. There is a separate LST implementation and customised visitors for each supported language. To manipulate Java source code, the org.openrewrite.java.JavaIsoVisitor is used. This contains a visit method for each element type in the LST, which returns the changed element.

Due to the comprehensive API, there is a very steep learning curve when creating recipes. The documentation of the Open Rewrite projekt and the Slack/Discord channels of the Open Rewrite Community help with the first steps.

Testing recipes

A migration to Spring Boot 3.2 is too extensive and generic to be secured by a test. Instead, it makes more sense to test small changes to the source code. To make this possible, Open Rewrite offers a possibility based on JUnit Jupiter to create tests for recipes.

	
		public class MigrateTestAnnotationTest implements RewriteTest {
		    @Override
		    public void defaults(RecipeSpec spec) {
		        spec.parser(JavaParser.fromJavaVersion())
			  .recipeFromResources("RecipesFQN")
		          .recipe(new MigrateTestAnnotation());
		    }
		    @Test
		    void migrateEmptyTestsWithBraces() {
		        rewriteRun(java("""
		          import org.testng.annotations.Test;
		          class MyTest {
		              @Test()
		              void test() {}
		          }
		          """, """
		          import org.junit.jupiter.api.Test;
		          class MyTest {
		              @Test
		              void test() {}
		          }
		      """));
		    }
		}
	

By implementing the interface org.openrewrite.test.RewriteTest, the test class is extended by the necessary infrastructure. The default method is used to configure Open Rewrite for the upcoming test cases. This is done via the Fluent API of the RecipeSpec. In this example, the parser is configured as a Java parser whose version is derived from the JDK version used. Recipes written in Java can be activated using the recipe method. YAML recipes can be loaded and activated from the resources using recipeFromResources. The actual tests are carried out with the method call rewriteRun within the method annotated with @Test. The method org.openrewrite.java.Assertions#java ensures that the Java source code passed as is is migrated to the should code. In addition to Java, there are further assertions for every other supported language.

Distribution of own recipes

There is great added value for organisations if the developed recipes are reused in other contexts. A Maven artefact is ideal for this. With the Recipe Starter, Modern offers a solid basis with initial examples for your own recipe collection and an integration of Maven and Gradle.

I have already covered the concepts described in this blog post in various conference contributions and workshops. In the last part of my blog post on the topic of Open Rewrite, I will discuss scaled use in organisations.

Would you like to find out more about exciting topics from the world of adesso? Then take a look at our previous blog posts.

Also interesting:

Picture Merlin Bögershausen

Author Merlin Bögershausen

Merlin Bögershausen is a software developer, speaker and author at adesso in Aachen. He deals with innovations in the Java universe and software development in the public sector.

Save this page. Remove this page.