From d6ff60a96e0701b3972350a5b4271bab48c6df98 Mon Sep 17 00:00:00 2001
From: aboyko
Date: Mon, 18 Dec 2023 20:54:59 -0500
Subject: [PATCH] Minimize size of messages for fetching and executing recipes
---
.../.classpath | 2 +-
.../.settings/org.eclipse.jdt.core.prefs | 6 +-
.../boot/ls/commands/RecipeDescriptor.java | 64 +++----
.../boot/ls/commands/RecipeTreeModel.java | 162 +++++++++-------
.../commands/RewriteRefactoringsHandler.java | 60 ++----
.../boot/ls/commands/SelectRecipesDialog.java | 55 ++++--
.../ide/vscode/commons/rewrite/LoadUtils.java | 10 +-
.../java/rewrite/RewriteRecipeRepository.java | 175 ++++++++++--------
.../rewrite/RewriteRecipeRepositoryTest.java | 134 ++++++++++++++
.../vscode-spring-boot/lib/rewrite.ts | 60 +++---
.../vscode-spring-boot/package.json | 8 +-
11 files changed, 449 insertions(+), 287 deletions(-)
create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepositoryTest.java
diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath b/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath
index bc57d2383..5508535a4 100644
--- a/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/.classpath
@@ -1,6 +1,6 @@
-
+
diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs b/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs
index 7adc0fb9a..d4540a53f 100644
--- a/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/.settings/org.eclipse.jdt.core.prefs
@@ -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
diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java
index 2ab085134..fd9eb047d 100644
--- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeDescriptor.java
@@ -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 tags;
- Duration estimatedEffortPerOccurrence;
-
List options;
List languages;
List 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 valid;
-
- boolean required;
-
- Object value;
- }
+ record OptionDescriptor(
+ String name,
+ String type,
+ String displayName,
+ String description,
+ String example,
+ List valid,
+ boolean required,
+ Object value
+ ) {}
+
+ record RecipeSelection(boolean selected, String id, RecipeSelection[] subselection) {}
+
}
\ No newline at end of file
diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java
index 318c553e7..10324852f 100644
--- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RecipeTreeModel.java
@@ -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 parentMap = new IdentityHashMap<>();
- private Map 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 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 fetchSubrecipes(RecipeDescriptor descriptor) {
+ RecipeDescriptor d = descriptor;
+ LinkedList 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 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 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());
+ }
+ }
+
}
diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java
index f0fb5f86a..113e599ad 100644
--- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java
+++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/commands/RewriteRefactoringsHandler.java
@@ -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, JsonDeserializer {
- @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
");
sb.append("