diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 12ec7782fb..f14b20da19 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE
Release with new features and bugfixes:
+* https://github.com/devonfw/IDEasy/issues/1031[#1031]: Added OpenRewrite commandlet
* https://github.com/devonfw/IDEasy/issues/2361[#2361]: Improve dotnet installation by setting DOTNET_ROOT
* https://github.com/devonfw/IDEasy/issues/218[#218]: Added possibility to start a command in a new window using the process builder
* https://github.com/devonfw/IDEasy/issues/788[#788]: Add support for IDE_OPTIONS variable per IDE commandlet
diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
index 79cdff16b2..0d45e767ca 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java
@@ -139,6 +139,7 @@ public CommandletManagerImpl(IdeContext context) {
add(new Npm(context));
add(new Mvn(context));
add(new Msvc(context));
+ add(new RewriteCommandlet(context));
add(new GcLogAnalyzer(context));
add(new GcViewer(context));
add(new Gradle(context));
diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RewriteCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RewriteCommandlet.java
new file mode 100644
index 0000000000..7ece1bb5f5
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RewriteCommandlet.java
@@ -0,0 +1,161 @@
+package com.devonfw.tools.ide.commandlet;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.devonfw.tools.ide.cli.CliException;
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.property.EnumProperty;
+import com.devonfw.tools.ide.tool.mvn.Mvn;
+import com.devonfw.tools.ide.tool.openrewrite.RecipeManager;
+import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapper;
+import com.devonfw.tools.ide.tool.openrewrite.RewriteRecipeEnum;
+
+/**
+ * {@link Commandlet} for OpenRewrite refactoring.
+ */
+public class RewriteCommandlet extends Commandlet {
+
+ private static final Logger LOG = LoggerFactory.getLogger(RewriteCommandlet.class);
+
+ public final EnumProperty command;
+ private final RecipeManager recipeManager;
+
+ /**
+ * The constructor.
+ *
+ * @param context the {@link IdeContext}.
+ */
+ public RewriteCommandlet(IdeContext context) {
+
+ super(context);
+ addKeyword(getName());
+ this.command = add(new EnumProperty<>("", true, "recipe_name", RewriteRecipeEnum.class));
+ this.recipeManager = new RecipeManager();
+ }
+
+ @Override
+ public String getName() {
+
+ return "rewrite";
+ }
+
+ /**
+ * Parses the raw MVN command from JSON into individual argument tokens, stripping the leading "mvn" if present.
+ *
+ * @param recipeRawCommands the raw command string from the recipe configuration.
+ * @return list of individual MVN argument tokens.
+ */
+ private List adaptMVNCommand(String recipeRawCommands) {
+ String trimmed = recipeRawCommands.trim();
+ if (trimmed.startsWith("mvn ")) {
+ trimmed = trimmed.substring(4);
+ } else if (trimmed.equals("mvn")) {
+ trimmed = "";
+ }
+ List args = new ArrayList<>();
+ for (String token : trimmed.split("\\s+")) {
+ if (!token.isEmpty()) {
+ args.add(token);
+ }
+ }
+ return args;
+ }
+
+ /**
+ * Converts a raw command to dry-run mode by replacing the ":run" goal with ":dryRun".
+ *
+ * @param recipeRawCommands the raw command string.
+ * @return the command with dry-run mode applied.
+ * @throws CliException if the command does not contain a ":run" goal to replace.
+ */
+ private String changeToDryRunCommand(String recipeRawCommands) {
+ String result = recipeRawCommands.replace(":run", ":dryRun");
+ if (result.equals(recipeRawCommands)) {
+ throw new CliException("Cannot convert to dry-run: command does not contain ':run' goal: " + recipeRawCommands);
+ }
+ return result;
+ }
+
+ private void showInfo(RecipeWrapper wrapper) {
+ LOG.info("Recipe [{}], {}", wrapper.ideasyCommand.name(), wrapper.description);
+ LOG.info("Reference {}", wrapper.url);
+ LOG.info("Raw command: {}", wrapper.rawCmd);
+ }
+
+ private boolean confirmApplyChange() {
+
+ String input = this.context.askForInput(
+ "***Before making actual changes to the code, please confirm it seriously."
+ + " It is strongly recommended to perform a DRY-RUN first***\n" +
+ "Type yes to apply changes, or press other keys to perform DRY-RUN: ");
+
+ return input.equalsIgnoreCase("yes");
+
+ }
+
+ /**
+ * Searches up from the current working directory to find the nearest pom.xml.
+ *
+ * @return the path to the project root containing pom.xml.
+ * @throws CliException if no pom.xml is found or cwd is not set.
+ */
+ Path findProjectRoot() {
+ Path dir = this.context.getCwd();
+ if (dir == null) {
+ throw new CliException("Cannot determine current working directory");
+ }
+ while (dir != null) {
+ if (Files.exists(dir.resolve("pom.xml"))) {
+ return dir;
+ }
+ dir = dir.getParent();
+ }
+ throw new CliException("No pom.xml found in current directory or any parent directory");
+ }
+
+ @Override
+ public void doRun() {
+
+ LOG.info("{} called", getClass().getSimpleName());
+
+ RewriteRecipeEnum recipeEnum = this.command.getValue();
+
+ if (!recipeManager.isValidRecipeEnum(recipeEnum)) {
+ throw new CliException("Invalid recipe name: " + recipeEnum);
+ }
+
+ RecipeWrapper wrapper = recipeManager.getRecipeWrapper(recipeEnum);
+
+ Path projectRoot = findProjectRoot();
+ LOG.info("Target project: {}", projectRoot);
+
+ showInfo(wrapper);
+
+ String commandLine = wrapper.rawCmd;
+
+ if (!confirmApplyChange()) {
+ commandLine = changeToDryRunCommand(commandLine);
+ }
+
+ LOG.info("Actual command line: {}", commandLine);
+
+ List args = adaptMVNCommand(commandLine);
+ // Inject -f so Maven runs in the correct project
+ args.add(0, "-f");
+ args.add(1, projectRoot.toString());
+ getCommandlet(Mvn.class).runTool(args);
+
+ }
+
+ @Override
+ public boolean isIdeHomeRequired() {
+
+ return false;
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java b/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java
index 8bd0714350..a5f354c90b 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java
@@ -26,6 +26,8 @@
import com.devonfw.tools.ide.tool.npm.NpmJsVersions;
import com.devonfw.tools.ide.tool.npm.NpmJsVersionsJsonDeserializer;
import com.devonfw.tools.ide.tool.npm.NpmJsVersionsJsonSerializer;
+import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapper;
+import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapperJsonDeserializer;
import com.devonfw.tools.ide.tool.pip.PypiObject;
import com.devonfw.tools.ide.tool.pip.PypiObjectJsonDeserializer;
import com.devonfw.tools.ide.tool.pip.PypiObjectJsonSerializer;
@@ -102,6 +104,8 @@ private static ObjectMapper create(boolean supportReflection) {
customModule.addDeserializer(ToolSecurity.class, new ToolSecurityJsonDeserializer());
customModule.addSerializer(Cve.class, new CveJsonSerializer());
customModule.addDeserializer(Cve.class, new CveJsonDeserializer());
+ // openrewrite mapping
+ customModule.addDeserializer(RecipeWrapper.class, new RecipeWrapperJsonDeserializer());
// pypi mapping
customModule.addDeserializer(PypiObject.class, new PypiObjectJsonDeserializer());
customModule.addSerializer(PypiObject.class, new PypiObjectJsonSerializer());
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManager.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManager.java
new file mode 100644
index 0000000000..74e9ff4b69
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManager.java
@@ -0,0 +1,72 @@
+package com.devonfw.tools.ide.tool.openrewrite;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import com.devonfw.tools.ide.cli.CliException;
+import com.devonfw.tools.ide.json.JsonMapping;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Manages the loading and lookup of OpenRewrite {@link RecipeWrapper} configurations from JSON.
+ */
+public class RecipeManager {
+
+ private static final String OPEN_REWRITE_CONFIG_JSON_PATH = "refactor/openrewrite.json";
+
+ private final Map recipes;
+
+ public RecipeManager() {
+ InputStream is = RecipeManager.class.getClassLoader().getResourceAsStream(OPEN_REWRITE_CONFIG_JSON_PATH);
+ if (is == null) {
+ throw new CliException("Failed to load " + OPEN_REWRITE_CONFIG_JSON_PATH + " from classpath");
+ }
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ ObjectMapper objectMapper = JsonMapping.create();
+ List wrapperList = objectMapper.readValue(reader, objectMapper.getTypeFactory().constructCollectionType(List.class, RecipeWrapper.class));
+
+ java.util.LinkedHashMap map = new java.util.LinkedHashMap<>();
+ for (RecipeWrapper one : wrapperList) {
+ map.put(one.ideasyCommand, one);
+ }
+ this.recipes = java.util.Collections.unmodifiableMap(map);
+ } catch (IOException e) {
+ throw new CliException("Failed to load " + OPEN_REWRITE_CONFIG_JSON_PATH, e);
+ }
+ }
+
+ /**
+ * Returns the list of all loaded {@link RecipeWrapper} configurations.
+ *
+ * @return an unmodifiable list of available recipes.
+ */
+ public List listAvailableRecipes() {
+
+ return List.copyOf(recipes.values());
+ }
+
+ /**
+ * Checks if a recipe for the given {@link RewriteRecipeEnum} exists in the configuration.
+ *
+ * @param recipeEnum the enum constant identifying the recipe.
+ * @return {@code true} if a matching recipe was found.
+ */
+ public boolean isValidRecipeEnum(RewriteRecipeEnum recipeEnum) {
+ return recipes.containsKey(recipeEnum);
+ }
+
+ /**
+ * Returns the {@link RecipeWrapper} for the given {@link RewriteRecipeEnum}.
+ *
+ * @param recipeEnum the enum constant identifying the recipe.
+ * @return the recipe wrapper.
+ */
+ public RecipeWrapper getRecipeWrapper(RewriteRecipeEnum recipeEnum) {
+ return recipes.get(recipeEnum);
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapper.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapper.java
new file mode 100644
index 0000000000..ae365c5b02
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapper.java
@@ -0,0 +1,21 @@
+package com.devonfw.tools.ide.tool.openrewrite;
+
+import com.devonfw.tools.ide.json.JsonObject;
+
+public class RecipeWrapper implements JsonObject {
+
+ public String description;
+ public String originName;
+ public String url;
+ public RewriteRecipeEnum ideasyCommand;
+ public String rawCmd;
+
+ public RecipeWrapper(String description, String originName, String url, RewriteRecipeEnum ideasyCommand, String rawCmd) {
+ this.description = description;
+ this.originName = originName;
+ this.url = url;
+ this.ideasyCommand = ideasyCommand;
+ this.rawCmd = rawCmd;
+ }
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapperJsonDeserializer.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapperJsonDeserializer.java
new file mode 100644
index 0000000000..a09d87d56b
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapperJsonDeserializer.java
@@ -0,0 +1,73 @@
+package com.devonfw.tools.ide.tool.openrewrite;
+
+import java.io.IOException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.devonfw.tools.ide.json.JsonBuilder;
+import com.devonfw.tools.ide.json.JsonObjectDeserializer;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+
+/**
+ * {@link JsonObjectDeserializer} for {@link RecipeWrapper}.
+ */
+public class RecipeWrapperJsonDeserializer extends JsonObjectDeserializer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(RecipeWrapperJsonDeserializer.class);
+
+ @Override
+ protected JsonBuilder createBuilder() {
+
+ return new RecipeWrapperBuilder();
+ }
+
+ private class RecipeWrapperBuilder extends JsonBuilder {
+
+ private String description;
+ private String originName;
+ private String url;
+ private RewriteRecipeEnum ideasyCommand;
+ private String rawCmd;
+
+ @Override
+ public void setProperty(String property, JsonParser p, DeserializationContext ctxt) throws IOException {
+
+ switch (property) {
+ case "description" -> {
+ this.description = readValueAsString(p, property, this.description);
+ }
+ case "origin_name" -> {
+ this.originName = readValueAsString(p, property, this.originName);
+ }
+ case "url" -> {
+ this.url = readValueAsString(p, property, this.url);
+ }
+ case "ideasy_command" -> {
+ String value = readValueAsString(p, property, null);
+ if (value != null) {
+ try {
+ this.ideasyCommand = RewriteRecipeEnum.valueOf(value);
+ } catch (IllegalArgumentException e) {
+ this.ideasyCommand = RewriteRecipeEnum.UNRECOGNIZED_RECIPE;
+ }
+ }
+ }
+ case "raw_cmd" -> {
+ this.rawCmd = readValueAsString(p, property, this.rawCmd);
+ }
+ default -> {
+ LOG.warn("Unknown recipe property: {}", property);
+ super.setProperty(property, p, ctxt);
+ }
+ }
+ }
+
+ @Override
+ public RecipeWrapper build() {
+
+ return new RecipeWrapper(this.description, this.originName, this.url, this.ideasyCommand, this.rawCmd);
+ }
+ }
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RewriteRecipeEnum.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RewriteRecipeEnum.java
new file mode 100644
index 0000000000..22d00a90bd
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RewriteRecipeEnum.java
@@ -0,0 +1,5 @@
+package com.devonfw.tools.ide.tool.openrewrite;
+
+public enum RewriteRecipeEnum {
+ FORMAT_JAVA_CODE, REMOVE_BLANK_LINES, UNRECOGNIZED_RECIPE
+}
diff --git a/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json b/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json
index 9d570eb1c6..5870fea20d 100644
--- a/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json
+++ b/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json
@@ -1,7 +1,8 @@
{
"resources": {
"includes": [
- {"pattern": "nls/.*"}
+ {"pattern": "nls/.*"},
+ {"pattern": "refactor/openrewrite.json"}
]
},
"bundles": [
diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties
index b13135299e..dd8b8b4580 100644
--- a/cli/src/main/resources/nls/Help.properties
+++ b/cli/src/main/resources/nls/Help.properties
@@ -125,6 +125,9 @@ cmd.release.val.args=Additional arguments to pass to the build and deploy comman
cmd.repository=Set up pre-configured git repositories using 'ide repository setup '
cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/.properties' and can therefore be shared with your project team for automatic or optional setup.
cmd.repository.val.repository=The name of the properties file of the pre-configured git repository to set up, omit to set up all active repositories.
+cmd.rewrite=Refactor existing code base with specific recipes provided by OpenRewrite
+cmd.rewrite.detail=OpenRewrite is a popular tool for refactoring (meta-programming). Detailed documentation can be found at https://docs.openrewrite.org/
+cmd.rewrite.val.recipe_name=OpenRewrite recipe names (FORMAT_JAVA_CODE|REMOVE_BLANK_LINES)
cmd.ruby=Tool commandlet for Ruby (programming language).
cmd.ruby.detail=Ruby is a dynamic, open-source programming language with a focus on simplicity and productivity. Detailed documentation can be found at https://www.ruby-lang.org/en/documentation/
cmd.ruff=Tool commandlet for Ruff.
diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties
index b87c187278..e14d531489 100644
--- a/cli/src/main/resources/nls/Help_de.properties
+++ b/cli/src/main/resources/nls/Help_de.properties
@@ -125,6 +125,9 @@ cmd.release.val.args=Zusätzliche Argumente, die an den Build- und Deploy-Befehl
cmd.repository=Richtet das vorkonfigurierte Git Repository ein mittels 'ide repository setup '.
cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup ' auf, ersetzen Sie durch den Namen Ihrer Projektkonfigurationsdatei, die sich in 'settings/repository/your_project_name' befindet und IDEasy wird Ihr Projekt basierend auf der vorhandenen Eigenschaftsdatei automatisch klonen, bauen und einrichten.\nWenn Sie den Projektnamen weglassen, werden alle im Repository-Verzeichnis gefundenen Projekte vorkonfiguriert.
cmd.repository.val.repository=Der Name der Properties-Datei des vorkonfigurierten Git Repositories zum Einrichten. Falls nicht angegeben, werden alle aktiven Projekte eingerichtet.
+cmd.rewrite=Refaktorieren Sie die vorhandene Codebasis mit spezifischen Rezepten von OpenRewrite
+cmd.rewrite.detail=OpenRewrite ist ein beliebtes Tool für Refactoring (Metaprogrammierung). Eine ausführliche Dokumentation finden Sie unter https://docs.openrewrite.org/
+cmd.rewrite.val.recipe_name=OpenRewrite Rezeptnamen (FORMAT_JAVA_CODE|REMOVE_BLANK_LINES)
cmd.ruby=Werkzeug Kommando für Ruby (Programmiersprache).
cmd.ruby.detail=Ruby ist eine dynamische, quelloffene Programmiersprache mit Fokus auf Einfachheit und Produktivität. Detaillierte Dokumentation findet sich unter https://www.ruby-lang.org/de/documentation/
cmd.ruff=Werkzeug Kommando für Ruff.
diff --git a/cli/src/main/resources/refactor/openrewrite.json b/cli/src/main/resources/refactor/openrewrite.json
new file mode 100644
index 0000000000..4ed6375340
--- /dev/null
+++ b/cli/src/main/resources/refactor/openrewrite.json
@@ -0,0 +1,16 @@
+[
+ {
+ "origin_name": "java.format_java_code",
+ "ideasy_command": "FORMAT_JAVA_CODE",
+ "description": "Format Java code using a standard comprehensive set of Java formatting recipes.",
+ "url": "https://docs.openrewrite.org/recipes/java/format/autoformat",
+ "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.AutoFormat -Drewrite.exportDatatables=true"
+ },
+ {
+ "origin_name": "java.remove_blank_lines",
+ "ideasy_command": "REMOVE_BLANK_LINES",
+ "description": "Add and/or remove blank lines.",
+ "url": "https://docs.openrewrite.org/recipes/java/format/blanklines",
+ "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.BlankLines -Drewrite.exportDatatables=true"
+ }
+]
diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/RewriteCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/RewriteCommandletTest.java
new file mode 100644
index 0000000000..5c905cbc95
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/RewriteCommandletTest.java
@@ -0,0 +1,146 @@
+package com.devonfw.tools.ide.commandlet;
+
+
+import java.nio.file.Path;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import com.devonfw.tools.ide.cli.CliException;
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeTestContext;
+import com.devonfw.tools.ide.tool.openrewrite.RecipeManager;
+import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapper;
+import com.devonfw.tools.ide.tool.openrewrite.RewriteRecipeEnum;
+
+/**
+ * Tests for {@link RewriteCommandlet}.
+ */
+class RewriteCommandletTest extends AbstractIdeContextTest {
+
+ /**
+ * Tests that the RewriteCommandlet is registered and accessible via the commandlet manager.
+ */
+ @Test
+ void testCommandletExists() {
+ IdeTestContext context = newContext("basic");
+ RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class);
+ assertThat(commandlet).isNotNull();
+ assertThat(commandlet.getName()).isEqualTo("rewrite");
+ }
+
+ /**
+ * Tests that {@link RecipeManager} correctly loads recipes and validates enums.
+ */
+ @Test
+ void testRecipeManagerLoadsRecipes() {
+ RecipeManager manager = new RecipeManager();
+ assertThat(manager.listAvailableRecipes()).isNotEmpty();
+ assertThat(manager.isValidRecipeEnum(RewriteRecipeEnum.FORMAT_JAVA_CODE)).isTrue();
+ assertThat(manager.isValidRecipeEnum(RewriteRecipeEnum.UNRECOGNIZED_RECIPE)).isFalse();
+ }
+
+ /**
+ * Tests that the dry-run conversion correctly replaces :run with :dryRun.
+ */
+ @Test
+ void testDryRunConversion() {
+ RecipeManager manager = new RecipeManager();
+ RecipeWrapper wrapper = manager.getRecipeWrapper(RewriteRecipeEnum.FORMAT_JAVA_CODE);
+ assertThat(wrapper.rawCmd).contains(":run");
+
+ String dryRunCmd = wrapper.rawCmd.replace(":run", ":dryRun");
+ assertThat(dryRunCmd).contains(":dryRun");
+ assertThat(dryRunCmd).doesNotContain(":run");
+ }
+
+ /**
+ * Tests that the dry-run conversion fails when no :run goal is present.
+ */
+ @Test
+ void testDryRunConversionFailsWithoutRunGoal() {
+ String commandWithoutRun = "mvn -U some:otherGoal -Dfoo=bar";
+ String result = commandWithoutRun.replace(":run", ":dryRun");
+ // No replacement happened
+ assertThat(result).isEqualTo(commandWithoutRun);
+ }
+
+ /**
+ * Tests that the commandlet properly parses MVN commands by stripping the "mvn" prefix.
+ */
+ @Test
+ void testAdaptMvnCommand() {
+ RecipeManager manager = new RecipeManager();
+ RecipeWrapper wrapper = manager.getRecipeWrapper(RewriteRecipeEnum.FORMAT_JAVA_CODE);
+
+ // Verify the raw command starts with "mvn"
+ assertThat(wrapper.rawCmd).startsWith("mvn");
+
+ String trimmed = wrapper.rawCmd.trim();
+ String withoutPrefix = trimmed.substring(4); // strip "mvn "
+ List args = List.of(withoutPrefix.split("\\s+"));
+ assertThat(args).isNotEmpty();
+ assertThat(args.getFirst()).startsWith("-"); // should be a Maven flag
+ }
+
+ /**
+ * Tests that ideHome is not required for the RewriteCommandlet.
+ */
+ @Test
+ void testIdeHomeNotRequired() {
+ IdeTestContext context = newContext("basic");
+ RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class);
+ assertThat(commandlet.isIdeHomeRequired()).isFalse();
+ }
+
+ /**
+ * Tests that {@link RewriteCommandlet#doRun()} throws a {@link CliException} when an invalid recipe enum is provided, exercising the error handling path.
+ */
+ @Test
+ void testDoRunThrowsForInvalidRecipe() {
+ IdeTestContext context = newContext("basic");
+ RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class);
+ commandlet.command.setValue(RewriteRecipeEnum.UNRECOGNIZED_RECIPE);
+ assertThatThrownBy(commandlet::doRun).isInstanceOf(CliException.class).hasMessageContaining("Invalid recipe name");
+ }
+
+ /**
+ * Tests that {@link RewriteCommandlet#findProjectRoot()} locates the nearest pom.xml when the current working directory contains one.
+ */
+ @Test
+ void testFindProjectRootFindsPomInCurrentDirectory() {
+ IdeTestContext context = newContext("release");
+ RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class);
+ // mvn directory contains pom.xml
+ Path mvnDir = context.getWorkspacePath().resolve("mvn");
+ context.setCwd(mvnDir, "main", context.getIdeHome());
+ Path projectRoot = commandlet.findProjectRoot();
+ assertThat(projectRoot).isEqualTo(mvnDir);
+ }
+
+ /**
+ * Tests that {@link RewriteCommandlet#findProjectRoot()} walks up the directory tree to find the nearest pom.xml.
+ */
+ @Test
+ void testFindProjectRootWalksUpDirectoryTree() {
+ IdeTestContext context = newContext("release");
+ RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class);
+ // empty directory does NOT contain pom.xml - should walk up to find it
+ Path emptyDir = context.getWorkspacePath().resolve("empty");
+ context.setCwd(emptyDir, "main", context.getIdeHome());
+ Path projectRoot = commandlet.findProjectRoot();
+ // Should find the pom.xml in the parent workspaces/main directory or above
+ assertThat(projectRoot.resolve("pom.xml")).exists();
+ }
+
+ /**
+ * Tests that {@link RewriteCommandlet#findProjectRoot()} throws when cwd is not set.
+ */
+ @Test
+ void testFindProjectRootThrowsWhenNoCwd() {
+ IdeTestContext context = newContext("basic");
+ RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class);
+ context.setCwd(null, "main", context.getIdeHome());
+ assertThatThrownBy(commandlet::findProjectRoot).isInstanceOf(CliException.class).hasMessageContaining("current working directory");
+ }
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManagerTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManagerTest.java
new file mode 100644
index 0000000000..5a5e5b4ad8
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManagerTest.java
@@ -0,0 +1,31 @@
+package com.devonfw.tools.ide.tool.openrewrite;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link RecipeManager}.
+ */
+class RecipeManagerTest extends Assertions {
+
+ static RecipeManager manager;
+
+ @BeforeAll
+ static void init() {
+ manager = new RecipeManager();
+ }
+
+ @Test
+ void testCreation() {
+ assertThat(manager.listAvailableRecipes()).isNotEmpty();
+ }
+
+ @Test
+ void testEnumValidation() {
+ assertThat(manager.isValidRecipeEnum(RewriteRecipeEnum.UNRECOGNIZED_RECIPE)).isFalse();
+ var validRecipe = java.util.Arrays.stream(RewriteRecipeEnum.values())
+ .filter(x -> x != RewriteRecipeEnum.UNRECOGNIZED_RECIPE).findAny().get();
+ assertThat(manager.isValidRecipeEnum(validRecipe)).isTrue();
+ }
+}
diff --git a/cli/src/test/resources/refactor/openrewrite.json b/cli/src/test/resources/refactor/openrewrite.json
new file mode 100644
index 0000000000..4ed6375340
--- /dev/null
+++ b/cli/src/test/resources/refactor/openrewrite.json
@@ -0,0 +1,16 @@
+[
+ {
+ "origin_name": "java.format_java_code",
+ "ideasy_command": "FORMAT_JAVA_CODE",
+ "description": "Format Java code using a standard comprehensive set of Java formatting recipes.",
+ "url": "https://docs.openrewrite.org/recipes/java/format/autoformat",
+ "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.AutoFormat -Drewrite.exportDatatables=true"
+ },
+ {
+ "origin_name": "java.remove_blank_lines",
+ "ideasy_command": "REMOVE_BLANK_LINES",
+ "description": "Add and/or remove blank lines.",
+ "url": "https://docs.openrewrite.org/recipes/java/format/blanklines",
+ "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.BlankLines -Drewrite.exportDatatables=true"
+ }
+]