Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
deb7f46
add: add help information for the rewrite command
yurii-yu Feb 26, 2025
0b18f92
add: add help information for the rewrite command
yurii-yu Mar 3, 2025
2eecccc
add: add help information for the rewrite command
yurii-yu Mar 6, 2025
d746c6a
add: add help information for the rewrite command
yurii-yu Mar 6, 2025
a7a0312
update: add necessary commandlet and property
yurii-yu Mar 6, 2025
6e5baf1
Merge remote-tracking branch 'refs/remotes/origin/main' into feature/…
yurii-yu Apr 25, 2025
1568f82
Merge branch 'main' into feature/1031_intergrate_with_openrewrite
yurii-yu Apr 25, 2025
1963978
Merge branch 'main' into feature/1031_intergrate_with_openrewrite
jan-vcapgemini Apr 28, 2025
276d69f
Merge branch 'main' into feature/1031_intergrate_with_openrewrite
yurii-yu May 21, 2025
9212855
Merge remote-tracking branch 'origin/feature/1031_intergrate_with_ope…
yurii-yu May 21, 2025
6ea329a
Merge branch 'main' into feature/1031_intergrate_with_openrewrite
hohwille Aug 22, 2025
6970443
add: help message for RefactorCommandlet
yurii-yu Aug 26, 2025
10f82d7
add: wrapper of the configurable recipe
yurii-yu Aug 26, 2025
5e29a3a
update: two configuration files (more recipes can be added in this way)
yurii-yu Aug 26, 2025
3f843e3
update: keep names consistent with the names in the config file
yurii-yu Aug 26, 2025
faa40be
add: placeholder enum
yurii-yu Aug 26, 2025
35eb98c
update: finish the implementation
yurii-yu Aug 26, 2025
7bffb92
update: finish the feature
yurii-yu Aug 26, 2025
631037c
Merge branch 'main' into feature/1031_intergrate_with_openrewrite
yurii-yu Oct 24, 2025
c32d2a1
fix: fix failed errors by correcting the text
yurii-yu Oct 24, 2025
208291f
Merge remote-tracking branch 'upstream' into feature/1031_integrate_w…
samuelkos17 Aug 3, 2026
ad90c44
#1031: Updated the OpenRewrite implementation to work with current ve…
samuelkos17 Aug 7, 2026
3be68a7
Merge remote-tracking branch 'upstream' into feature/1031_integrate_w…
samuelkos17 Aug 7, 2026
cdd87e4
#1031: Refactored the Openrewrite implementation to fit standards and…
samuelkos17 Aug 10, 2026
6444a39
Merge remote-tracking branch 'origin' into feature/1031_integrate_wit…
samuelkos17 Aug 10, 2026
c58b4b1
#1031: applied spotless
samuelkos17 Aug 10, 2026
d67ab84
Merge branch 'main' into feature/1031_integrate_with_openrewrite
maybeec Aug 16, 2026
c849851
#1031: Code clean up
samuelkos17 Aug 18, 2026
46b3da6
Merge branch 'main' into feature/1031_integrate_with_openrewrite
samuelkos17 Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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 <a href="https://docs.openrewrite.org/">OpenRewrite</a> refactoring.
*/
public class RewriteCommandlet extends Commandlet {

private static final Logger LOG = LoggerFactory.getLogger(RewriteCommandlet.class);

public final EnumProperty<RewriteRecipeEnum> 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<String> adaptMVNCommand(String recipeRawCommands) {
String trimmed = recipeRawCommands.trim();
if (trimmed.startsWith("mvn ")) {
trimmed = trimmed.substring(4);
} else if (trimmed.equals("mvn")) {
trimmed = "";
}
List<String> 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);

try {
List<String> args = adaptMVNCommand(commandLine);
// Inject -f <project-root> so Maven runs in the correct project
args.add(0, "-f");
args.add(1, projectRoot.toString());
getCommandlet(Mvn.class).runTool(args);
} catch (Exception e) {
Comment thread
samuelkos17 marked this conversation as resolved.
Outdated
throw new CliException("OpenRewrite execution failed for recipe '" + wrapper.ideasyCommand.name() + "': " + e.getMessage(), e);
}
}

@Override
public boolean isIdeHomeRequired() {

return false;
}
}
4 changes: 4 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.devonfw.tools.ide.tool.openrewrite;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

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<RewriteRecipeEnum, RecipeWrapper> recipes;

public RecipeManager() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
Objects.requireNonNull(RecipeManager.class.getClassLoader().getResourceAsStream(OPEN_REWRITE_CONFIG_JSON_PATH)), StandardCharsets.UTF_8))) {
Comment thread
samuelkos17 marked this conversation as resolved.
Outdated
ObjectMapper objectMapper = JsonMapping.create();
List<RecipeWrapper> wrapperList = objectMapper.readValue(reader, objectMapper.getTypeFactory().constructCollectionType(List.class, RecipeWrapper.class));

java.util.LinkedHashMap<RewriteRecipeEnum, RecipeWrapper> 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<RecipeWrapper> listAvailableRecipes() {

return List.copyOf(recipes.values());
}

private Optional<RecipeWrapper> findRecipeByName(String rawName) {
return recipes.values().stream().filter(x -> x.originName.equals(rawName)).findFirst();
}

/**
* Checks if a recipe with the given original OpenRewrite name exists in the configuration.
*
* @param rawName the original recipe name (e.g. {@code "java.format_autoformat"}).
* @return {@code true} if a matching recipe was found.
*/
public boolean isValidRecipeNameRawName(String rawName) {
return findRecipeByName(rawName).isPresent();
}
Comment thread
samuelkos17 marked this conversation as resolved.
Outdated

/**
* 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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;
}

//in case of future need
public String getName() {
Comment thread
samuelkos17 marked this conversation as resolved.
Outdated
return this.originName;
}

}
Original file line number Diff line number Diff line change
@@ -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<RecipeWrapper> {

private static final Logger LOG = LoggerFactory.getLogger(RecipeWrapperJsonDeserializer.class);

@Override
protected JsonBuilder<RecipeWrapper> createBuilder() {

return new RecipeWrapperBuilder();
}

private class RecipeWrapperBuilder extends JsonBuilder<RecipeWrapper> {

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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.devonfw.tools.ide.tool.openrewrite;

public enum RewriteRecipeEnum {
FORMAT_JAVA_CODE, REMOVE_BLANK_LINES, UNRECOGNIZED_RECIPE
}
Loading