diff --git a/headless-services/commons/commons-rewrite/pom.xml b/headless-services/commons/commons-rewrite/pom.xml
index 28cc187b96..3b87d422af 100644
--- a/headless-services/commons/commons-rewrite/pom.xml
+++ b/headless-services/commons/commons-rewrite/pom.xml
@@ -110,6 +110,12 @@
rewrite-spring
+
+ org.openrewrite
+ rewrite-test
+ test
+
+
org.springframework.ide.vscode
commons-maven
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java
index e9deeda714..26c72959a0 100644
--- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/ORDocUtils.java
@@ -21,6 +21,7 @@
import org.eclipse.lsp4j.DeleteFile;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.ResourceOperation;
import org.eclipse.lsp4j.TextDocumentEdit;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
@@ -72,6 +73,44 @@ public static Optional computeEdits(IDocument doc, Result result)
}
+ public static Optional computeDocumentEdits(WorkspaceEdit we, IDocument doc) {
+ if (!we.getDocumentChanges().isEmpty()) {
+ DocumentEdits edits = new DocumentEdits(doc, false);
+ List> changes = we.getDocumentChanges();
+ for (Either change : changes) {
+ if (change.isLeft()) {
+ TextDocumentEdit textDocumentEdit = change.getLeft();
+ List textEdits = textDocumentEdit.getEdits();
+ for (TextEdit textEdit : textEdits) {
+ Range range = textEdit.getRange();
+ Position start = range.getStart();
+ Position end = range.getEnd();
+ String newText = textEdit.getNewText();
+
+ try {
+ int startOffset = doc.getLineOffset(start.getLine()) + start.getCharacter();
+ int endOffset = doc.getLineOffset(end.getLine()) + end.getCharacter();
+
+ if (startOffset == endOffset) {
+ edits.insert(startOffset, newText);
+ } else if (newText.isEmpty()) {
+ edits.delete(startOffset, endOffset);
+ } else {
+ edits.replace(startOffset, endOffset, newText);
+ }
+ } catch (BadLocationException ex) {
+ log.error("Failed to apply text edit", ex);
+ }
+ }
+ }
+ }
+
+ return Optional.of(edits);
+ }
+ return Optional.empty();
+
+ }
+
public static Optional computeTextDocEdit(TextDocument doc, String oldContent, String newContent, String changeAnnotationId) {
TextDocument newDoc = new TextDocument(null, LanguageId.PLAINTEXT, 0, newContent);
@@ -161,7 +200,7 @@ public static Optional createWorkspaceEdit(SimpleTextDocumentServ
}
WorkspaceEdit we = new WorkspaceEdit();
we.setDocumentChanges(new ArrayList<>());
- for (Result result : results) {
+ for (Result result : results) {
if (result.getBefore() == null) {
String docUri = result.getAfter().getSourcePath().toUri().toASCIIString();
createNewFileEdit(docUri, result.getAfter().printAll(), changeAnnotationId, we);
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/AddFieldRecipe.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/AddFieldRecipe.java
new file mode 100644
index 0000000000..6b2ba79fcc
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/AddFieldRecipe.java
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.commons.rewrite.java;
+
+import org.openrewrite.Cursor;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.lang.NonNull;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.TypeUtils;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author Udayani V
+ */
+public class AddFieldRecipe extends Recipe {
+
+ @Override
+ public String getDisplayName() {
+ return "Add field";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Add field desccription.";
+ }
+
+ @NonNull
+ @Nullable
+ String fullyQualifiedName;
+
+ @NonNull
+ @Nullable
+ String classFqName;
+
+ @JsonCreator
+ public AddFieldRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedName, @NonNull @JsonProperty("classFqName") String classFqName) {
+ this.fullyQualifiedName = fullyQualifiedName;
+ this.classFqName = classFqName;
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+
+ return new JavaIsoVisitor() {
+
+ JavaType.FullyQualified fullyQualifiedType = JavaType.ShallowClass.build(fullyQualifiedName);
+ String fieldType = getFieldType(fullyQualifiedType);
+ String fieldName = getFieldName(fullyQualifiedType);
+
+ private final JavaTemplate fieldTemplate = JavaTemplate.builder("private final %s %s;"
+ .formatted(fieldType, fieldName))
+ .javaParser(JavaParser.fromJavaVersion()
+ .dependsOn(
+ """
+ package %s;
+
+ public interface %s {}
+ """.formatted(fullyQualifiedType.getPackageName(), fullyQualifiedType.getClassName()),
+ """
+ package %s;
+
+ public class A {
+ public class %s {
+
+ }
+ }
+ """.formatted(fullyQualifiedType.getPackageName(), fullyQualifiedType.getClassName()))
+ )
+ .imports(fullyQualifiedType.getFullyQualifiedName())
+ .contextSensitive()
+ .build();
+
+ @Override
+ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
+ if (TypeUtils.isOfClassType(classDecl.getType(), classFqName)) {
+
+ // Check if the class already has the field
+ boolean hasOwnerRepoField = classDecl.getBody().getStatements().stream()
+ .filter(J.VariableDeclarations.class::isInstance).map(J.VariableDeclarations.class::cast)
+ .anyMatch(varDecl -> varDecl.getTypeExpression() != null
+ && varDecl.getTypeExpression().toString().equals(fieldType));
+
+ if (!hasOwnerRepoField) {
+ classDecl = classDecl.withBody(fieldTemplate.apply(new Cursor(getCursor(), classDecl.getBody()),
+ classDecl.getBody().getCoordinates().firstStatement()));
+
+ maybeAddImport(fullyQualifiedType.getFullyQualifiedName(), false);
+ }
+ return classDecl;
+ }
+ classDecl = (J.ClassDeclaration) super.visitClassDeclaration(classDecl, ctx);
+ return classDecl;
+ }
+ };
+ }
+
+ private static String getFieldName(JavaType.FullyQualified fullyQualifiedType) {
+ return Character.toLowerCase(fullyQualifiedType.getClassName().charAt(0)) + fullyQualifiedType.getClassName().substring(1);
+ }
+
+ private static String getFieldType(JavaType.FullyQualified fullyQualifiedType) {
+ if(fullyQualifiedType.getOwningClass() != null) {
+ String[] parts = fullyQualifiedType.getFullyQualifiedName().split("\\.");
+ if (parts.length < 2) {
+ return fullyQualifiedType.getClassName();
+ }
+ return parts[parts.length - 2] + "." + parts[parts.length - 1];
+ }
+
+ return fullyQualifiedType.getClassName();
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java
new file mode 100644
index 0000000000..96430f02ae
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipe.java
@@ -0,0 +1,292 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.commons.rewrite.java;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.openrewrite.Cursor;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.NlsRewrite.Description;
+import org.openrewrite.NlsRewrite.DisplayName;
+import org.openrewrite.Recipe;
+import org.openrewrite.Tree;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.internal.lang.NonNull;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.JavaVisitor;
+import org.openrewrite.java.tree.Expression;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.J.Block;
+import org.openrewrite.java.tree.J.ClassDeclaration;
+import org.openrewrite.java.tree.J.MethodDeclaration;
+import org.openrewrite.java.tree.J.VariableDeclarations;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.JavaType.FullyQualified;
+import org.openrewrite.java.tree.Space;
+import org.openrewrite.java.tree.Statement;
+import org.openrewrite.java.tree.TypeTree;
+import org.openrewrite.java.tree.TypeUtils;
+import org.openrewrite.marker.Markers;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author Udayani V
+ */
+public class ConstructorInjectionRecipe extends Recipe {
+
+ @Override
+ public @DisplayName String getDisplayName() {
+ return "Add bean injection";
+ }
+
+ @Override
+ public @Description String getDescription() {
+ return "Add bean injection.";
+ }
+
+ @NonNull
+ @Nullable
+ String fullyQualifiedName;
+
+ @NonNull
+ @Nullable
+ String fieldName;
+
+ @NonNull
+ @Nullable
+ String classFqName;
+
+ @JsonCreator
+ public ConstructorInjectionRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedName,
+ @NonNull @JsonProperty("fieldName") String fieldName,
+ @NonNull @JsonProperty("classFqName") String classFqName) {
+ this.fullyQualifiedName = fullyQualifiedName;
+ this.fieldName = fieldName;
+ this.classFqName = classFqName;
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+
+ return new CustomFieldIntoConstructorParameterVisitor(classFqName, fieldName);
+ }
+
+ class CustomFieldIntoConstructorParameterVisitor extends JavaVisitor {
+
+ private final String classFqName;
+ private final String fieldName;
+ private static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired";
+
+ public CustomFieldIntoConstructorParameterVisitor(String classFqName, String fieldName) {
+ this.classFqName = classFqName;
+ this.fieldName = fieldName;
+ }
+
+ @Override
+ public J visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
+
+ if (TypeUtils.isOfClassType(classDecl.getType(), classFqName)) {
+ List constructors = classDecl.getBody().getStatements().stream()
+ .filter(J.MethodDeclaration.class::isInstance).map(J.MethodDeclaration.class::cast)
+ .filter(MethodDeclaration::isConstructor).collect(Collectors.toList());
+ boolean applicable = false;
+ if (constructors.isEmpty()) {
+ applicable = true;
+ } else if (constructors.size() == 1) {
+ MethodDeclaration c = constructors.get(0);
+ getCursor().putMessage("applicableConstructor", c);
+ applicable = isNotConstructorInitializingField(c, fieldName);
+ } else {
+ List autowiredConstructors = constructors.stream()
+ .filter(constr -> constr.getLeadingAnnotations().stream()
+ .map(a -> TypeUtils.asFullyQualified(a.getType())).filter(Objects::nonNull)
+ .map(FullyQualified::getFullyQualifiedName).anyMatch(AUTOWIRED::equals))
+ .limit(2).collect(Collectors.toList());
+ if (autowiredConstructors.size() == 1) {
+ MethodDeclaration c = autowiredConstructors.get(0);
+ getCursor().putMessage("applicableConstructor", autowiredConstructors.get(0));
+ applicable = isNotConstructorInitializingField(c, fieldName);
+ }
+ }
+ if (applicable) {
+ return super.visitClassDeclaration(classDecl, ctx);
+ }
+ }
+ return super.visitClassDeclaration(classDecl, ctx);
+ }
+
+ public static boolean isNotConstructorInitializingField(MethodDeclaration c, String fieldName) {
+ return c.getBody() == null || c.getBody().getStatements().stream().filter(J.Assignment.class::isInstance)
+ .map(J.Assignment.class::cast).noneMatch(a -> {
+ Expression expr = a.getVariable();
+ if (expr instanceof J.FieldAccess) {
+ J.FieldAccess fa = (J.FieldAccess) expr;
+ if (fieldName.equals(fa.getSimpleName()) && fa.getTarget() instanceof J.Identifier) {
+ J.Identifier target = (J.Identifier) fa.getTarget();
+ if ("this".equals(target.getSimpleName())) {
+ return true;
+ }
+ }
+ }
+ if (expr instanceof J.Identifier) {
+ JavaType.Variable fieldType = c.getMethodType().getDeclaringType().getMembers().stream()
+ .filter(v -> fieldName.equals(v.getName())).findFirst().orElse(null);
+ if (fieldType != null) {
+ J.Identifier identifier = (J.Identifier) expr;
+ return fieldType.equals(identifier.getFieldType());
+ }
+ }
+ return false;
+ });
+ }
+
+ @Override
+ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable,
+ ExecutionContext ctx) {
+
+ Cursor blockCursor = getCursor().dropParentUntil(it -> it instanceof J.Block || it == Cursor.ROOT_VALUE);
+ if (!(blockCursor.getValue() instanceof J.Block)) {
+ return multiVariable;
+ }
+ VariableDeclarations mv = multiVariable;
+ if (blockCursor.getParent() != null && blockCursor.getParent().getValue() instanceof ClassDeclaration
+ && multiVariable.getVariables().size() == 1
+ && fieldName.equals(multiVariable.getVariables().get(0).getName().getSimpleName())) {
+ if (mv.getModifiers().stream().noneMatch(m -> m.getType() == J.Modifier.Type.Final)) {
+ Space prefix = Space.firstPrefix(mv.getVariables());
+ J.Modifier m = new J.Modifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null,
+ J.Modifier.Type.Final, Collections.emptyList());
+ if (mv.getModifiers().isEmpty()) {
+ mv = mv.withTypeExpression(mv.getTypeExpression().withPrefix(prefix));
+ } else {
+ m = m.withPrefix(prefix);
+ }
+ mv = mv.withModifiers(ListUtils.concat(mv.getModifiers(), m));
+ }
+ MethodDeclaration constructor = blockCursor.getParent().getMessage("applicableConstructor");
+ ClassDeclaration c = blockCursor.getParent().getValue();
+ TypeTree fieldType = TypeTree.build(fullyQualifiedName);
+ if (constructor == null) {
+ doAfterVisit(new AddConstructorVisitor(c.getSimpleName(), fieldName, fieldType));
+ } else {
+ doAfterVisit(new AddConstructorParameterAndAssignment(constructor, fieldName, fieldType));
+ }
+ }
+ return mv;
+ }
+ }
+
+ private static class AddConstructorVisitor extends JavaVisitor {
+ private final String className;
+ private final String fieldName;
+ private final TypeTree type;
+
+ public AddConstructorVisitor(String className, String fieldName, TypeTree type) {
+ this.className = className;
+ this.fieldName = fieldName;
+ this.type = type;
+ }
+
+ @Override
+ public J visitBlock(Block block, ExecutionContext p) {
+ J result = (Block) super.visitBlock(block, p);
+ if (getCursor().getParent() != null) {
+ Object n = getCursor().getParent().getValue();
+ if (n instanceof ClassDeclaration) {
+ ClassDeclaration classDecl = (ClassDeclaration) n;
+ JavaType.FullyQualified typeFqn = TypeUtils.asFullyQualified(type.getType());
+ if (typeFqn != null && classDecl.getKind() == ClassDeclaration.Kind.Type.Class
+ && className.equals(classDecl.getSimpleName())) {
+ JavaTemplate.Builder template = JavaTemplate.builder(""
+ + classDecl.getSimpleName() + "(" + getFieldType(typeFqn) + " " + fieldName + ") {\n"
+ + "this." + fieldName + " = " + fieldName + ";\n"
+ + "}\n"
+ ).contextSensitive();
+ FullyQualified fq = TypeUtils.asFullyQualified(type.getType());
+ if (fq != null) {
+ template.imports(fq.getFullyQualifiedName());
+ maybeAddImport(fq);
+ }
+ Optional firstMethod = block.getStatements().stream()
+ .filter(MethodDeclaration.class::isInstance).findFirst();
+
+ return firstMethod
+ .map(statement -> (J) template.build().apply(getCursor(),
+ statement.getCoordinates().before()))
+ .orElseGet(() -> template.build().apply(getCursor(),
+ block.getCoordinates().lastStatement()));
+ }
+ }
+ }
+ return result;
+ }
+ }
+
+ private static class AddConstructorParameterAndAssignment extends JavaIsoVisitor {
+ private final MethodDeclaration constructor;
+ private final String fieldName;
+ private final String methodType;
+
+ public AddConstructorParameterAndAssignment(MethodDeclaration constructor, String fieldName, TypeTree type) {
+ this.constructor = constructor;
+ this.fieldName = fieldName;
+ JavaType.FullyQualified fq = TypeUtils.asFullyQualified(type.getType());
+ if (fq != null) {
+ methodType = getFieldType(fq);
+ } else {
+ throw new IllegalArgumentException("Unable to determine parameter type");
+ }
+ }
+
+ @Override
+ public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, ExecutionContext p) {
+ J.MethodDeclaration md = super.visitMethodDeclaration(method, p);
+ if (md == this.constructor && md.getBody() != null) {
+ List params = md.getParameters().stream().filter(s -> !(s instanceof J.Empty))
+ .collect(Collectors.toList());
+ String paramsStr = Stream
+ .concat(params.stream().map(s -> "#{}"), Stream.of(methodType + " " + fieldName))
+ .collect(Collectors.joining(", "));
+
+ md = JavaTemplate.builder(paramsStr).contextSensitive().build().apply(getCursor(),
+ md.getCoordinates().replaceParameters(), params.toArray());
+ updateCursor(md);
+
+ // noinspection ConstantConditions
+ md = JavaTemplate.builder("this." + fieldName + " = " + fieldName + ";").contextSensitive().build()
+ .apply(getCursor(), md.getBody().getCoordinates().lastStatement());
+ }
+ return md;
+ }
+ }
+
+ private static String getFieldType(JavaType.FullyQualified fullyQualifiedType) {
+ if (fullyQualifiedType.getOwningClass() != null) {
+ String[] parts = fullyQualifiedType.getFullyQualifiedName().split("\\.");
+ if (parts.length < 2) {
+ return fullyQualifiedType.getClassName();
+ }
+ return parts[parts.length - 2] + "." + parts[parts.length - 1];
+ }
+
+ return fullyQualifiedType.getClassName();
+ }
+}
\ No newline at end of file
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/InjectBeanCompletionRecipe.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/InjectBeanCompletionRecipe.java
new file mode 100644
index 0000000000..59d29aab91
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/InjectBeanCompletionRecipe.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.commons.rewrite.java;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openrewrite.NlsRewrite.Description;
+import org.openrewrite.NlsRewrite.DisplayName;
+import org.openrewrite.Recipe;
+import org.openrewrite.internal.lang.NonNull;
+import org.openrewrite.internal.lang.Nullable;
+
+/**
+ * @author Udayani V
+ */
+public class InjectBeanCompletionRecipe extends Recipe {
+
+ @Override
+ public @DisplayName String getDisplayName() {
+ return "Inject bean completions";
+ }
+
+ @Override
+ public @Description String getDescription() {
+ return "Automates the injection of a specified bean into Spring components by adding the necessary field and import, creating the constructor if it doesn't exist, and injecting the bean as a constructor parameter.";
+ }
+
+ @NonNull
+ @Nullable
+ String fullyQualifiedName;
+
+ @NonNull
+ @Nullable
+ String fieldName;
+
+ @NonNull
+ @Nullable
+ String classFqName;
+
+ public InjectBeanCompletionRecipe(String fullyQualifiedName, String fieldName, String classFqName) {
+ this.fullyQualifiedName = fullyQualifiedName;
+ this.fieldName = fieldName;
+ this.classFqName = classFqName;
+ }
+
+ @Override
+ public List getRecipeList() {
+ List list = new ArrayList<>();
+ list.add(new AddFieldRecipe(fullyQualifiedName, classFqName));
+ list.add(new ConstructorInjectionRecipe(fullyQualifiedName, fieldName, classFqName));
+ return list;
+ }
+
+}
diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/AddFieldRecipeTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/AddFieldRecipeTest.java
new file mode 100644
index 0000000000..534107b19b
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/AddFieldRecipeTest.java
@@ -0,0 +1,393 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.commons.rewrite.java;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.InMemoryExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.RecipeRun;
+import org.openrewrite.SourceFile;
+import org.openrewrite.internal.InMemoryLargeSourceSet;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+import org.openrewrite.tree.ParseError;
+
+public class AddFieldRecipeTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new AddFieldRecipe("com.example.test.OwnerRepository", "com.example.demo.FooBar"))
+ .parser(JavaParser.fromJavaVersion()
+ .logCompilationWarningsAndErrors(true));
+ }
+
+ public static void runRecipeAndAssert(Recipe recipe, String beforeSourceStr, String sourceStrPassed, String expectedSourceStr, String dependsOn) {
+ JavaParser javaParser = JavaParser.fromJavaVersion().dependsOn(dependsOn).build();
+
+ List list = javaParser.parse(beforeSourceStr).map(sf -> {
+ if (sf instanceof ParseError pe) {
+ return pe.getErroneous();
+ }
+ return sf;
+ }).toList();
+ SourceFile beforeSource = list.get(0);
+
+ assertThat(beforeSource.printAll()).isEqualTo(sourceStrPassed);
+
+ InMemoryLargeSourceSet ss = new InMemoryLargeSourceSet(list);
+ RecipeRun recipeRun = recipe.run(ss, new InMemoryExecutionContext(t -> {
+ throw new RuntimeException(t);
+ }));
+ org.openrewrite.Result res = recipeRun.getChangeset().getAllResults().get(0);
+ assertThat(res.getAfter().printAll()).isEqualTo(expectedSourceStr);
+ }
+
+ // The test parses invalid LST and then applies the recipe
+ @Test
+ void addField() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {
+ ownerR
+
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {}
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ class FooBar {
+
+ private final OwnerRepository ownerRepository;
+
+ public void test() {}
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.demo;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new AddFieldRecipe("com.example.demo.OwnerRepository", "com.example.demo.FooBar");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void addFieldAndImport() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {
+ ownerR
+
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {}
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ class FooBar {
+
+ private final OwnerRepository ownerRepository;
+
+ public void test() {}
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new AddFieldRecipe("com.example.test.OwnerRepository", "com.example.demo.FooBar");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void addNestedField() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {
+ ownerR
+
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {}
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ class FooBar {
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ public void test() {}
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new AddFieldRecipe("com.example.test.Inner.OwnerRepository", "com.example.demo.FooBar");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void addToNestedComponent() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ class FooBar {
+ class Inner {
+ public void test() {
+ ownerR
+ }
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ class FooBar {
+ class Inner {
+ public void test() {}
+ }
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+import com.example.test.OwnerRepository;
+
+class FooBar {
+ class Inner {
+ private final OwnerRepository ownerRepository;
+ public void test() {}
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new AddFieldRecipe("com.example.test.OwnerRepository", "com.example.demo.FooBar$Inner");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void addFieldToFirstClass() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {
+ ownerR
+
+ }
+
+ }
+ class FooBarNew {
+
+ public void test1() {}
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ class FooBar {
+
+ public void test() {}
+
+ }
+ class FooBarNew {
+
+ public void test1() {}
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ class FooBar {
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ public void test() {}
+
+ }
+ class FooBarNew {
+
+ public void test1() {}
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new AddFieldRecipe("com.example.test.Inner.OwnerRepository", "com.example.demo.FooBar");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void addFieldToSecondClass() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import org.springframework.stereotype.Component;
+
+ @Component
+ class FooBar {
+
+ public void test() {
+ ownerR
+
+ }
+
+ }
+ @Component
+ class FooBarNew {
+
+ public void test1() {}
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import org.springframework.stereotype.Component;
+
+ @Component
+ class FooBar {
+
+ public void test() {}
+
+ }
+ @Component
+ class FooBarNew {
+
+ public void test1() {}
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+import org.springframework.stereotype.Component;
+
+@Component
+ class FooBar {
+
+ public void test() {}
+
+ }
+ @Component
+ class FooBarNew {
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ public void test1() {}
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new AddFieldRecipe("com.example.test.Inner.OwnerRepository", "com.example.demo.FooBarNew");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+}
diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipeTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipeTest.java
new file mode 100644
index 0000000000..3f765cc106
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/ConstructorInjectionRecipeTest.java
@@ -0,0 +1,649 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.commons.rewrite.java;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.InMemoryExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.RecipeRun;
+import org.openrewrite.SourceFile;
+import org.openrewrite.internal.InMemoryLargeSourceSet;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+import org.openrewrite.tree.ParseError;
+
+public class ConstructorInjectionRecipeTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A"))
+ .parser(JavaParser.fromJavaVersion().classpath("spring-beans"));
+ }
+
+ public static void runRecipeAndAssert(Recipe recipe, String beforeSourceStr, String sourceStrPassed, String expectedSourceStr, String dependsOn) {
+ JavaParser javaParser = JavaParser.fromJavaVersion().dependsOn(dependsOn).build();
+
+ List list = javaParser.parse(beforeSourceStr).map(sf -> {
+ if (sf instanceof ParseError pe) {
+ return pe.getErroneous();
+ }
+ return sf;
+ }).toList();
+ SourceFile beforeSource = list.get(0);
+
+ assertThat(beforeSource.printAll()).isEqualTo(sourceStrPassed);
+
+ InMemoryLargeSourceSet ss = new InMemoryLargeSourceSet(list);
+ RecipeRun recipeRun = recipe.run(ss, new InMemoryExecutionContext(t -> {
+ throw new RuntimeException(t);
+ }));
+ org.openrewrite.Result res = recipeRun.getChangeset().getAllResults().get(0);
+ assertThat(res.getAfter().printAll()).isEqualTo(expectedSourceStr);
+ }
+
+ @Test
+ void injectFieldIntoNewConstructor() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ A(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void injectFieldIntoExistingSingleConstructor() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ A() {
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ A() {
+ }
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ A(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void injectFieldIntoAutowiredConstructor() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+ import org.springframework.beans.factory.annotation.Autowired;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ @Autowired
+ A() {
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+ import org.springframework.beans.factory.annotation.Autowired;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ @Autowired
+ A() {
+ }
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+ import org.springframework.beans.factory.annotation.Autowired;
+
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ @Autowired
+ A(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void injectFieldIntoExistingConstructorWithFields() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ String a;
+
+ private final OwnerRepository ownerRepository;
+
+ A(String a) {
+ this.a = a;
+ }
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ String a;
+
+ private final OwnerRepository ownerRepository;
+
+ A(String a) {
+ this.a = a;
+ }
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+
+ String a;
+
+ private final OwnerRepository ownerRepository;
+
+ A(String a, OwnerRepository ownerRepository) {
+this.a = a;
+ this.ownerRepository = ownerRepository;
+ }
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void injectInnerClassFieldIntoExistingConstructorWithFields() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ public class A {
+
+ String a;
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ A(String a) {
+ this.a = a;
+ }
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ public class A {
+
+ String a;
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ A(String a) {
+ this.a = a;
+ }
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ public class A {
+
+ String a;
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ A(String a, Inner.OwnerRepository ownerRepository) {
+this.a = a;
+ this.ownerRepository = ownerRepository;
+ }
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public class Inner {
+ public static class OwnerRepository{}
+ }
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.Inner.OwnerRepository", "ownerRepository", "com.example.demo.A");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void injectInnerClassFieldIntoNewConstructor() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ public class A {
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ public class A {
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.Inner.OwnerRepository;
+
+ public class A {
+
+ private final Inner.OwnerRepository ownerRepository;
+
+ A(Inner.OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public class Inner {
+ public static class OwnerRepository{}
+ }
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.Inner.OwnerRepository", "ownerRepository", "com.example.demo.A");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void nestedClass_InjectFieldIntoNewConstructor() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+ public class Inner {
+ String a;
+ private final OwnerRepository ownerRepository;
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+ public class Inner {
+ String a;
+ private final OwnerRepository ownerRepository;
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+public class Inner {
+ String a;
+ private final OwnerRepository ownerRepository;
+
+ Inner(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A$Inner");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void nestedClass_InjectFieldIntoExistingConstructorWithFields() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+ public class Inner {
+ String a;
+ private final OwnerRepository ownerRepository;
+
+ Inner(String a) {
+ this.a = a;
+ }
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+ public class Inner {
+ String a;
+ private final OwnerRepository ownerRepository;
+
+ Inner(String a) {
+ this.a = a;
+ }
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+public class Inner {
+ String a;
+ private final OwnerRepository ownerRepository;
+
+ Inner(String a, OwnerRepository ownerRepository) {
+ this.a = a;
+ this.ownerRepository = ownerRepository;
+ }
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A$Inner");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+ @Test
+ void nestedClass_InjectFieldIntoExistingSingleConstructor() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+ int param;
+ A(int param) {
+ this.param = param;
+ }
+ public class Inner {
+ private final OwnerRepository ownerRepository;
+
+ Inner() {
+ }
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+ int param;
+ A(int param) {
+ this.param = param;
+ }
+ public class Inner {
+ private final OwnerRepository ownerRepository;
+
+ Inner() {
+ }
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+
+ public class A {
+int param;
+A(int param) {
+ this.param = param;
+}
+public class Inner {
+ private final OwnerRepository ownerRepository;
+
+ Inner(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ public void test() {
+
+ }
+ }
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A$Inner");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+}
diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/InjectBeanCompletionRecipeTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/InjectBeanCompletionRecipeTest.java
new file mode 100644
index 0000000000..10d18935e5
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/InjectBeanCompletionRecipeTest.java
@@ -0,0 +1,119 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.commons.rewrite.java;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.InMemoryExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.RecipeRun;
+import org.openrewrite.SourceFile;
+import org.openrewrite.internal.InMemoryLargeSourceSet;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+import org.openrewrite.tree.ParseError;
+
+public class InjectBeanCompletionRecipeTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new ConstructorInjectionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A"))
+ .parser(JavaParser.fromJavaVersion().classpath("spring-beans"));
+ }
+
+ public static void runRecipeAndAssert(Recipe recipe, String beforeSourceStr, String sourceStrPassed, String expectedSourceStr, String dependsOn) {
+ JavaParser javaParser = JavaParser.fromJavaVersion().dependsOn(dependsOn).build();
+
+ List list = javaParser.parse(beforeSourceStr).map(sf -> {
+ if (sf instanceof ParseError pe) {
+ return pe.getErroneous();
+ }
+ return sf;
+ }).toList();
+ SourceFile beforeSource = list.get(0);
+
+ assertThat(beforeSource.printAll()).isEqualTo(sourceStrPassed);
+
+ InMemoryLargeSourceSet ss = new InMemoryLargeSourceSet(list);
+ RecipeRun recipeRun = recipe.run(ss, new InMemoryExecutionContext(t -> {
+ throw new RuntimeException(t);
+ }));
+ org.openrewrite.Result res = recipeRun.getChangeset().getAllResults().get(0);
+ assertThat(res.getAfter().printAll()).isEqualTo(expectedSourceStr);
+ }
+
+ @Test
+ void injectFieldIntoNewConstructor() {
+
+ String beforeSourceStr = """
+ package com.example.demo;
+
+ import org.springframework.stereotype.Controller;
+
+ @Controller
+ public class A {
+
+ public void test() {
+ }
+
+ }
+ """;
+
+ String sourceStrPassed = """
+ package com.example.demo;
+
+ import org.springframework.stereotype.Controller;
+
+ @Controller
+ public class A {
+
+ public void test() {
+ }
+
+ }
+ """;
+
+ String expectedSourceStr = """
+ package com.example.demo;
+
+ import com.example.test.OwnerRepository;
+import org.springframework.stereotype.Controller;
+
+@Controller
+ public class A {
+
+ private final OwnerRepository ownerRepository;
+
+ A(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+public void test() {
+}
+
+ }
+ """;
+
+ String dependsOn = """
+ package com.example.test;
+ public interface OwnerRepository{}
+ """;
+
+ Recipe recipe = new InjectBeanCompletionRecipe("com.example.test.OwnerRepository", "ownerRepository", "com.example.demo.A");
+ runRecipeAndAssert(recipe, beforeSourceStr, sourceStrPassed, expectedSourceStr, dependsOn);
+ }
+
+
+}
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java
index 50b739412a..2855ef2d3a 100644
--- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java
@@ -25,6 +25,7 @@
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProcessor;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
+import org.springframework.ide.vscode.boot.java.beans.BeanCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider;
@@ -36,6 +37,7 @@
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
+import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
import org.springframework.ide.vscode.boot.java.scope.ScopeCompletionProcessor;
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippet;
import org.springframework.ide.vscode.boot.java.snippets.JavaSnippetContext;
@@ -111,7 +113,8 @@ BootJavaCompletionEngine javaCompletionEngine(
@Qualifier("adHocProperties") ProjectBasedPropertyIndexProvider adHocProperties,
JavaSnippetManager snippetManager,
CompilationUnitCache cuCache,
- SpringMetamodelIndex springIndex) {
+ SpringMetamodelIndex springIndex,
+ RewriteRefactorings rewriteRefactorings ) {
SpringPropertyIndexProvider indexProvider = params.indexProvider;
JavaProjectFinder javaProjectFinder = params.projectFinder;
@@ -126,7 +129,8 @@ BootJavaCompletionEngine javaCompletionEngine(
providers.put(Annotations.SCOPE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ScopeCompletionProcessor())));
providers.put(Annotations.DEPENDS_ON, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new DependsOnCompletionProcessor(springIndex))));
providers.put(Annotations.QUALIFIER, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new QualifierCompletionProvider(springIndex))));
- providers.put(Annotations.PROFILE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ProfileCompletionProvider(springIndex))));
+ providers.put(Annotations.BEAN, new BeanCompletionProvider(javaProjectFinder, springIndex, rewriteRefactorings));
+
providers.put(Annotations.RESOURCE_JAVAX, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("name", new ResourceCompletionProvider(springIndex))));
providers.put(Annotations.RESOURCE_JAKARTA, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("name", new ResourceCompletionProvider(springIndex))));
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java
new file mode 100644
index 0000000000..5e48e7fbd6
--- /dev/null
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProposal.java
@@ -0,0 +1,124 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.beans;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import org.eclipse.lsp4j.CompletionItemKind;
+import org.eclipse.lsp4j.WorkspaceEdit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
+import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
+import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
+import org.springframework.ide.vscode.commons.rewrite.ORDocUtils;
+import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
+import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
+import org.springframework.ide.vscode.commons.rewrite.java.InjectBeanCompletionRecipe;
+import org.springframework.ide.vscode.commons.util.Renderable;
+import org.springframework.ide.vscode.commons.util.text.IDocument;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+
+/**
+ * @author Udayani V
+ */
+public class BeanCompletionProposal implements ICompletionProposal {
+
+ private static final Logger log = LoggerFactory.getLogger(BeanCompletionProposal.class);
+
+ private DocumentEdits edits;
+ private IDocument doc;
+ private String label;
+ private String detail;
+ private String fieldType;
+ private String className;
+ private Renderable documentation;
+ private RewriteRefactorings rewriteRefactorings;
+
+ private Gson gson;
+
+ public BeanCompletionProposal(DocumentEdits edits, IDocument doc, String label, String detail, String fieldType, String className,
+ Renderable documentation, RewriteRefactorings rewriteRefactorings) {
+ this.edits = edits;
+ this.doc = doc;
+ this.label = label;
+ this.detail = detail;
+ this.fieldType = fieldType;
+ this.className = className;
+ this.documentation = documentation;
+ this.rewriteRefactorings = rewriteRefactorings;
+ this.gson = new GsonBuilder()
+ .registerTypeAdapter(RecipeScope.class, (JsonDeserializer) (json, type, context) -> {
+ try {
+ return RecipeScope.values()[json.getAsInt()];
+ } catch (Exception e) {
+ return null;
+ }
+ })
+ .create();
+ }
+
+ @Override
+ public String getLabel() {
+ return this.label;
+ }
+
+ @Override
+ public CompletionItemKind getKind() {
+ return CompletionItemKind.Constructor;
+ }
+
+ @Override
+ public DocumentEdits getTextEdit() {
+ return this.edits;
+ }
+
+ @Override
+ public String getDetail() {
+ return this.detail;
+ }
+
+ @Override
+ public Renderable getDocumentation() {
+ return this.documentation;
+ }
+
+ @Override
+ public Optional> getAdditionalEdit() {
+ return Optional.of(() -> {
+ try {
+ FixDescriptor f = new FixDescriptor(InjectBeanCompletionRecipe.class.getName(), List.of(this.doc.getUri()),"Inject bean completions")
+ .withParameters(Map.of("fullyQualifiedName", this.fieldType, "fieldName", this.label, "classFqName",this.className))
+ .withRecipeScope(RecipeScope.NODE);
+ JsonElement jsonElement = gson.toJsonTree(f);
+ CompletableFuture workspaceEdits = this.rewriteRefactorings.createEdit(jsonElement);
+
+ CompletableFuture> docEditsFuture = workspaceEdits.thenApply(workspaceEdit -> {
+ Optional docEdits = ORDocUtils.computeDocumentEdits(workspaceEdit, doc);
+ return docEdits;
+
+ });
+ return docEditsFuture.get().orElse(null);
+ } catch (InterruptedException | ExecutionException e) {
+ log.error("" + e);
+ return null;
+ }
+ });
+ }
+}
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProvider.java
new file mode 100644
index 0000000000..49634f5f10
--- /dev/null
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanCompletionProvider.java
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.beans;
+
+import java.util.Collection;
+import java.util.Optional;
+
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.IAnnotationBinding;
+import org.eclipse.jdt.core.dom.SimpleName;
+import org.eclipse.jdt.core.dom.TypeDeclaration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
+import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
+import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
+import org.springframework.ide.vscode.commons.java.IJavaProject;
+import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
+import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
+import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
+import org.springframework.ide.vscode.commons.protocol.spring.Bean;
+import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
+import org.springframework.ide.vscode.commons.util.text.TextDocument;
+
+/**
+ * @author Udayani V
+ */
+public class BeanCompletionProvider implements CompletionProvider {
+
+ private static final Logger log = LoggerFactory.getLogger(BeanCompletionProvider.class);
+
+ private final JavaProjectFinder javaProjectFinder;
+ private final SpringMetamodelIndex springIndex;
+ private final RewriteRefactorings rewriteRefactorings;
+
+ public BeanCompletionProvider(JavaProjectFinder javaProjectFinder, SpringMetamodelIndex springIndex,
+ RewriteRefactorings rewriteRefactorings) {
+ this.javaProjectFinder = javaProjectFinder;
+ this.springIndex = springIndex;
+ this.rewriteRefactorings = rewriteRefactorings;
+ }
+
+ @Override
+ public void provideCompletions(ASTNode node, int offset, TextDocument doc,
+ Collection completions) {
+ try {
+ Optional optionalProject = this.javaProjectFinder.find(doc.getId());
+ if (optionalProject.isEmpty()) {
+ return;
+ }
+
+ IJavaProject project = optionalProject.get();
+ TypeDeclaration topLevelClass = findParentClass(node);
+ if (topLevelClass == null) {
+ return;
+ }
+
+ if (node instanceof SimpleName && isSpringComponent(topLevelClass)) {
+ String className = getFullyQualifiedName(topLevelClass);
+ Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
+ for (Bean bean : beans) {
+ if (FuzzyMatcher.matchScore(node.toString(), bean.getName()) != 0.0) {
+ DocumentEdits edits = new DocumentEdits(doc, false);
+ edits.replace(offset - node.toString().length(), offset, bean.getName());
+ BeanCompletionProposal proposal = new BeanCompletionProposal(edits, doc, bean.getName(),
+ "Autowire bean", bean.getType(), className, null, rewriteRefactorings);
+ completions.add(proposal);
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.error("problem while looking for bean completions", e);
+ }
+ }
+
+ private static boolean isSpringComponent(TypeDeclaration node) {
+ for (IAnnotationBinding annotation : node.resolveBinding().getAnnotations()) {
+ if (isSpringComponentAnnotation(annotation)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isSpringComponentAnnotation(IAnnotationBinding annotation) {
+ String annotationName = annotation.getAnnotationType().getQualifiedName();
+ if (annotationName.equals("org.springframework.stereotype.Component")) {
+ return true;
+ }
+ for (IAnnotationBinding metaAnnotation : annotation.getAnnotationType().getAnnotations()) {
+ if (metaAnnotation.getAnnotationType().getQualifiedName().equals("org.springframework.stereotype.Component")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static TypeDeclaration findParentClass(ASTNode node) {
+ ASTNode current = node;
+ while (current != null) {
+ if (current instanceof TypeDeclaration) {
+ return (TypeDeclaration) current;
+ }
+ current = current.getParent();
+ }
+ return null;
+ }
+
+ private static String getFullyQualifiedName(TypeDeclaration typeDecl) {
+ if (typeDecl.resolveBinding() != null) {
+ String qualifiedName = typeDecl.resolveBinding().getQualifiedName();
+ return qualifiedName.replaceAll("\\.(?=[^\\.]+$)", "\\$");
+ }
+ CompilationUnit cu = (CompilationUnit) typeDecl.getRoot();
+ String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";
+ String typeName = typeDecl.getName().getFullyQualifiedName();
+ return packageName.isEmpty() ? typeName : packageName + "." + typeName;
+ }
+
+}
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java
index 0da6b4ba2d..c003a9fd9e 100644
--- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRecipeRepository.java
@@ -56,7 +56,6 @@
import org.openrewrite.config.YamlResourceLoader;
import org.openrewrite.internal.InMemoryLargeSourceSet;
import org.openrewrite.java.JavaParser;
-import org.openrewrite.maven.AddDependency;
import org.openrewrite.maven.MavenParser;
import org.openrewrite.tree.ParseError;
import org.slf4j.Logger;
diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java
index 2419f85c78..a74f96479a 100644
--- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java
+++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java
@@ -15,6 +15,7 @@
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
@@ -27,6 +28,7 @@
import org.openrewrite.Parser.Input;
import org.openrewrite.Recipe;
import org.openrewrite.SourceFile;
+import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.internal.RecipeIntrospectionUtils;
import org.openrewrite.java.JavaParser;
import org.openrewrite.marker.Range;
@@ -154,16 +156,8 @@ private CompletableFuture createRecipe(FixDescriptor d) {
.orElseGet(() -> recipeRepo.getRecipe(d.getRecipeId()).thenApply(opt -> opt.orElseThrow())))
.thenApply(r -> {
if (d.getParameters() != null) {
- for (Entry entry : d.getParameters().entrySet()) {
- try {
- Field f = r.getClass().getDeclaredField(entry.getKey());
- f.setAccessible(true);
- f.set(r, entry.getValue());
- } catch (Exception e) {
- log.error("", e);;
- }
- }
- }
+ setParameters(r, d.getParameters());
+ }
if (d.getRecipeScope() == RecipeScope.NODE) {
if (d.getRangeScope() == null) {
throw new IllegalArgumentException("Missing scope AST node!");
@@ -187,4 +181,41 @@ private CompletableFuture createRecipe(FixDescriptor d) {
return r;
});
}
+
+ /**
+ * Sets the parameters for a given recipe. If the recipe is a DeclarativeRecipe,
+ * it iterates over its sub-recipes and sets the parameters for each sub-recipe.
+ */
+ private void setParameters(Recipe recipe, Map parameters) {
+ if (recipe instanceof DeclarativeRecipe) {
+ List subRecipes = ((DeclarativeRecipe) recipe).getRecipeList();
+ for (Recipe subRecipe : subRecipes) {
+ setParameters(subRecipe, parameters);
+ }
+ } else {
+ for (Entry entry : parameters.entrySet()) {
+ try {
+ Field field = findField(recipe, entry.getKey());
+ if (field != null) {
+ field.setAccessible(true);
+ field.set(recipe, entry.getValue());
+ }
+ } catch (Exception e) {
+ log.error("", e);;
+ }
+ }
+ }
+ }
+
+ private Field findField(Object obj, String fieldName) {
+ Class> clazz = obj.getClass();
+ while (clazz != null) {
+ try {
+ return clazz.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ clazz = clazz.getSuperclass();
+ }
+ }
+ return null;
+ }
}
diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/BeanCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/BeanCompletionProviderTest.java
new file mode 100644
index 0000000000..2ff7f3977d
--- /dev/null
+++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/BeanCompletionProviderTest.java
@@ -0,0 +1,362 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2024 Broadcom, 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:
+ * Broadcom, Inc. - initial API and implementation
+ *******************************************************************************/
+package org.springframework.ide.vscode.boot.java.beans.test;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.lsp4j.CompletionItem;
+import org.eclipse.lsp4j.Location;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.Range;
+import org.eclipse.lsp4j.TextDocumentIdentifier;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Import;
+import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
+import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
+import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf;
+import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
+import org.springframework.ide.vscode.commons.java.IJavaProject;
+import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
+import org.springframework.ide.vscode.commons.protocol.spring.Bean;
+import org.springframework.ide.vscode.commons.util.text.LanguageId;
+import org.springframework.ide.vscode.languageserver.testharness.Editor;
+import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
+import org.springframework.ide.vscode.project.harness.ProjectsHarness;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+
+/**
+ * @author Udayani V
+ */
+@ExtendWith(SpringExtension.class)
+@BootLanguageServerTest
+@Import(SymbolProviderTestConf.class)
+public class BeanCompletionProviderTest {
+
+ @Autowired private BootLanguageServerHarness harness;
+ @Autowired private JavaProjectFinder projectFinder;
+ @Autowired private SpringMetamodelIndex springIndex;
+ @Autowired private SpringSymbolIndex indexer;
+
+ private File directory;
+ private IJavaProject project;
+ private Bean[] indexedBeans;
+ private String tempJavaDocUri;
+ private Bean bean1;
+ private Bean bean2;
+ private Bean bean3;
+ private Bean bean4;
+ private Bean bean5;
+
+ @BeforeEach
+ public void setup() throws Exception {
+ harness.intialize(null);
+
+ directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI());
+
+ String projectDir = directory.toURI().toString();
+ project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
+
+ CompletableFuture initProject = indexer.waitOperation();
+ initProject.get(5, TimeUnit.SECONDS);
+
+ indexedBeans = springIndex.getBeansOfProject(project.getElementName());
+
+ tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
+ bean1 = new Bean("ownerRepository", "org.springframework.samples.petclinic.owner.OwnerRepository", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
+ bean2 = new Bean("ownerService", "org.springframework.samples.petclinic.owner.OwnerService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
+ bean3 = new Bean("visitRepository", "org.springframework.samples.petclinic.owner.VisitRepository", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
+ bean4 = new Bean("visitService", "org.springframework.samples.petclinic.owner.VisitService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
+ bean5 = new Bean("petService", "org.springframework.samples.petclinic.pet.Inner.PetService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
+
+ springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3, bean4, bean5});
+ }
+
+ @AfterEach
+ public void restoreIndexState() {
+ this.springIndex.updateBeans(project.getElementName(), indexedBeans);
+ }
+
+ @Test
+ public void testBeanCompletion_withMatches() throws Exception {
+ assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 0,
+ """
+package org.sample.test;
+
+import org.springframework.samples.petclinic.owner.OwnerRepository;
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class TestBeanCompletionClass {
+
+ private final OwnerRepository ownerRepository;
+
+ TestBeanCompletionClass(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ public void test() {
+ownerRepository<*>
+ }
+}
+ """);
+ }
+
+ @Test
+ public void testBeanCompletion_withoutMatches() throws Exception {
+ assertCompletions(getCompletion("rand<*>"), new String[] {}, 0, "");
+ }
+
+ @Test
+ public void testBeanCompletion_chooseSecondCompletion() throws Exception {
+ assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 1,
+ """
+package org.sample.test;
+
+import org.springframework.samples.petclinic.owner.OwnerService;
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class TestBeanCompletionClass {
+
+ private final OwnerService ownerService;
+
+ TestBeanCompletionClass(OwnerService ownerService) {
+ this.ownerService = ownerService;
+ }
+
+ public void test() {
+ownerService<*>
+ }
+}
+ """);
+ }
+
+ @Test
+ public void testBeanCompletion_injectInnerClass() throws Exception {
+ assertCompletions(getCompletion("pet<*>"), new String[] {"petService"}, 0,
+ """
+package org.sample.test;
+
+import org.springframework.samples.petclinic.pet.Inner.PetService;
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class TestBeanCompletionClass {
+
+ private final Inner.PetService petService;
+
+ TestBeanCompletionClass(Inner.PetService petService) {
+ this.petService = petService;
+ }
+
+ public void test() {
+petService<*>
+ }
+}
+ """);
+ }
+
+ @Test
+ public void testBeanCompletion_multipleClasses() throws Exception {
+ String content = """
+ package org.sample.test;
+
+ import org.springframework.samples.petclinic.owner.OwnerRepository;
+ import org.springframework.stereotype.Controller;
+
+ @Controller
+ public class TestBeanCompletionClass {
+ private final OwnerRepository ownerRepository;
+
+ TestBeanCompletionClass(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ public void test() {
+ }
+ }
+
+ @Controller
+ public class TestBeanCompletionSecondClass {
+
+ public void test() {
+ owner<*>
+ }
+ }
+ """;
+
+ assertCompletions(content, new String[] {"ownerRepository", "ownerService"}, 1,
+ """
+package org.sample.test;
+
+import org.springframework.samples.petclinic.owner.OwnerRepository;
+import org.springframework.samples.petclinic.owner.OwnerService;
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class TestBeanCompletionClass {
+ private final OwnerRepository ownerRepository;
+
+ TestBeanCompletionClass(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ public void test() {
+ }
+}
+
+@Controller
+public class TestBeanCompletionSecondClass {
+
+ private final OwnerService ownerService;
+
+ TestBeanCompletionSecondClass(OwnerService ownerService) {
+ this.ownerService = ownerService;
+ }
+
+ public void test() {
+ ownerService<*>
+ }
+}
+ """);
+ }
+
+ @Test
+ public void testBeanCompletion_isNotSpringComponent() throws Exception {
+ String content = """
+ package org.sample.test;
+
+ public class TestBeanCompletionClass {
+
+ public void test() {
+ owner<*>
+ }
+ }
+ """;
+ // No suggestions when it is not a spring component
+ assertCompletions(content, new String[] {}, 0, "");
+ }
+
+ @Test
+ public void testBeanCompletion_isOutsideMethod() throws Exception {
+ String content = """
+ package org.sample.test;
+
+ import org.springframework.stereotype.Controller;
+
+ @Controller
+ public class TestBeanCompletionClass {
+ owner<*>
+ }
+ """;
+ assertCompletions(content, new String[] {}, 0, "");
+ }
+
+ @Test
+ public void testBeanCompletion_nestedComponent() throws Exception {
+ String content = """
+package org.sample.test;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class TestBeanCompletionClass {
+ @Component
+ public class Inner {
+
+ public void test() {
+ ownerRe<*>
+ }
+ }
+}
+ """;
+
+ assertCompletions(content, new String[] {"ownerRepository"}, 0,
+ """
+package org.sample.test;
+
+import org.springframework.samples.petclinic.owner.OwnerRepository;
+import org.springframework.stereotype.Component;
+
+@Component
+public class TestBeanCompletionClass {
+ @Component
+ public class Inner {
+
+ private final OwnerRepository ownerRepository;
+
+ Inner(OwnerRepository ownerRepository) {
+ this.ownerRepository = ownerRepository;
+ }
+
+ public void test() {
+ ownerRepository<*>
+ }
+ }
+}
+ """);
+ }
+
+ private String getCompletion(String completionLine) {
+ String content = """
+ package org.sample.test;
+
+ import org.springframework.stereotype.Controller;
+
+ @Controller
+ public class TestBeanCompletionClass {
+
+ public void test() {
+ """ +
+ completionLine + "\n" +
+ """
+ }
+ }
+ """;
+ return content;
+ }
+
+ private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedResult) throws Exception {
+ assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedResult);
+ }
+
+ private void assertCompletions(String editorContent, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedResult) throws Exception {
+ Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
+
+ List completions = editor.getCompletions();
+ assertEquals(noOfExcpectedCompletions, completions.size());
+
+ if (expectedCompletions != null) {
+ String[] completionItems = completions.stream()
+ .map(item -> item.getLabel())
+ .toArray(size -> new String[size]);
+
+ assertArrayEquals(expectedCompletions, completionItems);
+ }
+
+ if (noOfExcpectedCompletions > 0) {
+ editor.apply(completions.get(chosenCompletion));
+ assertEquals(expectedResult, editor.getText());
+ }
+ }
+
+}