Define bean in configuration class quickfix

This commit is contained in:
aboyko
2022-10-07 11:25:34 -04:00
parent c5da49443d
commit d3ef3c92e4
9 changed files with 263 additions and 26 deletions

View File

@@ -14,13 +14,10 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
@@ -169,18 +166,4 @@ public class IClasspathUtil {
.collect(CollectorUtil.toImmutableList());
}
public static Set<String> getBinaryClasspathEntries(IJavaProject project) throws Exception {
if (project == null) {
return Collections.emptySet();
} else {
IClasspath classpath = project.getClasspath();
Stream<File> classpathEntries = IClasspathUtil.getAllBinaryRoots(classpath).stream();
return classpathEntries
.filter(file -> file.exists())
.map(file -> file.getAbsolutePath()).collect(Collectors.toSet());
}
}
}

View File

@@ -0,0 +1,103 @@
/*******************************************************************************
* Copyright (c) 2022 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.java;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaParser;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
public class DefineMethod extends Recipe {
private static final String MESSAGE = "methodPresent";
private String targetFqName;
private String signature;
private String template;
private List<String> typeStubs = Collections.emptyList();
private List<String> imports = Collections.emptyList();
private List<String> classpath = Collections.emptyList();
public DefineMethod() {
}
public DefineMethod(String targetFqName, String signature, String template, List<String> imports, List<String> typeStubs, List<String> classpath) {
this.targetFqName = targetFqName;
this.signature = signature;
this.template = template;
this.imports = imports;
this.typeStubs = typeStubs;
this.classpath = classpath;
}
@Override
public String getDisplayName() {
return "Define a bean in type";
}
@Override
protected TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
private MethodMatcher matcher = new MethodMatcher(targetFqName + ' ' + signature);
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext executionContext) {
JavaType.Method type = method.getMethodType();
if (type != null && matcher.matches(type)) {
getCursor().dropParentUntil(J.ClassDeclaration.class::isInstance).putMessage(MESSAGE, true);
}
return method;
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext executionContext) {
J.ClassDeclaration c = super.visitClassDeclaration(classDecl, executionContext);
Boolean methodPresent = getCursor().pollMessage(MESSAGE);
if (template != null && methodPresent == null) {
JavaType.FullyQualified type = c.getType();
if (type != null && targetFqName.equals(type.getFullyQualifiedName())) {
JavaTemplate t = JavaTemplate.builder(() -> getCursor(), template)
.javaParser(() -> JavaParser
.fromJavaVersion()
.dependsOn(typeStubs.toArray(new String[typeStubs.size()]))
.classpath(classpath.stream().map(s -> Paths.get(s)).collect(Collectors.toList()))
.build())
.imports(imports.toArray(new String[imports.size()])).build();
J.Block body = classDecl.getBody().withTemplate(t, classDecl.getBody().getCoordinates().addMethodDeclaration((m, n) -> 1));
for (String fq : imports) {
maybeAddImport(fq);
}
c = c.withBody(body);
}
}
return c;
}
};
}
}

View File

@@ -11,7 +11,6 @@
package org.springframework.ide.vscode.commons.rewrite.java;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -232,7 +231,7 @@ public class ORAstUtils {
public static JavaParser createJavaParser(IJavaProject project) {
try {
List<Path> classpath = IClasspathUtil.getBinaryClasspathEntries(project).stream().map(s -> new File(s).toPath()).collect(Collectors.toList());
List<Path> classpath = IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath()).collect(Collectors.toList());
JavaParser jp = JavaParser.fromJavaVersion().build();
jp.setClasspath(classpath);
return jp;

View File

@@ -549,6 +549,11 @@ public class SpringSymbolIndex implements InitializingBean {
.map(enhanced -> enhanced.getSymbol());
}
public List<EnhancedSymbolInformation> getEnhancedSymbols(IJavaProject project) {
List<EnhancedSymbolInformation> list = symbolsByProject.get(project.getElementName());
return list == null ? Collections.emptyList() : Collections.unmodifiableList(list);
}
synchronized private CompletableFuture<IJavaProject> projectInitializedFuture(IJavaProject project) {
if (project == null) {
return CompletableFuture.completedFuture(null);

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 2022 Pivotal, 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
@@ -21,6 +21,7 @@ public class Annotations {
public static final String CONDITIONAL = "org.springframework.context.annotation.Conditional";
public static final String COMPONENT = "org.springframework.stereotype.Component";
public static final String CONFIGURATION = "org.springframework.context.annotation.Configuration";
public static final String REPOSITORY = "org.springframework.stereotype.Repository";
public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";

View File

@@ -24,6 +24,7 @@ import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.handlers.AbstractSymbolProvider;
import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation;
import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation;
@@ -64,8 +65,14 @@ public class ComponentSymbolProvider extends AbstractSymbolProvider {
WorkspaceSymbol symbol = new WorkspaceSymbol(
beanLabel("+", annotationTypeName, metaAnnotationNames, beanName, beanType.getName()), SymbolKind.Interface,
Either.forLeft(new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()))));
SymbolAddOnInformation[] addon = new SymbolAddOnInformation[] {new BeansSymbolAddOnInformation(beanName, beanType.getQualifiedName())};
SymbolAddOnInformation[] addon = new SymbolAddOnInformation[0];
if (Annotations.CONFIGURATION.equals(annotationType.getQualifiedName())
|| metaAnnotations.stream().anyMatch(t -> Annotations.CONFIGURATION.equals(t.getQualifiedName()))) {
addon = new SymbolAddOnInformation[] {new ConfigBeanSymbolAddOnInformation(beanName, beanType.getQualifiedName())};
} else {
addon = new SymbolAddOnInformation[] {new BeansSymbolAddOnInformation(beanName, beanType.getQualifiedName())};
}
return new EnhancedSymbolInformation(symbol, addon);
}

View File

@@ -0,0 +1,19 @@
/*******************************************************************************
* Copyright (c) 2022 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.beans;
public class ConfigBeanSymbolAddOnInformation extends BeansSymbolAddOnInformation {
public ConfigBeanSymbolAddOnInformation(String beanID, String beanType) {
super(beanID, beanType);
}
}

View File

@@ -12,7 +12,7 @@ package org.springframework.ide.vscode.boot.java.rewrite;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -139,7 +139,7 @@ public class RewriteReconciler implements JavaReconciler {
JavaParser javaParser = ORAstUtils.createJavaParser(project);
if (javaParser != null) {
List<CompilationUnit> cus = ORAstUtils.parseInputs(javaParser, docs.stream().map(d -> new Parser.Input(Path.of(d.getUri()), () -> {
List<CompilationUnit> cus = ORAstUtils.parseInputs(javaParser, docs.stream().map(d -> new Parser.Input(Paths.get(URI.create(d.getUri())), () -> {
return new ByteArrayInputStream(d.get().getBytes());
})).collect(Collectors.toList()));

View File

@@ -12,29 +12,47 @@ package org.springframework.ide.vscode.boot.java.rewrite.reconcile;
import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.openrewrite.ExecutionContext;
import org.openrewrite.SourceFile;
import org.openrewrite.Tree;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaVisitor;
import org.openrewrite.java.tree.J.ClassDeclaration;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.java.tree.JavaType.FullyQualified;
import org.openrewrite.java.tree.TypeUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.java.Boot3JavaProblemType;
import org.springframework.ide.vscode.boot.java.beans.BeansSymbolAddOnInformation;
import org.springframework.ide.vscode.boot.java.beans.ConfigBeanSymbolAddOnInformation;
import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation;
import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeSpringJavaProblemDescriptor;
import org.springframework.ide.vscode.commons.rewrite.java.FixAssistMarker;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet;
public class NotRegisteredBeansProblem implements RecipeSpringJavaProblemDescriptor {
private static final String DEFINE_METHOD_RECIPE = "org.springframework.ide.vscode.commons.rewrite.java.DefineMethod";
private static final List<String> AOT_BEANS = List.of(
"org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor",
"org.springframework.beans.factory.aot.BeanRegistrationAotProcessor",
@@ -66,8 +84,58 @@ public class NotRegisteredBeansProblem implements RecipeSpringJavaProblemDescrip
}
return false;
}).limit(1).collect(Collectors.toList());
Builder<FixDescriptor> fixListBuilder = ImmutableList.builder();
List<JavaType.Method> constructors = c.getType().getMethods().stream().filter(m -> m.isConstructor()).collect(Collectors.toList());
if (beanSymbols.isEmpty()) {
return c = c.withName(c.getName().withMarkers(c.getName().getMarkers().add(new FixAssistMarker(Tree.randomId(), getId()))));
SourceFile source = getCursor().firstEnclosing(SourceFile.class);
String uri = source.getSourcePath().toUri().toString();
FixAssistMarker marker = new FixAssistMarker(Tree.randomId(), getId());
JavaProjectFinder projectFinder = applicationContext.getBean(JavaProjectFinder.class);
if (projectFinder != null) {
IJavaProject project = projectFinder.find(new TextDocumentIdentifier(uri)).orElse(null);
if (project != null) {
for (EnhancedSymbolInformation s : index.getEnhancedSymbols(project)) {
if (s.getAdditionalInformation() != null) {
ConfigBeanSymbolAddOnInformation configInfo = Arrays.stream(s.getAdditionalInformation()).filter(ConfigBeanSymbolAddOnInformation.class::isInstance).map(ConfigBeanSymbolAddOnInformation.class::cast).findFirst().orElse(null);
if (configInfo != null) {
for (JavaType.Method constructor : constructors) {
String constructorParamsSignature = "(" + constructor.getParameterTypes().stream().map(pt -> typePattern(pt)).collect(Collectors.joining(",")) + ")";
String beanMethodName = "get" + type.getClassName();
String pattern = beanMethodName + constructorParamsSignature;
String contructorParamsLabel = "(" + constructor.getParameterTypes().stream().map(NotRegisteredBeansProblem::typeStr).collect(Collectors.joining(", ")) + ")";
Builder<String> paramBuilder = ImmutableList.builder();
for (int i = 0; i < constructor.getParameterNames().size() && i < constructor.getParameterTypes().size(); i++) {
JavaType paramType = constructor.getParameterTypes().get(i);
String paramName = constructor.getParameterNames().get(i);
paramBuilder.add(typeStr(paramType) + ' ' + paramName);
}
String paramsStr = String.join(", ", paramBuilder.build().toArray(String[]::new));
fixListBuilder.add(new FixDescriptor(DEFINE_METHOD_RECIPE, List.of(s.getSymbol().getLocation().getLeft().getUri()), "Define bean in config '" + configInfo.getBeanID() + "' with constructor " + contructorParamsLabel)
.withRecipeScope(RecipeScope.FILE)
.withParameters(Map.of(
"targetFqName", configInfo.getBeanType(),
"signature", pattern,
"template", "@Bean\n"
+ type.getClassName() + " " + beanMethodName + "(" + paramsStr + ") {\n"
+ "return new " + type.getClassName() + "(" + constructor.getParameterNames().stream().collect(Collectors.joining(", ")) + ");\n"
+ "}\n",
"imports", allFQTypes(constructor).toArray(String[]::new),
"typeStubs", new String[] { source.printAll() },
"classpath", IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath().toString()).toArray(String[]::new)
))
);
}
}
}
}
}
}
marker.withFixes(fixListBuilder.build().toArray(FixDescriptor[]::new));
c = c.withName(c.getName().withMarkers(c.getName().getMarkers().add(marker)));
}
}
}
@@ -76,7 +144,25 @@ public class NotRegisteredBeansProblem implements RecipeSpringJavaProblemDescrip
};
}
private static Set<String> allFQTypes(JavaType type) {
ImmutableSet.Builder<String> b = ImmutableSet.builder();
if (type instanceof JavaType.FullyQualified) {
b.add(((JavaType.FullyQualified) type).getFullyQualifiedName());
if (type instanceof JavaType.Parameterized) {
((JavaType.Parameterized) type).getTypeParameters().forEach(t -> b.addAll(allFQTypes(t)));
}
} else if (type instanceof JavaType.Array) {
b.addAll(allFQTypes(((JavaType.Array) type).getElemType()));
} else if (type instanceof JavaType.Method) {
JavaType.Method m = (JavaType.Method) type;
b.addAll(allFQTypes(m.getDeclaringType()));
b.addAll(allFQTypes(m.getReturnType()));
m.getParameterTypes().forEach(pt -> b.addAll(allFQTypes(pt)));
}
return b.build();
}
@Override
public boolean isApplicable(IJavaProject project) {
return springBootVersionGreaterOrEqual(3, 0, 0).test(project);
@@ -86,5 +172,39 @@ public class NotRegisteredBeansProblem implements RecipeSpringJavaProblemDescrip
public ProblemType getProblemType() {
return Boot3JavaProblemType.JAVA_BEAN_NOT_REGISTERED_IN_AOT;
}
private static String typePattern(JavaType type) {
if (type instanceof JavaType.Primitive) {
if (type.equals(JavaType.Primitive.String)) {
return ((JavaType.Primitive) type).getClassName();
}
return ((JavaType.Primitive) type).getKeyword();
} else if (type instanceof JavaType.FullyQualified) {
return ((JavaType.FullyQualified) type).getFullyQualifiedName();
} else if (type instanceof JavaType.Array) {
JavaType elemType = ((JavaType.Array) type).getElemType();
return typePattern(elemType) + "[]";
}
return null;
}
private static String typeStr(JavaType type) {
if (type instanceof JavaType.Primitive) {
if (type.equals(JavaType.Primitive.String)) {
return ((JavaType.Primitive) type).getClassName();
}
return ((JavaType.Primitive) type).getKeyword();
} else if (type instanceof JavaType.Parameterized) {
JavaType.Parameterized parametereized = (JavaType.Parameterized) type;
return parametereized.getClassName() + "<" + parametereized.getTypeParameters().stream().map(NotRegisteredBeansProblem::typeStr).collect(Collectors.joining(", ")) + ">";
} else if (type instanceof JavaType.FullyQualified) {
return ((JavaType.FullyQualified) type).getClassName();
} else if (type instanceof JavaType.Array) {
JavaType elemType = ((JavaType.Array) type).getElemType();
return typeStr(elemType) + "[]";
}
return null;
}
}