Rework loading of rewrite recipes and code actions

This commit is contained in:
aboyko
2023-04-03 18:26:51 -04:00
parent 4ba7f8450a
commit f2668e5641
5 changed files with 134 additions and 290 deletions

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
@@ -13,7 +13,6 @@ package org.springframework.tooling.boot.ls;
import java.util.List;
import org.eclipse.jface.preference.FieldEditorPreferencePage;
import org.eclipse.jface.preference.PathEditor;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
@@ -33,8 +32,8 @@ public class PlugRecipesPreferencePage extends FieldEditorPreferencePage impleme
addField(new FileListEditor(Constants.PREF_REWRITE_RECIPES_SCAN_FILES, "JAR and YAML files to scan for Recipes",
"Select JARs and YAML files:", fieldEditorParent, List.of("jar", "yml", "yaml")));
addField(new PathEditor(Constants.PREF_REWRITE_RECIPES_SCAN_DIRS, "Directories to scan for Recipes",
"Select directory to scan for Recipes", fieldEditorParent));
// addField(new PathEditor(Constants.PREF_REWRITE_RECIPES_SCAN_DIRS, "Directories to scan for Recipes",
// "Select directory to scan for Recipes", fieldEditorParent));
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* 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.commons.rewrite.config;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.openrewrite.internal.RecipeIntrospectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
public class CodeActionRepoLoader {
final static Logger log = LoggerFactory.getLogger(CodeActionRepoLoader.class);
private final List<CodeActionRepository> codeActionRepos = new ArrayList<>();
public CodeActionRepoLoader(String... acceptPackages) {
scanClasses(new ClassGraph().acceptPackages(acceptPackages), getClass().getClassLoader());
}
public CodeActionRepoLoader(Path p, ClassLoader classLoader) {
if (Files.isDirectory(p)) {
String dir = p.toString();
scanClasses(new ClassGraph().acceptPaths(dir).ignoreParentClassLoaders().overrideClassLoaders(classLoader),
classLoader);
} else {
String jarName = p.toFile().getName();
scanClasses(
new ClassGraph().acceptJars(jarName).ignoreParentClassLoaders().overrideClassLoaders(classLoader),
classLoader);
}
}
private void scanClasses(ClassGraph classGraph, ClassLoader classLoader) {
try (ScanResult result = classGraph.ignoreClassVisibility().overrideClassLoaders(classLoader).scan()) {
for (ClassInfo classInfo : result.getSubclasses(CodeActionRepository.class.getName())) {
Class<?> codeActionRepoClass = classInfo.loadClass();
Constructor<?> primaryConstructor = RecipeIntrospectionUtils
.getZeroArgsConstructor(codeActionRepoClass);
if (primaryConstructor == null) {
// TODO: error!!!
} else {
try {
CodeActionRepository repo = (CodeActionRepository) primaryConstructor.newInstance();
codeActionRepos.add(repo);
} catch (Throwable t) {
log.warn("Unable to configure " + codeActionRepoClass.getName(), t);
}
}
}
}
}
public List<CodeActionRepository> listCodeActionDescriptorsRepositories() {
return codeActionRepos;
}
}

View File

