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

@@ -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");
}
}