diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java index e7b475c28..ba1c59af0 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/IClasspathUtil.java @@ -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 getBinaryClasspathEntries(IJavaProject project) throws Exception { - if (project == null) { - return Collections.emptySet(); - } else { - IClasspath classpath = project.getClasspath(); - Stream classpathEntries = IClasspathUtil.getAllBinaryRoots(classpath).stream(); - return classpathEntries - .filter(file -> file.exists()) - .map(file -> file.getAbsolutePath()).collect(Collectors.toSet()); - } - } - - } diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/DefineMethod.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/DefineMethod.java new file mode 100644 index 000000000..943b96608 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/DefineMethod.java @@ -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 typeStubs = Collections.emptyList(); + + private List imports = Collections.emptyList(); + + private List classpath = Collections.emptyList(); + + public DefineMethod() { + } + + public DefineMethod(String targetFqName, String signature, String template, List imports, List typeStubs, List 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 getVisitor() { + return new JavaIsoVisitor() { + + 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; + } + }; + } + +} diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java index b64cef3a6..0f4051f94 100644 --- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java @@ -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 classpath = IClasspathUtil.getBinaryClasspathEntries(project).stream().map(s -> new File(s).toPath()).collect(Collectors.toList()); + List classpath = IClasspathUtil.getAllBinaryRoots(project.getClasspath()).stream().map(f -> f.toPath()).collect(Collectors.toList()); JavaParser jp = JavaParser.fromJavaVersion().build(); jp.setClasspath(classpath); return jp; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java index f0be33256..bdd0343a2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java @@ -549,6 +549,11 @@ public class SpringSymbolIndex implements InitializingBean { .map(enhanced -> enhanced.getSymbol()); } + public List getEnhancedSymbols(IJavaProject project) { + List list = symbolsByProject.get(project.getElementName()); + return list == null ? Collections.emptyList() : Collections.unmodifiableList(list); + } + synchronized private CompletableFuture projectInitializedFuture(IJavaProject project) { if (project == null) { return CompletableFuture.completedFuture(null); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index 90a9cab3d..c6c86bfe8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -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"; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index b12efe1e0..a90e9722b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -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); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigBeanSymbolAddOnInformation.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigBeanSymbolAddOnInformation.java new file mode 100644 index 000000000..58beddf04 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigBeanSymbolAddOnInformation.java @@ -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); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java index f5c71a62e..a7a5d5949 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteReconciler.java @@ -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 cus = ORAstUtils.parseInputs(javaParser, docs.stream().map(d -> new Parser.Input(Path.of(d.getUri()), () -> { + List 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())); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java index 27634e917..26db3a937 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java @@ -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 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 fixListBuilder = ImmutableList.builder(); + List 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 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 allFQTypes(JavaType type) { + ImmutableSet.Builder 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; + } + }