@@ -1,217 +0,0 @@
/*******************************************************************************
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.config;
import static java.util.Collections.emptyList;
import static org.openrewrite.internal.RecipeIntrospectionUtils.constructRecipe;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.annotation.Nullable;
import org.openrewrite.Contributor;
import org.openrewrite.Recipe;
import org.openrewrite.config.CategoryDescriptor;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.config.RecipeDescriptor;
import org.openrewrite.config.RecipeExample;
import org.openrewrite.config.ResourceLoader;
import org.openrewrite.config.YamlResourceLoader;
import org.openrewrite.internal.RecipeIntrospectionUtils;
import org.openrewrite.style.NamedStyles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
public class StsClasspathScanningLoader implements ResourceLoader, StsResourceLoader {
final static Logger log = LoggerFactory.getLogger(StsClasspathScanningLoader.class);
private final List<Recipe> recipes = new ArrayList<>();
private final List<NamedStyles> styles = new ArrayList<>();
private final List<RecipeDescriptor> recipeDescriptors = new ArrayList<>();
private final List<CategoryDescriptor> categoryDescriptors = new ArrayList<>();
private final Map<String, List<Contributor>> recipeAttributions = new HashMap<>();
private final Map<String, List<RecipeExample>> recipeExamples = new HashMap<>();
private final List<CodeActionRepository> codeActionRepos = new ArrayList<>();
public StsClasspathScanningLoader(Path p, Properties properties, Collection<? extends ResourceLoader> dependencyResourceLoaders, ClassLoader classLoader) {
if (Files.isDirectory(p)) {
String dir = p.toString();
scanClasses(new ClassGraph()
.acceptPaths(dir)
.ignoreParentClassLoaders()
.overrideClassLoaders(classLoader), classLoader);
scanYaml(new ClassGraph()
.acceptPaths(dir)
.ignoreParentClassLoaders()
.overrideClassLoaders(classLoader)
.acceptPaths("META-INF/rewrite"), properties, dependencyResourceLoaders, classLoader);
} else {
String jarName = p.toFile().getName();
scanClasses(new ClassGraph()
.acceptJars(jarName)
.ignoreParentClassLoaders()
.overrideClassLoaders(classLoader), classLoader);
scanYaml(new ClassGraph()
.acceptJars(jarName)
.ignoreParentClassLoaders()
.overrideClassLoaders(classLoader)
.acceptPaths("META-INF/rewrite"), properties, dependencyResourceLoaders, classLoader);
}
}
public StsClasspathScanningLoader(Properties properties, String[] acceptPackages) {
scanClasses(new ClassGraph().acceptPackages(acceptPackages), getClass().getClassLoader());
scanYaml(new ClassGraph().acceptPaths("META-INF/rewrite"), properties, emptyList(), null);
}
/**
* Construct a ClasspathScanningLoader scans the provided classload for recipes
*
* @param properties Yaml placeholder properties
* @param classLoader Limit scan to classes loadable by this classloader
*/
public StsClasspathScanningLoader(Properties properties, ClassLoader classLoader) {
scanClasses(new ClassGraph()
.ignoreParentClassLoaders()
.overrideClassLoaders(classLoader), classLoader);
scanYaml(new ClassGraph()
.ignoreParentClassLoaders()
.overrideClassLoaders(classLoader)
.acceptPaths("META-INF/rewrite"), properties, emptyList(), classLoader);
}
/**
* This must be called _after_ scanClasses or the descriptors of declarative recipes will be missing any
* non-declarative recipes they depend on that would be discovered by scanClasses
*/
private void scanYaml(ClassGraph classGraph, Properties properties, Collection<? extends ResourceLoader> dependencyResourceLoaders, @Nullable ClassLoader classLoader) {
try (ScanResult scanResult = classGraph.enableMemoryMapping().scan()) {
List<YamlResourceLoader> yamlResourceLoaders = new ArrayList<>();
scanResult.getResourcesWithExtension("yml").forEachInputStreamIgnoringIOException((res, input) -> {
yamlResourceLoaders.add(new YamlResourceLoader(input, res.getURI(), properties, classLoader, dependencyResourceLoaders));
});
// Extract in two passes so that the full list of recipes from all sources are known when computing recipe descriptors
// Otherwise recipes which include recipes from other sources in their recipeList will have incomplete descriptors
for(YamlResourceLoader resourceLoader : yamlResourceLoaders) {
recipes.addAll(resourceLoader.listRecipes());
categoryDescriptors.addAll(resourceLoader.listCategoryDescriptors());
styles.addAll(resourceLoader.listStyles());
recipeAttributions.putAll(resourceLoader.listContributors());
recipeExamples.putAll(resourceLoader.listRecipeExamples());
}
for(YamlResourceLoader resourceLoader : yamlResourceLoaders) {
recipeDescriptors.addAll(resourceLoader.listRecipeDescriptors(recipes, recipeAttributions, recipeExamples));
}
}
}
private void scanClasses(ClassGraph classGraph, ClassLoader classLoader) {
try (ScanResult result = classGraph
.ignoreClassVisibility()
.overrideClassLoaders(classLoader)
.scan()) {
for (ClassInfo classInfo : result.getSubclasses(Recipe.class.getName())) {
Class<?> recipeClass = classInfo.loadClass();
if (recipeClass.getName().equals(DeclarativeRecipe.class.getName()) || recipeClass.getEnclosingClass() != null) {
continue;
}
try {
Recipe recipe = constructRecipe(recipeClass);
recipeDescriptors.add(recipe.getDescriptor());
recipes.add(recipe);
} catch (Throwable t) {
log.warn("Unable to configure " + recipeClass.getName(), t);
}
}
for (ClassInfo classInfo : result.getSubclasses(NamedStyles.class.getName())) {
Class<?> styleClass = classInfo.loadClass();
try {
Constructor<?> constructor = RecipeIntrospectionUtils.getZeroArgsConstructor(styleClass);
if(constructor != null) {
constructor.setAccessible(true);
styles.add((NamedStyles) constructor.newInstance());
}
} catch (Throwable t) {
log.warn("Unable to configure " + styleClass.getName(), t);
}
}
for (ClassInfo classInfo : result.getSubclasses(CodeActionRepository.class.getName())) {
Class<?> codeActionRepoClass = classInfo.loadClass();
Constructor<?> primaryConstructor = RecipeIntrospectionUtils.getZeroArgsConstructor(codeActionRepoClass);
if (primaryConstructor == null) {
//TODO: error!!!
} else {
try {
CodeActionRepository repo = (CodeActionRepository) primaryConstructor.newInstance();
codeActionRepos.add(repo);
} catch (Throwable t) {
log.warn("Unable to configure " + codeActionRepoClass.getName(), t);
}
}
}
}
}
@Override
public Collection<Recipe> listRecipes() {
return recipes;
}
@Override
public Collection<RecipeDescriptor> listRecipeDescriptors() {
return recipeDescriptors;
}
@Override
public Collection<CategoryDescriptor> listCategoryDescriptors() {
return categoryDescriptors;
}
@Override
public Collection<NamedStyles> listStyles() {
return styles;
}
public Map<String, List<RecipeExample>> listRecipeExamples() {
return recipeExamples;
}
public List<CodeActionRepository> listCodeActionDescriptorsRepositories() {
return codeActionRepos;
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2022, 2023 VMware, Inc.
* 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
@@ -10,89 +10,72 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.rewrite.config;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openrewrite.config.ClasspathScanningLoader;
import org.openrewrite.Recipe;
import org.openrewrite.config.Environment;
import org.openrewrite.config.RecipeDescriptor;
import org.openrewrite.config.ResourceLoader;
import static java.util.Collections.emptyList;
public class StsEnvironment {
public class StsEnvironment extends Environment {
final private Supplier<Stream<CodeActionRepository>> codeActionRepos;
final private Supplier<Stream<CodeActionRepository>> codeActionRepos;
final private Environment env;
public StsEnvironment(Collection<? extends ResourceLoader> resourceLoaders) {
super(resourceLoaders);
codeActionRepos = () -> resourceLoaders.stream().filter(StsResourceLoader.class::isInstance).map(StsResourceLoader.class::cast).flatMap(l -> l.listCodeActionDescriptorsRepositories().stream());
private StsEnvironment(Environment env, List<CodeActionRepoLoader> loaders) {
this.env = env;
codeActionRepos = () -> loaders.stream().flatMap(l -> l.listCodeActionDescriptorsRepositories().stream());
}
public static class Builder extends Environment.Builder {
final private Properties props;
public Builder(Properties properties) {
super(properties);
this.props = properties;
}
public static class Builder {
private List<CodeActionRepoLoader> loaders = new ArrayList<>();
private Environment.Builder envBuilder = new Environment.Builder(new Properties());;
@Override
public Builder scanRuntimeClasspath(String... acceptPackages) {
return (Builder) load(new StsClasspathScanningLoader(props, acceptPackages));
loaders.add(new CodeActionRepoLoader(acceptPackages));
envBuilder.scanRuntimeClasspath(acceptPackages);
return this;
}
@Override
public Builder scanClassLoader(ClassLoader classLoader) {
return (Builder) load(new StsClasspathScanningLoader(props, classLoader));
public Builder scanJar(Path jar, ClassLoader classLoader) {
loaders.add(new CodeActionRepoLoader(jar, classLoader));
envBuilder.scanJar(jar, Collections.emptyList(), classLoader);
return this;
}
@Override
public Builder scanJar(Path jar, Collection<Path> dependencies, ClassLoader classLoader) {
List<ClasspathScanningLoader> list = new ArrayList<>();
for (Path dep : dependencies) {
ClasspathScanningLoader classpathScanningLoader = new ClasspathScanningLoader(dep, props, emptyList(), classLoader);
list.add(classpathScanningLoader);
}
return (Builder) load(new StsClasspathScanningLoader(jar, props, list, classLoader), list);
}
public Builder scanPath(Path dir, Collection<Path> dependencies, ClassLoader classLoader) {
List<ClasspathScanningLoader> list = new ArrayList<>();
for (Path dep : dependencies) {
ClasspathScanningLoader classpathScanningLoader = new ClasspathScanningLoader(dep, props, emptyList(), classLoader);
list.add(classpathScanningLoader);
}
return (Builder) load(new StsClasspathScanningLoader(dir, props, list, classLoader));
}
@SuppressWarnings("unchecked")
public StsEnvironment build() {
try {
Field f = Environment.Builder.class.getDeclaredField("resourceLoaders");
f.setAccessible(true);
return new StsEnvironment((Collection<ResourceLoader>) f.get(this));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
return new StsEnvironment(envBuilder.build(), loaders);
}
public void load(ResourceLoader loader) {
envBuilder.load(loader, Collections.emptyList());
}
}
public List<RecipeCodeActionDescriptor> listCodeActionDescriptors() {
return codeActionRepos.get().flatMap(r -> r.getCodeActionDescriptors().stream()).collect(Collectors.toList());
}
public static Builder builder() {
return new Builder(new Properties());
}
public Collection<Recipe> listRecipes() {
return env.listRecipes();
}
public Collection<RecipeDescriptor> listRecipeDescriptors() {
return env.listRecipeDescriptors();
}
public static Builder builder() {
return new Builder();
}
}

View File

@@ -15,7 +15,6 @@ import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
@@ -249,7 +248,7 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
if (pathStr.endsWith(".jar")) {
URLClassLoader classLoader = new URLClassLoader(new URL[] { f.toUri().toURL() },
getClass().getClassLoader());
builder.scanJar(f, Collections.emptyList(), classLoader);
builder.scanJar(f, classLoader);
} else if (pathStr.endsWith(".yml") || pathStr.endsWith(".yaml")) {
builder.load(new YamlResourceLoader(new FileInputStream(f.toFile()), f.toUri(), new Properties()));
}
@@ -257,17 +256,17 @@ public class RewriteRecipeRepository implements ApplicationContextAware {
log.error("Skipping folder " + p, e);
}
}
for (String p : scanDirs) {
try {
Path d = Path.of(p);
if (Files.isDirectory(d)) {
URLClassLoader classLoader = new URLClassLoader(new URL[] { d.toUri().toURL()}, getClass().getClassLoader());
builder.scanPath(d, Collections.emptyList(), classLoader);
}
} catch (Exception e) {
log.error("Skipping folder " + p, e);
}
}
// for (String p : scanDirs) {
// try {
// Path d = Path.of(p);
// if (Files.isDirectory(d)) {
// URLClassLoader classLoader = new URLClassLoader(new URL[] { d.toUri().toURL()}, getClass().getClassLoader());
// builder.scanPath(d, Collections.emptyList(), classLoader);
// }
// } catch (Exception e) {
// log.error("Skipping folder " + p, e);
// }
// }
return builder.build();
}