Minimize size of messages for fetching and executing recipes

This commit is contained in:
aboyko
2023-12-18 20:54:59 -05:00
parent f74f3ec291
commit d6ff60a96e
11 changed files with 449 additions and 287 deletions

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src/"/>
<classpathentry kind="output" path="target/classes"/>

View File

@@ -1,10 +1,10 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
org.eclipse.jdt.core.compiler.compliance=11
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=11
org.eclipse.jdt.core.compiler.source=17

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* Copyright (c) 2022, 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -10,12 +10,16 @@
*******************************************************************************/
package org.springframework.tooling.boot.ls.commands;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.Set;
class RecipeDescriptor {
final class RecipeDescriptor {
enum CheckedState {
UNCHECKED,
CHECKED,
GRAYED
}
String name;
@@ -25,45 +29,29 @@ class RecipeDescriptor {
Set<String> tags;
Duration estimatedEffortPerOccurrence;
List<OptionDescriptor> options;
List<String> languages;
List<RecipeDescriptor> recipeList;
URI source;
RecipeDescriptor getCopyWithoutSubRecipes() {
RecipeDescriptor copy = new RecipeDescriptor();
copy.name = name;
copy.displayName = displayName;
copy.description = description;
copy.tags = tags;
copy.estimatedEffortPerOccurrence = estimatedEffortPerOccurrence;
copy.options = options;
copy.languages = languages;
copy.source = source;
return copy;
}
static class OptionDescriptor {
boolean hasSubRecipes = false;
RecipeDescriptor parent;
CheckedState checked = CheckedState.UNCHECKED;
String name;
String type;
String displayName;
String description;
String example;
List<String> valid;
boolean required;
Object value;
}
record OptionDescriptor(
String name,
String type,
String displayName,
String description,
String example,
List<String> valid,
boolean required,
Object value
) {}
record RecipeSelection(boolean selected, String id, RecipeSelection[] subselection) {}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* Copyright (c) 2022, 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -10,55 +10,48 @@
*******************************************************************************/
package org.springframework.tooling.boot.ls.commands;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Status;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.eclipse.lsp4j.services.WorkspaceService;
import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.CheckedState;
import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.RecipeSelection;
public class RecipeTreeModel {
public enum CheckedState {
UNCHECKED,
CHECKED,
GRAYED
}
private static final String REWRITE_REFACTORINGS_LIST = "sts/rewrite/list";
private static final String REWRITE_REFACTORINGS_SUBLIST = "sts/rewrite/sublist";
private RecipeDescriptor[] recipeDescriptors;
private Map<RecipeDescriptor, RecipeDescriptor> parentMap = new IdentityHashMap<>();
private Map<RecipeDescriptor, CheckedState> checkedMap = new IdentityHashMap<>();
RecipeTreeModel(RecipeDescriptor[] recipeDescriptors) {
this.recipeDescriptors = recipeDescriptors;
for (RecipeDescriptor d : recipeDescriptors) {
initParentMap(d);
}
}
private void initParentMap(RecipeDescriptor d) {
if (d.recipeList != null) {
for (RecipeDescriptor dc : d.recipeList) {
parentMap.put(dc, d);
initParentMap(dc);
}
}
final private WorkspaceService workspaceService;
final private String recipeFilter;
RecipeTreeModel(WorkspaceService workspaceService, String recipeFilter) {
this.workspaceService = workspaceService;
this.recipeFilter = recipeFilter;
}
public void check(RecipeDescriptor d) {
if (simpleCheck(d)) {
inferCheckedStateFromChildren(parentMap.get(d));
inferCheckedStateFromChildren(d.parent);
}
}
private boolean simpleCheck(RecipeDescriptor d) {
if (checkedMap.get(d) != CheckedState.CHECKED) {
checkedMap.put(d, CheckedState.CHECKED);
for (RecipeDescriptor dc : d.recipeList) {
simpleCheck(dc);
if (d.checked != CheckedState.CHECKED) {
d.checked = CheckedState.CHECKED;
if (d.recipeList != null) {
for (RecipeDescriptor dc : d.recipeList) {
simpleCheck(dc);
}
}
return true;
}
@@ -67,32 +60,29 @@ public class RecipeTreeModel {
public void uncheck(RecipeDescriptor d) {
if (simpleUncheck(d)) {
inferCheckedStateFromChildren(parentMap.get(d));
inferCheckedStateFromChildren(d.parent);
}
}
private boolean simpleUncheck(RecipeDescriptor d) {
if (checkedMap.get(d) != CheckedState.UNCHECKED) {
checkedMap.put(d, CheckedState.UNCHECKED);
for (RecipeDescriptor dc : d.recipeList) {
simpleUncheck(dc);
if (d.checked != CheckedState.UNCHECKED) {
d.checked = CheckedState.UNCHECKED;
if (d.recipeList != null) {
for (RecipeDescriptor dc : d.recipeList) {
simpleUncheck(dc);
}
}
return true;
}
return false;
}
public CheckedState getCheckedState(RecipeDescriptor d) {
CheckedState state = checkedMap.get(d);
return state == null ? CheckedState.UNCHECKED : state;
}
private void inferCheckedStateFromChildren(RecipeDescriptor d) {
if (d != null && d.recipeList != null) {
boolean all = true;
boolean none = true;
for (RecipeDescriptor child : d.recipeList) {
CheckedState childState = getCheckedState(child);
CheckedState childState = child.checked;
if (childState == CheckedState.UNCHECKED) {
all = false;
} else {
@@ -105,9 +95,9 @@ public class RecipeTreeModel {
} else if (none) {
inferredState = CheckedState.UNCHECKED;
}
if (getCheckedState(d) != inferredState) {
checkedMap.put(d, inferredState);
inferCheckedStateFromChildren(parentMap.get(d));
if (d.checked != inferredState) {
d.checked = inferredState;
inferCheckedStateFromChildren(d.parent);
}
}
}
@@ -116,32 +106,70 @@ public class RecipeTreeModel {
return recipeDescriptors;
}
public RecipeDescriptor getSelectedRecipeDescriptors() throws CoreException {
RecipeDescriptor[] recipes = Arrays.stream(recipeDescriptors).map(this::copySelectedDescriptor).filter(Objects::nonNull).toArray(RecipeDescriptor[]::new);
if (recipes.length == 0) {
throw new CoreException(Status.error("No recipes selected"));
} else if (recipes.length == 1) {
return recipes[0];
} else {
RecipeDescriptor aggregate = new RecipeDescriptor();
aggregate.name = recipes.length + " recipes";
aggregate.displayName = recipes.length + " recipes";
aggregate.description = "Multiple recipes to be applied. Number of recipes " + recipes.length;
aggregate.tags = Arrays.stream(recipes).flatMap(r -> r.tags.stream()).collect(Collectors.toSet());
aggregate.recipeList = Arrays.asList(recipes);
return aggregate;
public RecipeSelection[] getRecipeSelection() throws CoreException {
List<RecipeSelection> rootSelected = new ArrayList<>();
for (int i = 0; i < recipeDescriptors.length; i++) {
if (recipeDescriptors[i].checked != CheckedState.UNCHECKED) {
rootSelected.add(new RecipeSelection(true, recipeDescriptors[i].name, createRecipeSelection(recipeDescriptors[i])));
}
}
if (rootSelected.isEmpty()) {
throw new CoreException(Status.error("No recipes selected"));
}
return rootSelected.toArray(new RecipeSelection[rootSelected.size()]);
}
private RecipeDescriptor copySelectedDescriptor(RecipeDescriptor d) {
if (getCheckedState(d) != CheckedState.UNCHECKED) {
RecipeDescriptor copy = d.getCopyWithoutSubRecipes();
if (d.recipeList != null) {
copy.recipeList = d.recipeList.stream().map(this::copySelectedDescriptor).filter(Objects::nonNull).collect(Collectors.toList());
}
return copy;
private RecipeSelection[] createRecipeSelection(RecipeDescriptor d) {
if (d.recipeList != null) {
return d.recipeList.stream()
.map(s -> new RecipeSelection(s.checked != CheckedState.UNCHECKED, s.name, createRecipeSelection(s)))
.toArray(RecipeSelection[]::new);
}
return null;
}
CompletableFuture<Void> fetchSubrecipes(RecipeDescriptor descriptor) {
RecipeDescriptor d = descriptor;
LinkedList<Integer> indexPath = new LinkedList<>();
for (; d.parent != null; d = d.parent) {
indexPath.addFirst(d.parent.recipeList.indexOf(d));
}
ExecuteCommandParams commandParams = new ExecuteCommandParams();
commandParams.setCommand(REWRITE_REFACTORINGS_SUBLIST);
commandParams.setArguments(List.of(d.name, indexPath));
return workspaceService.executeCommand(commandParams).thenAccept(json -> {
RecipeDescriptor[] fetchedDescriptors = RewriteRefactoringsHandler.SERIALIZATION_GSON.fromJson(RewriteRefactoringsHandler.SERIALIZATION_GSON.toJson(json), RecipeDescriptor[].class);
for (RecipeDescriptor fd : fetchedDescriptors) {
fd.parent = descriptor;
fd.checked = descriptor.checked != CheckedState.UNCHECKED ? CheckedState.CHECKED : CheckedState.UNCHECKED;
}
descriptor.recipeList = Arrays.asList(fetchedDescriptors);
});
}
CompletableFuture<Void> fetchRootRecipes() {
ExecuteCommandParams commandParams = new ExecuteCommandParams();
commandParams.setCommand(REWRITE_REFACTORINGS_LIST);
commandParams.setArguments(List.of(recipeFilter));
return workspaceService.executeCommand(commandParams).thenAccept(json -> {
recipeDescriptors = RewriteRefactoringsHandler.SERIALIZATION_GSON.fromJson(RewriteRefactoringsHandler.SERIALIZATION_GSON.toJson(json), RecipeDescriptor[].class);
});
}
String getSelectedRecipeDisplayName() {
List<RecipeDescriptor> rootSelected = new ArrayList<>();
for (int i = 0; i < recipeDescriptors.length; i++) {
if (recipeDescriptors[i].checked != CheckedState.UNCHECKED) {
rootSelected.add(recipeDescriptors[i]);
}
}
if (rootSelected.isEmpty()) {
return "No Recipes Selected";
} else if (rootSelected.size() == 1) {
return rootSelected.get(0).displayName;
} else {
return "%s recipes".formatted(rootSelected.size());
}
}
}

View File

@@ -11,10 +11,6 @@
package org.springframework.tooling.boot.ls.commands;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.commands.AbstractHandler;
@@ -37,17 +33,11 @@ import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;
import org.springframework.tooling.boot.ls.BootLanguageServerPlugin;
import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.RecipeSelection;
import org.springsource.ide.eclipse.commons.core.CoreUtil;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
@SuppressWarnings("restriction")
public class RewriteRefactoringsHandler extends AbstractHandler {
@@ -58,25 +48,11 @@ public class RewriteRefactoringsHandler extends AbstractHandler {
NON_BOOT_UPGRADE
}
private static class DurationTypeConverter implements JsonSerializer<Duration>, JsonDeserializer<Duration> {
@Override
public JsonElement serialize(Duration src, Type srcType, JsonSerializationContext context) {
return new JsonPrimitive(src.toNanos());
}
@Override
public Duration deserialize(JsonElement json, Type type, JsonDeserializationContext context)
throws JsonParseException {
return Duration.ofNanos(json.getAsLong());
}
}
private static Gson serializationGson = new GsonBuilder()
.registerTypeAdapter(Duration.class, new DurationTypeConverter())
static final Gson SERIALIZATION_GSON = new GsonBuilder()
.setPrettyPrinting()
.create();
private static final String REWRITE_REFACTORINGS_LIST = "sts/rewrite/list";
private static final String REWRITE_REFACTORINGS_EXEC = "sts/rewrite/execute";
private RecipeFilter recipeFilter;
@@ -106,41 +82,26 @@ public class RewriteRefactoringsHandler extends AbstractHandler {
if (project != null && CoreUtil.promptForProjectSave(project)) {
LanguageServerDefinition def = LanguageServersRegistry.getInstance().getDefinition(BootLanguageServerPlugin.BOOT_LS_DEFINITION_ID);
Assert.isLegal(def != null, "No definition found for Boot Language Server");
final String uri = project.getLocationURI().toASCIIString();
ExecuteCommandParams commandParams = new ExecuteCommandParams();
commandParams.setCommand(REWRITE_REFACTORINGS_LIST);
commandParams.setArguments(List.of(uri, recipeFilter.toString()));
try {
List<Object> allRewriteRecipesJson = new ArrayList<>();
List<Object> syncRecipesJson = Collections.synchronizedList(allRewriteRecipesJson);
LanguageServers.forProject(project).withPreferredServer(def).computeFirst(ls ->
ls.getWorkspaceService().executeCommand(commandParams).thenAccept(or -> {
if (or != null) {
syncRecipesJson.add(or);
}
})
.thenRun(() -> {
allRewriteRecipesJson.stream().filter(List.class::isInstance).map(List.class::cast).findFirst().ifPresent(obj -> {
RecipeDescriptor[] descriptors = serializationGson.fromJson(serializationGson.toJson(obj), RecipeDescriptor[].class);
LanguageServers.forProject(project).withPreferredServer(def).computeFirst(ls -> {
PlatformUI.getWorkbench().getDisplay().asyncExec(() -> {
RecipeTreeModel recipesModel = new RecipeTreeModel(descriptors);
RecipeTreeModel recipesModel = new RecipeTreeModel(ls.getWorkspaceService(), recipeFilter.toString());
int returnCode = new SelectRecipesDialog(Display.getCurrent().getActiveShell(), recipesModel).open();
if (returnCode == Window.OK) {
try {
RecipeDescriptor recipeToApply = recipesModel.getSelectedRecipeDescriptors();
final RecipeSelection[] recipeSelection = recipesModel.getRecipeSelection();
PlatformUI.getWorkbench().getProgressService().run(true, false, monitor -> {
try {
monitor.beginTask("Applying recipe '" + recipeToApply.displayName + "'", IProgressMonitor.UNKNOWN);
monitor.beginTask("Applying recipe '%s'...".formatted(recipesModel.getSelectedRecipeDisplayName()), IProgressMonitor.UNKNOWN);
ExecuteCommandParams cmdParams = new ExecuteCommandParams();
cmdParams.setCommand(REWRITE_REFACTORINGS_EXEC);
cmdParams.setArguments(List.of(
uri,
serializationGson.toJsonTree(recipeToApply)
SERIALIZATION_GSON.toJsonTree(recipeSelection)
));
ls.getWorkspaceService().executeCommand(cmdParams).get();
@@ -157,8 +118,9 @@ public class RewriteRefactoringsHandler extends AbstractHandler {
}
}
});
});
}));
return null;
});
} catch (Exception e) {
throw new ExecutionException("Failed to apply Rewrite recipe(s)", e);
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022 VMware, Inc.
* Copyright (c) 2022, 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -43,7 +43,7 @@ import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.osgi.framework.FrameworkUtil;
import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.OptionDescriptor;
import org.springframework.tooling.boot.ls.commands.RecipeTreeModel.CheckedState;
import org.springframework.tooling.boot.ls.commands.RecipeDescriptor.CheckedState;
@SuppressWarnings("restriction")
public class SelectRecipesDialog extends StatusDialog {
@@ -51,6 +51,7 @@ public class SelectRecipesDialog extends StatusDialog {
private static final int MARGIN = 5;
private static final String SELECT_REWRITE_RECIPE_S_FROM_THE_LIST = "Select Rewrite Recipe(s) from the list";
private static String fgStyleSheet;
private static final Object LOADING = new Object();
private RecipeTreeModel model;
@@ -76,37 +77,55 @@ public class SelectRecipesDialog extends StatusDialog {
@Override
public Object[] getElements(Object inputElement) {
if (inputElement instanceof RecipeTreeModel) {
return ((RecipeTreeModel) inputElement).getRecipeDescriptors();
if (inputElement instanceof RecipeTreeModel model) {
if (model.getRecipeDescriptors() == null) {
model.fetchRootRecipes().thenAccept(v -> {
PlatformUI.getWorkbench().getDisplay().asyncExec(() -> treeViewer.refresh());
});
return new Object[] { LOADING };
} else {
return model.getRecipeDescriptors();
}
}
return new Object[0];
}
@Override
public Object[] getChildren(Object parentElement) {
if (parentElement instanceof RecipeDescriptor) {
RecipeDescriptor r = (RecipeDescriptor) parentElement;
return r.recipeList.toArray(new RecipeDescriptor[r.recipeList.size()]);
if (parentElement instanceof RecipeDescriptor r) {
if (r.hasSubRecipes) {
if (r.recipeList == null) {
model.fetchSubrecipes(r).thenAccept(v -> PlatformUI.getWorkbench().getDisplay().asyncExec(() -> treeViewer.refresh(r)));
return new Object[] { LOADING };
} else {
return r.recipeList.toArray(new RecipeDescriptor[r.recipeList.size()]);
}
}
}
return new RecipeDescriptor[0];
}
@Override
public Object getParent(Object element) {
if (element instanceof RecipeDescriptor r) {
return r.parent;
}
return null;
}
@Override
public boolean hasChildren(Object element) {
if (element instanceof RecipeDescriptor) {
RecipeDescriptor r = (RecipeDescriptor) element;
return r.recipeList != null && !r.recipeList.isEmpty();
if (element instanceof RecipeDescriptor r) {
return r.hasSubRecipes;
}
return false;
}
});
treeViewer.setLabelProvider(LabelProvider.createTextProvider(input -> {
if (input instanceof RecipeDescriptor) {
if (input == LOADING) {
return "Loading...";
} else if (input instanceof RecipeDescriptor) {
return ((RecipeDescriptor)input).displayName;
}
return "unknown";
@@ -117,7 +136,7 @@ public class SelectRecipesDialog extends StatusDialog {
public boolean isGrayed(Object element) {
if (element instanceof RecipeDescriptor) {
RecipeDescriptor r = (RecipeDescriptor) element;
return model.getCheckedState(r) == CheckedState.GRAYED;
return r.checked == CheckedState.GRAYED;
}
return false;
}
@@ -126,7 +145,7 @@ public class SelectRecipesDialog extends StatusDialog {
public boolean isChecked(Object element) {
if (element instanceof RecipeDescriptor) {
RecipeDescriptor r = (RecipeDescriptor) element;
return model.getCheckedState(r) != CheckedState.UNCHECKED;
return r.checked != CheckedState.UNCHECKED;
}
return false;
}
@@ -168,7 +187,7 @@ public class SelectRecipesDialog extends StatusDialog {
// Replace browser's built-in context menu with none
docViewer.setMenu(new Menu(getShell(), SWT.NONE));
docViewer.setText(wrapHtml("Select a Recipe on the left to read description"));
docViewer.setText(wrapHtml("Select a Recipe on the left to read description"));
treeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
@@ -200,7 +219,7 @@ public class SelectRecipesDialog extends StatusDialog {
}
private void updateStatus() {
boolean anythingSelected = Arrays.stream(model.getRecipeDescriptors()).anyMatch(d -> model.getCheckedState(d) != CheckedState.UNCHECKED);
boolean anythingSelected = model.getRecipeDescriptors() != null && Arrays.stream(model.getRecipeDescriptors()).anyMatch(d -> d.checked != CheckedState.UNCHECKED);
updateStatus(anythingSelected ? Status.info(SELECT_REWRITE_RECIPE_S_FROM_THE_LIST) : Status.error(SELECT_REWRITE_RECIPE_S_FROM_THE_LIST));
}
@@ -211,12 +230,12 @@ public class SelectRecipesDialog extends StatusDialog {
sb.append("</p>");
sb.append("<ul>");
for (OptionDescriptor option : r.options) {
if (option.value != null) {
if (option.value() != null) {
sb.append("<li>");
sb.append("<pre>");
sb.append(option.value);
sb.append(option.value());
sb.append("</pre>");
sb.append(option.description);
sb.append(option.description());
sb.append("</li>");
}
}

View File

@@ -52,12 +52,18 @@ public class LoadUtils {
}
public static Recipe createRecipe(RecipeDescriptor d, Function<String, Class<? extends Recipe>> getRecipeClass) {
return createRecipe(d, getRecipeClass, false);
}
public static Recipe createRecipe(RecipeDescriptor d, Function<String, Class<? extends Recipe>> getRecipeClass, boolean shallow) {
Class<? extends Recipe> recipeClazz = getRecipeClass == null ? null : getRecipeClass.apply(d.getName());
if (recipeClazz == null || DeclarativeRecipe.class.getName().equals(recipeClazz.getName())) {
DeclarativeRecipe recipe = new DeclarativeRecipe(d.getName(), d.getDisplayName(), d.getDescription(),
d.getTags(), d.getEstimatedEffortPerOccurrence(), d.getSource(), false, d.getMaintainers());
for (RecipeDescriptor subDescriptor : d.getRecipeList()) {
recipe.getRecipeList().add(createRecipe(subDescriptor, getRecipeClass));
if (!shallow) {
for (RecipeDescriptor subDescriptor : d.getRecipeList()) {
recipe.getRecipeList().add(createRecipe(subDescriptor, getRecipeClass));
}
}
return recipe;
} else {

View File

@@ -18,6 +18,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -72,6 +73,7 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
public class RewriteRecipeRepository {
@@ -99,6 +101,7 @@ public class RewriteRecipeRepository {
private static final String CMD_REWRITE_RELOAD = "sts/rewrite/reload";
private static final String CMD_REWRITE_EXECUTE = "sts/rewrite/execute";
private static final String CMD_REWRITE_LIST = "sts/rewrite/list";
private static final String CMD_REWRITE_SUBLIST = "sts/rewrite/sublist";
private static final String CMD_REWRITE_RECIPE_EXECUTE = "sts/rewrite/recipe/execute";
private static final Logger log = LoggerFactory.getLogger(RewriteRecipeRepository.class);
@@ -116,8 +119,9 @@ public class RewriteRecipeRepository {
private Set<String> scanDirs;
private Set<String> recipeFilters;
private static Gson serializationGson = new GsonBuilder()
static final Gson serializationGson = new GsonBuilder()
.registerTypeAdapter(Duration.class, new DurationTypeConverter())
.setPrettyPrinting()
.create();
public RewriteRecipeRepository(SimpleLanguageServer server, JavaProjectFinder projectFinder, BootJavaConfig config) {
@@ -265,40 +269,117 @@ public class RewriteRecipeRepository {
}
private static JsonElement recipeToJson(Recipe r) {
JsonElement jsonElement = serializationGson.toJsonTree(r.getDescriptor());
RecipeDescriptor descriptor = r.getDescriptor();
JsonElement jsonElement = serializationGson.toJsonTree(Map.of(
"name", descriptor.getName(),
"displayName", descriptor.getDisplayName(),
"description", descriptor.getDescription(),
"options", descriptor.getOptions(),
"tags", descriptor.getTags(),
"hasSubRecipes", !descriptor.getRecipeList().isEmpty()
));
return jsonElement;
}
CompletableFuture<List<Recipe>> getRootRecipes(Predicate<Recipe> rootFilter) {
return recipes().thenApply(recipesMap -> recipesMap.values().stream().filter(rootFilter).collect(Collectors.toList()));
}
CompletableFuture<List<Recipe>> getSubRecipes(String rootRecipeId, List<Integer> path) {
return recipes().thenApply(recipesMap -> {
Recipe recipe = recipesMap.get(rootRecipeId);
for (int i : path) {
if (i < recipe.getRecipeList().size()) {
recipe = recipe.getRecipeList().get(i);
} else {
return Collections.emptyList();
}
}
return recipe == null ? Collections.emptyList() : recipe.getRecipeList();
});
}
Recipe createRecipeFromSelection(Recipe original, RecipeSelectionDescriptor[] selection) {
if (selection == null) {
return original;
} else {
boolean sameSubrecipes = true;
List<Recipe> newSubRecipes = new ArrayList<>(selection.length);
for (int i = 0; i < selection.length; i++) {
if (selection[i].selected) {
Recipe originalSubRecipe = original.getRecipeList().get(i);
Recipe newSubRecipe = createRecipeFromSelection(originalSubRecipe, selection[i].subselection());
newSubRecipes.add(newSubRecipe);
if (sameSubrecipes) {
sameSubrecipes = newSubRecipe == originalSubRecipe;
}
} else {
sameSubrecipes = false;
}
}
if (sameSubrecipes) {
return original;
} else {
@SuppressWarnings("unchecked")
Recipe newRecipe = LoadUtils.createRecipe(original.getDescriptor(), id -> {
try {
return (Class<Recipe>) Class.forName(id);
} catch (ClassNotFoundException e) {
return null;
}
}, true);
newRecipe.getRecipeList().addAll(newSubRecipes);
return newRecipe;
}
}
}
private void registerCommands() {
server.onCommand(CMD_REWRITE_LIST, params -> {
JsonElement uri = (JsonElement) params.getArguments().get(0);
RecipeFilter f = params.getArguments().size() > 1 ? RecipeFilter.valueOf(((JsonElement) params.getArguments().get(1)).getAsString()) : RecipeFilter.ALL;
return listProjectRefactoringRecipes(uri.getAsString()).thenApply(recipes -> recipes.stream()
.filter(RECIPE_LIST_FILTERS.get(f))
.map(RewriteRecipeRepository::recipeToJson)
.collect(Collectors.toList()));
RecipeFilter f = params.getArguments().size() > 0 ? RecipeFilter.valueOf(((JsonElement) params.getArguments().get(0)).getAsString()) : RecipeFilter.ALL;
return getRootRecipes(r -> isAcceptableGlobalCommandRecipe(r) && RECIPE_LIST_FILTERS.get(f).test(r)).thenApply(recipes -> recipes.stream()
.map(RewriteRecipeRepository::recipeToJson)
.collect(Collectors.toList()));
});
server.onCommand(CMD_REWRITE_SUBLIST, params -> {
String rootRecipeId = ((JsonElement) params.getArguments().get(0)).getAsString();
JsonArray path = (JsonArray) params.getArguments().get(1);
return getSubRecipes(rootRecipeId, path.asList().stream().map(j -> j.getAsInt()).collect(Collectors.toList())).thenApply(recipes -> recipes.stream()
.map(RewriteRecipeRepository::recipeToJson)
.collect(Collectors.toList()));
});
server.onCommand(CMD_REWRITE_EXECUTE, params -> {
return recipes().thenCompose(recipes -> {
return recipes().thenCompose(recipesMap -> {
String uri = ((JsonElement) params.getArguments().get(0)).getAsString();
JsonElement recipesJson = ((JsonElement) params.getArguments().get(1));
boolean needsConfirmation = params.getArguments().size() > 2 ? ((JsonElement) params.getArguments().get(2)).getAsBoolean() : false;
RecipeDescriptor d = serializationGson.fromJson(recipesJson, RecipeDescriptor.class);
Recipe aggregateRecipe = LoadUtils.createRecipe(d, id -> Optional.ofNullable(recipes.get(id)).map(r -> r.getClass()).orElse(null));
if (aggregateRecipe instanceof DeclarativeRecipe && aggregateRecipe.getRecipeList().isEmpty()) {
throw new RuntimeException("No recipes found to perform!");
} else if (aggregateRecipe.getRecipeList().size() == 1) {
Recipe r = aggregateRecipe.getRecipeList().get(0);
RecipeSelectionDescriptor[] descriptors = serializationGson.fromJson(recipesJson, RecipeSelectionDescriptor[].class);
List<Recipe> recipes = Arrays.stream(descriptors).map(d -> createRecipeFromSelection(recipesMap.get(d.id()), d.subselection())).collect(Collectors.toList());
if (recipes.size() == 1) {
Recipe r = recipes.get(0);
String progressToken = params.getWorkDoneToken() == null
|| params.getWorkDoneToken().getLeft() == null
? (r.getName() == null ? UUID.randomUUID().toString() : r.getName())
: params.getWorkDoneToken().getLeft();
return apply(r, uri, progressToken, needsConfirmation);
} else {
String name = recipes.size() + " recipes";
DeclarativeRecipe aggregateRecipe = new DeclarativeRecipe(
name,
name,
recipes.stream().map(r -> r.getDescription()).collect(Collectors.joining("\n")),
recipes.stream().flatMap(r -> r.getTags().stream()).collect(Collectors.toSet()),
null,
null,
false,
Collections.emptyList()
);
aggregateRecipe.getRecipeList().addAll(recipes);
String progressToken = params.getWorkDoneToken() == null
|| params.getWorkDoneToken().getLeft() == null
? (aggregateRecipe.getName() == null ? UUID.randomUUID().toString()
@@ -397,21 +478,6 @@ public class RewriteRecipeRepository {
}
}
private CompletableFuture<List<Recipe>> listProjectRefactoringRecipes(String uri) {
if (uri != null) {
/*
* When LS started on listing rewrite recipes project lookup may not find any projects as classpath might still be resolving.
* Therefore, it is best probably to list the available recipes and figure out if it is a Spring Boot project recipes is applied to
* and if not throw an exception.
*/
// Optional<IJavaProject> projectOpt = projectFinder.find(new TextDocumentIdentifier(uri));
// if (projectOpt.isPresent()) {
return recipes().thenApply(recipes -> recipes.values().stream().filter(this::isAcceptableGlobalCommandRecipe).collect(Collectors.toList()));
// }
}
return CompletableFuture.completedFuture(Collections.emptyList());
}
private static ProjectParser createRewriteProjectParser(IJavaProject jp, Function<Path, Parser.Input> inputProvider) {
switch (jp.getProjectBuild().getType()) {
case ProjectBuild.MAVEN_PROJECT_TYPE:
@@ -438,47 +504,6 @@ public class RewriteRecipeRepository {
return s;
}
// private static Recipe convert(Recipe r, RecipeDescriptor d) {
// try {
// if (d.selected) {
// if (d.children != null && !d.children.isEmpty()) {
// Recipe recipe = r instanceof DeclarativeRecipe ? new DeclarativeRecipe(r.getName(), r.getDisplayName(), r.getDescription(), r.getTags(), r.getEstimatedEffortPerOccurrence(), null)
// : r.getClass().getDeclaredConstructor().newInstance();
// int i = 0;
// for (Recipe sr : r.getRecipeList()) {
// Recipe convertedSubRecipe = convert(sr, d.children.get(i++));
// if (convertedSubRecipe != null) {
// recipe.doNext(convertedSubRecipe);
// }
// }
// return recipe;
// } else {
// return r;
// }
// }
// } catch (Exception e) {
// log.error("", e);
// }
// return null;
// }
// @SuppressWarnings("unused")
// private static class RecipeDescriptor {
// String id;
// String label;
// String detail;
// List<RecipeDescriptor> children;
// boolean selected;
//
// RecipeDescriptor(Recipe r) {
// this.id = r.getName();
// this.label = r.getDisplayName();
// this.detail = r.getDescription();
// List<Recipe> subRecipes = r.getRecipeList();
// if (r instanceof DeclarativeRecipe && !subRecipes.isEmpty() && (subRecipes.size() > 1 || subRecipes.get(0) instanceof DeclarativeRecipe)) {
// this.children = r.getRecipeList().stream().map(sr -> new RecipeDescriptor(sr)).collect(Collectors.toList());
// }
// }
// }
record RecipeSelectionDescriptor(boolean selected, String id, RecipeSelectionDescriptor[] subselection) {};
}

View File

@@ -0,0 +1,134 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.rewrite;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openrewrite.Recipe;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository.RecipeSelectionDescriptor;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(SymbolProviderTestConf.class)
public class RewriteRecipeRepositoryTest {
@Autowired RewriteRecipeRepository recipeRepo;
@Test
void listSubRecipe() throws Exception {
List<Recipe> recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1", List.of()).get();
assertEquals(8, recipes.size());
recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1", List.of(0, 2)).get();
assertEquals(13, recipes.size());
recipes = recipeRepo.getSubRecipes("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1",List.of(0, 345)).get();
assertEquals(0, recipes.size());
recipes = recipeRepo.getSubRecipes("Hohoho", List.of()).get();
assertEquals(0, recipes.size());
}
@Test
void listRootRecipes() throws Exception {
List<Recipe> recipes = recipeRepo.getRootRecipes(r -> true).get();
assertThat(recipes.size()).isGreaterThan(5);
recipes = recipeRepo.getRootRecipes(r -> r.getName().startsWith("Hohoho")).get();
assertEquals(0, recipes.size());
}
@Test
void createRecipeFromSelectionDescriptor() throws Exception {
RecipeSelectionDescriptor descriptor = new RecipeSelectionDescriptor(true, "org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1", new RecipeSelectionDescriptor[] {
new RecipeSelectionDescriptor(true, "org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0", new RecipeSelectionDescriptor[] { // pick boot 3.0
new RecipeSelectionDescriptor(true, "org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7", null), // pick boot 2.7
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(true, "org.openrewrite.java.migrate.UpgradeToJava17", new RecipeSelectionDescriptor[] { // pick Java 17
new RecipeSelectionDescriptor(true, "", null), // java 11
new RecipeSelectionDescriptor(true, "", null), // java 17
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(true, "", null), // text blocks
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
}),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
}),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(true, "", null),
new RecipeSelectionDescriptor(false, "", null),
new RecipeSelectionDescriptor(false, "", null)
});
Recipe boot31Recipes = recipeRepo.getRecipe("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1").get().get();
Recipe boot31 = recipeRepo.createRecipeFromSelection(boot31Recipes, descriptor.subselection());
assertEquals(2, boot31.getRecipeList().size());
Recipe security61 = boot31.getRecipeList().get(1);
assertThat(security61.getRecipeList().size()).isGreaterThan(5);
Recipe boot30 = boot31.getRecipeList().get(0);
assertEquals("org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0", boot30.getName());
assertEquals(2, boot30.getRecipeList().size());
Recipe boot27 = boot30.getRecipeList().get(0);
assertThat(boot27.getRecipeList().size()).isGreaterThan(10);
Recipe java17 = boot30.getRecipeList().get(1);
assertThat(java17.getName()).isEqualTo("org.openrewrite.java.migrate.UpgradeToJava17");
assertThat(java17.getRecipeList().size()).isEqualTo(3);
assertThat(java17.getRecipeList().get(0).getName()).isEqualTo("org.openrewrite.java.migrate.Java8toJava11");
assertThat(java17.getRecipeList().get(1).getName()).isEqualTo("org.openrewrite.java.migrate.JavaVersion17");
assertThat(java17.getRecipeList().get(2).getName()).isEqualTo("org.openrewrite.java.migrate.lang.UseTextBlocks");
}
}

View File

@@ -11,11 +11,8 @@ interface RecipeDescriptor {
displayName: string;
description: string;
tags: string[];
estimatedEffortPerOccurrence: number;
options: OptionDescriptor[];
languages: string[];
recipeList: RecipeDescriptor[];
source: string;
hasSubRecipes: boolean;
}
interface OptionDescriptor {
@@ -29,10 +26,16 @@ interface OptionDescriptor {
value: any;
}
interface RecipeSelectionDescriptor {
selected: boolean;
id: string;
subselection: RecipeSelectionDescriptor[];
}
interface RecipeQuickPickItem extends VSCode.QuickPickItem{
readonly id: string;
selected: boolean;
children: RecipeQuickPickItem[],
children: RecipeQuickPickItem[] | undefined,
readonly recipeDescriptor: RecipeDescriptor;
}
@@ -95,45 +98,33 @@ async function showRefactorings(uri: VSCode.Uri, filter: string) {
if (!uri) {
uri = await getTargetPomXml();
}
const choices = await showCurrentPathQuickPick(VSCode.commands.executeCommand('sts/rewrite/list', uri.toString(true), filter).then((cmds: RecipeDescriptor[]) => cmds.map(convertToQuickPickItem)), []);
const recipeDescriptors = choices.filter(i => i.selected).map(convertToRecipeDescriptor);
const choices = await showCurrentPathQuickPick(VSCode.commands.executeCommand('sts/rewrite/list', filter).then((cmds: RecipeDescriptor[]) => cmds.map(d => convertToQuickPickItem(d, false))), []);
const recipeDescriptors = choices.filter(i => i.selected).map(convertToRecipeSelectionDescriptor);
const needsConfirmation = await shwoNeedsConfirmation();
if (recipeDescriptors.length) {
const aggregateRecipeDescriptor = recipeDescriptors.length === 1 ? recipeDescriptors[0] : {
name: `${recipeDescriptors.length} recipes`,
displayName: `${recipeDescriptors.length} recipes`,
description: recipeDescriptors.map(d => d.description).join('\n'),
tags: [...new Set<string>(recipeDescriptors.flatMap(d => d.tags))],
languages: [...new Set<string>(recipeDescriptors.flatMap(d => d.languages))],
options: [],
recipeList: recipeDescriptors,
estimatedEffortPerOccurrence: recipeDescriptors.filter(d => d.estimatedEffortPerOccurrence).map(d => d.estimatedEffortPerOccurrence).reduce((p, c) => p + c, 0)
};
if (aggregateRecipeDescriptor.estimatedEffortPerOccurrence === 0) {
delete aggregateRecipeDescriptor.estimatedEffortPerOccurrence;
}
VSCode.commands.executeCommand('sts/rewrite/execute', uri.toString(true), aggregateRecipeDescriptor, needsConfirmation);
VSCode.commands.executeCommand('sts/rewrite/execute', uri.toString(true), recipeDescriptors, needsConfirmation);
} else {
VSCode.window.showErrorMessage('No Recipes were selected!');
}
}
function convertToRecipeDescriptor(i: RecipeQuickPickItem): RecipeDescriptor {
function convertToRecipeSelectionDescriptor(i: RecipeQuickPickItem): RecipeSelectionDescriptor {
return {
...i.recipeDescriptor,
recipeList: i.children.filter(c => c.selected).map(convertToRecipeDescriptor)
selected: i.selected,
id: i.id,
subselection: i.children ? i.children.map(convertToRecipeSelectionDescriptor) : undefined
};
}
function convertToQuickPickItem(i: RecipeDescriptor): RecipeQuickPickItem {
function convertToQuickPickItem(i: RecipeDescriptor, selected?: boolean): RecipeQuickPickItem {
return {
id: i.name,
label: i.displayName,
detail: i.options.filter(o => !!o.value).map(o => `${o.name}: ${JSON.stringify(o.value)}`).join('\n\n'),
description: i.description,
selected: false,
children: i.recipeList ? i.recipeList.map(convertToQuickPickItem) : [],
buttons: i.recipeList && i.recipeList.length ? [ SUB_RECIPES_BUTTON ] : undefined,
selected: !!selected,
children: undefined,
buttons: i.hasSubRecipes ? [ SUB_RECIPES_BUTTON ] : undefined,
recipeDescriptor: i
};
}
@@ -188,7 +179,7 @@ function showCurrentPathQuickPick(itemsPromise: Thenable<RecipeQuickPickItem[]>,
if (e.button === SUB_RECIPES_BUTTON) {
currentItems.forEach(i => i.selected = quickPick.selectedItems.includes(i));
itemsPath.push(e.item);
showCurrentPathQuickPick(Promise.resolve(items), itemsPath).then(resolve, reject);
showCurrentPathQuickPick(navigateToSubRecipes(e.item, itemsPath).then(() => items), itemsPath).then(resolve, reject);
}
});
quickPick.onDidTriggerButton(b => {
@@ -222,6 +213,17 @@ function showCurrentPathQuickPick(itemsPromise: Thenable<RecipeQuickPickItem[]>,
});
}
async function navigateToSubRecipes(item: RecipeQuickPickItem, itemsPath: RecipeQuickPickItem[]) {
if (!item.children) {
const indexPath = [];
for (let i = 1; i < itemsPath.length; i++) {
indexPath.push(itemsPath[i - 1].children.indexOf(itemsPath[i]));
}
const recipeDescriptors: RecipeDescriptor[] = await VSCode.commands.executeCommand('sts/rewrite/sublist', itemsPath[0].id, indexPath);
item.children = recipeDescriptors.map(d => convertToQuickPickItem(d, item.selected));
}
}
function updateParentSelection(hierarchy: RecipeQuickPickItem[]): void {
if (hierarchy.length) {
const parent = hierarchy.pop();

View File

@@ -12,7 +12,7 @@
"license": "EPL-1.0",
"engines": {
"npm": ">=6.0.0",
"vscode": "^1.67.0"
"vscode": "^1.75.0"
},
"categories": [
"Programming Languages",
@@ -26,8 +26,6 @@
"application-yaml"
],
"activationEvents": [
"onCommand:vscode-spring-boot.rewrite.list.refactorings",
"onCommand:vscode-spring-boot.rewrite.list.boot-upgrades",
"onCommand:vscode-spring-boot.ls.start"
],
"contributes": {
@@ -1029,9 +1027,9 @@
},
"devDependencies": {
"@types/node": "^18.8.0",
"@types/vscode": "1.67.0",
"@types/vscode": "1.75.0",
"typescript": "^4.8.0",
"vsce": "^2.11.0"
"vsce": "^2.15.0"
},
"extensionDependencies": [
"redhat.java"