Preconditions that must be met to allow a successful migration can be checked before scanning an application. This allows a user to be informed about unmet preconditions that would prevent a successful migration before the sometimes lengthy parsing is done.
To add a new precondition check you need to provide a component extending abstract PreconditionCheck
class
and return the result of the precondition check as PreconditionCheckResult
.
@Component
public class MyPreconditionCheck extends PreconditionCheck {
@Override
public PreconditionCheckResult verify(Path projectRoot, List<Resource> projectResources) {
// verify precondition is met...
}
}
This class must be placed under org.springframework.sbm
to be picked up by component scan and gets executed after scanning
the project but before parsing resources into an AST.
If one of the preconditions fails (ResultState.WARN
or ResultState.FAILED
), the scan is interrupted and a message is
shown to the user.
First step on your journey to implement a new recipe will be the implementation of an Action
.
Every Action that needs access to the ProjectContext to modify resources should extend AbstractAction
.
class MyAction extends AbstractAction {
void apply(ProjectContext context) {
// analyse and modify AST
}
}
Use [TestProjectContext] to test your Action.
A long running action can leave the user without any information about the progress of the migration.
The Action
interface defines methods to display progress information to the user.
There are two types of rendered information, process and log messages.
logEvent(String)
logs a message inside a routine
When an action starts, a first process (the action) is automatically started. It begins with the action and ends with success or error. If no other progress change is reported during the execution of the action, a loader is rendered during the
. My Action
.. My Action
[..] My Action
. Sub Routine
.. Sub Routine
[ok] Sub Routine
[ok] My Action
Recipes bundle a set of Actions for a migration. They can be declared programmatically as Spring beans or declarative using yaml syntax. The main difference is that yaml recipes can be provided to SBM without code changes on startup allowing to add new, declarative recipes without recompilation while the bean definition is arguably easier to use.
Declarative recipes must be placed under src/main`resources/recipes
.
See initialize-spring-boot-migration.yaml for a recipe in YAML syntax
A recipe can be defined as Spring bean using @Bean annotated method, see MigrateMuleToBoot.java for a recipe declared as Spring bean.
Note
|
Remember to provide a description to all Actions to display the description to the user when the Action is applied. |
ProjectContext context = ...
String annotationPattern = "@java.lang.Deprecated";
org.openrewrite.java.RemoveAnnotation rewriteRecipe = new RemoveAnnotation(annotationPattern);
context.getProjectJavaSources().apply(rewriteRecipe);
Use org.springframework.sbm.engine.recipe.OpenRewriteDeclarativeRecipeAdapter
to use embedded OpenRewrite YAML syntax in SBM YAML to run a declarative OpenRewrite recipe as SBM action.
- name: test-recipe
description: "Remove @Deprecated annotations"
condition:
type: org.springframework.sbm.common.migration.conditions.TrueCondition
actions:
- type: org.springframework.sbm.engine.recipe.OpenRewriteDeclarativeRecipeAdapter
description: "Use a OpenRewrite recipe to remove @Deprecated annotations"
openRewriteRecipe: |-
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.java.RemoveAnnotation
displayName: "Remove @Deprecated annotation"
description: "Remove @Deprecated annotation"
recipeList:
- org.openrewrite.java.RemoveAnnotation:
annotationPattern: "@java.lang.Deprecated"
Use org.springframework.sbm.engine.recipe.OpenRewriteNamedRecipeAdapter
to run an OpenRewrite recipe by name as SBM action.
Here an OpenRewrite recipe with name org.springframework.sbm.dummy.RemoveDeprecatedAnnotation
must exist on the classpath and will be executed as action in test-recipe
.
- name: test-recipe
description: "Remove @Deprecated annotations"
condition:
type: org.springframework.sbm.common.migration.conditions.TrueCondition
actions:
- type: org.springframework.sbm.engine.recipe.OpenRewriteNamedRecipeAdapter
description: "Call a OpenRewrite recipe to remove @Deprecated annotations"
openRewriteRecipeName: org.springframework.sbm.dummy.RemoveDeprecatedAnnotation
@Configuration
public class SomeRecipe {
@Bean
Recipe someRecipe(RewriteRecipeLoader rewriteRecipeLoader, RewriteMigrationResultMerger resultMerger) {
return Recipe.builder()
...
.action(
OpenRewriteDeclarativeRecipeAdapter.builder()
.rewriteRecipeLoader(rewriteRecipeLoader)
.resultMerger(resultMerger)
.openRewriteRecipe(
"type: specs.openrewrite.org/v1beta/recipe\n" +
"name: org.openrewrite.java.RemoveAnnotation\n" +
"displayName: \"Remove @Deprecated annotation\"\n" +
"description: \"Remove @Deprecated annotation\"\n" +
"recipeList:\n" +
" - org.openrewrite.java.RemoveAnnotation:\n" +
" annotationPattern: \"@java.lang.Deprecated\"\n"
)
.build())
...
.build();
}
}
@Configuration
public class SomeRecipe {
@Bean
Recipe someRecipe(RewriteRecipeLoader rewriteRecipeLoader, RewriteMigrationResultMerger resultMerger) {
return Recipe.builder()
...
.action(
OpenRewriteNamedRecipeAdapter.builder()
.rewriteRecipeLoader(rewriteRecipeLoader)
.resultMerger(resultMerger)
.openRewriteRecipeName(
"org.springframework.sbm.dummy.RemoveDeprecatedAnnotation"
)
.build())
...
.build();
}
}
Every Action
and every Recipe
must have a Condition
which defines if the Recipe
or Action
is applicable.
Therefore the Condition
interface must be implemented which defines a evaluate(ProjectContext)
method which must return true if the Recipe or Action is applicable and false otherwise.
A condition should only read from the ProjectContext
and never modify the AST.
public class MyCondition implements Condition {
@Override
public boolean evaluate(ProjectContext context) {
// analyze ProjectContext to evaluate condition
}
}
Projects can come as single module or multi-module project.
Working with single module projects is significantly easier because only one BuildFile
exists.
With multi-module projects the application modules play a central role and there must be means to
select a module.
A common multi-module project has a root modules which consists of one or more child modules which again can consist of multiple child modules and so on.
-
The root module
-
The application module(s), bundle all modules of an application and define the composition of deployable artifact buulding a runnable application, e.g. a war module
-
The component module(s), define reusable components which are not deployable in isolation
public void apply(ProjectContext context) {
Modules modules = context.getModules();
Module module = modules.getModule("path/of/module");
boolean isMultiModule = modules.isMultiModuleProject();
Module root = modules.getRootModule(); // type="pom" or type="ear"
modules.getApplicationModules(); // type="war", containing "main" method
modules.getComponentModules(); // type="jar" without main method
List<Module> parentModules = module.getParentModules();
List<Module> subModules = module.getSubModules();
}
The buildfiles of the scanned project are represented by BuildFile
.
The BuildFile
API offers methods to read and modify the buildfile.
BuildFile
s can be retrieved through the ProjectContext
.
// Retrieve the root build file
BuildFile rootBuildFile = projectContext.getBuildFile();
public void apply(ProjectContext context) {
// ...get buildFile for module
BuildFile buildFile = context...
Dependency dependency = Dependency.builder()
.groupId("...")
.artifactId("...")
.version("...")
.scope("test")
.build();
buildFile.addDependency(dependency);
}
A new JavaSource
must be added to a JavaSourceSet
of a given ApplicationModule
.
The default JavaSourceSet
s are 'main' (src/main/java
) and 'test' (src/test/java
).
Example: Adding a new Java class to the 'main' source set of an ApplicationModule
public void apply(ProjectContext context) {
ApplicationModule targetModule = ... // retrieve target module
String javaCode =
"package com.example.foo;\n" +
"public class Bar {}";
Path projectRootDirectory = context.getProjectRootDirectory();
targetModule.getMainJavaSourceSet().addJavaSource(projectRootDirectory, sourceFolder, src, packageName);
}
Add this snippet to your Action to use freemarker
public class MyAction extends AbstractAction {
@Autowired
@JsonIgnore
@Setter
private Configuration configuration;
// ...
}
and place your template under src/main/resources/templates
Example: using Freemarker template in Action
Map<String, String> params = new HashMap<>();
params.put("groupId", "com.example.change");
params.put("artifactId", projectName);
params.put("version", "0.1.0-SNAPSHOT");
StringWriter writer = new StringWriter();
try {
Template template = configuration.getTemplate("minimal-pom-xml.ftl");
template.process(params, writer);
} catch (TemplateException | IOException e) {
throw new RuntimeException(e);
}
String src = writer.toString();
The ProjectContext
only offers direct access to Java and BuildFile resources.
To access other resources the concept of Finder
s exists.
A Finder
implements the ResourceFinder
interface.
public interface ProjectResourceFinder<T> {
T apply(ProjectResourceSet projectResourceSet);
}
These Finder
s can than be provided to the search(…)
method of ProjectContext
.
The ProjectContext
will provide the ProjectResourceSet
to the Finder
and the Finder
can then filter/search