From bc5006ff14677eeda6540eaa02cafbf59de00c49 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Fri, 19 Jul 2024 14:36:22 +0200 Subject: [PATCH] GH-1298: added bean name code completion and navigation support for name attribute of resource annotation --- .../BootJavaCompletionEngineConfigurer.java | 4 + .../boot/app/BootLanguageServerBootApp.java | 2 + ...nnotationAttributeCompletionProcessor.java | 42 ++-- .../beans/ResourceCompletionProvider.java | 42 ++++ .../beans/ResourceDefinitionProvider.java | 77 +++++++ .../test/ResourceCompletionProviderTest.java | 196 ++++++++++++++++++ .../test/ResourceDefinitionProviderTest.java | 135 ++++++++++++ 7 files changed, 478 insertions(+), 20 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceCompletionProviderTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceDefinitionProviderTest.java 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 15f347bb7b..d10d13bfbc 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 @@ -28,6 +28,7 @@ import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor; import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider; +import org.springframework.ide.vscode.boot.java.beans.ResourceCompletionProvider; 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; @@ -121,6 +122,9 @@ BootJavaCompletionEngine javaCompletionEngine( 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.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)))); + return new BootJavaCompletionEngine(cuCache, providers, snippetManager); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index cd13f1390d..c265a96b43 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -47,6 +47,7 @@ import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider; +import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler; @@ -397,6 +398,7 @@ JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaPr return new JavaDefinitionHandler(cuCache, projectFinder, List.of( new ValueDefinitionProvider(), new DependsOnDefinitionProvider(springIndex), + new ResourceDefinitionProvider(springIndex), new QualifierDefinitionProvider(springIndex))); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java index 2960737d71..fa970c9899 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java @@ -61,39 +61,41 @@ public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding // case: @Qualifier(<*>) if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) { - createCompletionProposals(project, doc, node, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\""); + createCompletionProposals(project, doc, node, "value", completions, offset, offset, "", (beanName) -> "\"" + beanName + "\""); } // case: @Qualifier(prefix<*>) else if (node instanceof SimpleName && node.getParent() instanceof Annotation) { - computeProposalsForSimpleName(project, node, completions, offset, doc); + computeProposalsForSimpleName(project, node, "value", completions, offset, doc); } // case: @Qualifier(value=<*>) else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair - && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { - computeProposalsForSimpleName(project, node, completions, offset, doc); + && completionProviders.containsKey(((MemberValuePair)node.getParent()).getName().toString())) { + String attributeName = ((MemberValuePair)node.getParent()).getName().toString(); + computeProposalsForSimpleName(project, node, attributeName, completions, offset, doc); } // case: @Qualifier("prefix<*>") else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - computeProposalsForStringLiteral(project, node, completions, offset, doc); + computeProposalsForStringLiteral(project, node, "value", completions, offset, doc); } } // case: @Qualifier({"prefix<*>"}) else if (node instanceof StringLiteral && node.getParent() instanceof ArrayInitializer) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - computeProposalsForInsideArrayInitializer(project, node, completions, offset, doc); + computeProposalsForInsideArrayInitializer(project, node, "value", completions, offset, doc); } } // case: @Qualifier(value="prefix<*>") else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair - && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { + && completionProviders.containsKey(((MemberValuePair)node.getParent()).getName().toString())) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { - computeProposalsForStringLiteral(project, node, completions, offset, doc); + String attributeName = ((MemberValuePair)node.getParent()).getName().toString(); + computeProposalsForStringLiteral(project, node, attributeName, completions, offset, doc); } } // case: @Qualifier({<*>}) else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotation) { - computeProposalsForArrayInitializr(project, (ArrayInitializer) node, completions, offset, doc); + computeProposalsForArrayInitializr(project, (ArrayInitializer) node, "value", completions, offset, doc); } } catch (Exception e) { @@ -104,12 +106,12 @@ else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotat /** * create the concrete completion proposal */ - private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, Collection completions, int startOffset, int endOffset, + private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, String attributeName, Collection completions, int startOffset, int endOffset, String filterPrefix, Function createReplacementText) { Set alreadyMentionedValues = alreadyMentionedValues(node); - AnnotationAttributeCompletionProvider completionProvider = this.completionProviders.get("value"); + AnnotationAttributeCompletionProvider completionProvider = this.completionProviders.get(attributeName); if (completionProvider != null) { List candidates = completionProvider.getCompletionCandidates(project); @@ -135,7 +137,7 @@ private void createCompletionProposals(IJavaProject project, TextDocument doc, A // internal computation of the right positions, prefixes, etc. // - private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument doc) { + private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, String attributeName, Collection completions, int offset, TextDocument doc) { String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition()); int startOffset = node.getStartPosition(); @@ -144,30 +146,30 @@ private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, C String proposalPrefix = "\""; String proposalPostfix = "\""; - createCompletionProposals(project, doc, node, completions, startOffset, endOffset, prefix, (beanName) -> proposalPrefix + beanName + proposalPostfix); + createCompletionProposals(project, doc, node, attributeName, completions, startOffset, endOffset, prefix, (beanName) -> proposalPrefix + beanName + proposalPostfix); } - private void computeProposalsForStringLiteral(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument doc) throws BadLocationException { + private void computeProposalsForStringLiteral(IJavaProject project, ASTNode node, String attributeName, Collection completions, int offset, TextDocument doc) throws BadLocationException { int length = offset - (node.getStartPosition() + 1); String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, length), length); int startOffset = offset - prefix.length(); int endOffset = node.getStartPosition() + node.getLength() - 1; - createCompletionProposals(project, doc, node, completions, startOffset, endOffset, prefix, (beanName) -> beanName); + createCompletionProposals(project, doc, node, attributeName, completions, startOffset, endOffset, prefix, (beanName) -> beanName); } - private void computeProposalsForArrayInitializr(IJavaProject project, ArrayInitializer node, Collection completions, int offset, TextDocument doc) { - createCompletionProposals(project, doc, node, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\""); + private void computeProposalsForArrayInitializr(IJavaProject project, ArrayInitializer node, String attributeName, Collection completions, int offset, TextDocument doc) { + createCompletionProposals(project, doc, node, attributeName, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\""); } - private void computeProposalsForInsideArrayInitializer(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument doc) throws BadLocationException { + private void computeProposalsForInsideArrayInitializer(IJavaProject project, ASTNode node, String attributeName, Collection completions, int offset, TextDocument doc) throws BadLocationException { int length = offset - (node.getStartPosition() + 1); if (length >= 0) { - computeProposalsForStringLiteral(project, node, completions, offset, doc); + computeProposalsForStringLiteral(project, node, attributeName, completions, offset, doc); } else { - createCompletionProposals(project, doc, node, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\","); + createCompletionProposals(project, doc, node, attributeName, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\","); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java new file mode 100644 index 0000000000..d9f51a5766 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceCompletionProvider.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +/** + * @author Martin Lippert + */ +public class ResourceCompletionProvider implements AnnotationAttributeCompletionProvider { + + private final SpringMetamodelIndex springIndex; + + public ResourceCompletionProvider(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public List getCompletionCandidates(IJavaProject project) { + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + + return Arrays.stream(beans).map(bean -> bean.getName()) + .distinct() + .toList(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java new file mode 100644 index 0000000000..45af2eaf73 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +/** + * @author Martin Lippert + */ +public class ResourceDefinitionProvider implements IJavaDefinitionProvider { + + private final SpringMetamodelIndex springIndex; + + public ResourceDefinitionProvider(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n) { + if (n instanceof StringLiteral) { + StringLiteral valueNode = (StringLiteral) n; + + ASTNode parent = ASTUtils.getNearestAnnotationParent(valueNode); + + if (parent != null && parent instanceof Annotation) { + Annotation a = (Annotation) parent; + IAnnotationBinding binding = a.resolveAnnotationBinding(); + if (binding != null && binding.getAnnotationType() != null + && (Annotations.RESOURCE_JAVAX.equals(binding.getAnnotationType().getQualifiedName()) + || Annotations.RESOURCE_JAKARTA.equals(binding.getAnnotationType().getQualifiedName()))) { + String beanName = valueNode.getLiteralValue(); + + if (beanName != null && beanName.length() > 0) { + return findBeansWithName(project, beanName); + } + } + } + } + return Collections.emptyList(); + } + + private List findBeansWithName(IJavaProject project, String beanName) { + Bean[] beans = this.springIndex.getBeansWithName(project.getElementName(), beanName); + + return Arrays.stream(beans) + .map(bean -> { + return new LocationLink(bean.getLocation().getUri(), bean.getLocation().getRange(), bean.getLocation().getRange()); + }) + .collect(Collectors.toList()); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceCompletionProviderTest.java new file mode 100644 index 0000000000..693ecfd2ae --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceCompletionProviderTest.java @@ -0,0 +1,196 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - 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.Map; +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.AnnotationMetadata; +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 Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class ResourceCompletionProviderTest { + + @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; + + @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(); + AnnotationMetadata annotationBean1 = new AnnotationMetadata("org.springframework.beans.factory.annotation.Qualifier", false, Map.of("value", new String[] {"quali1"})); + AnnotationMetadata annotationBean2 = new AnnotationMetadata("org.springframework.beans.factory.annotation.Qualifier", false, Map.of("value", new String[] {"quali2"})); + + bean1 = new Bean("bean1", "type1", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean1}); + bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, new AnnotationMetadata[] {annotationBean2}); + + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2}); + } + + @AfterEach + public void restoreIndexState() { + this.springIndex.updateBeans(project.getElementName(), indexedBeans); + } + + @Test + public void testResourceNameCompletionWithoutQuotesWithAttributeName() throws Exception { + assertCompletions("@Resource(name=<*>)", new String[] {"bean1", "bean2"}, 0, "@Resource(name=\"bean1\"<*>)"); + } + + @Test + public void testResourceCompletionInsideOfQuotesWithoutPrefix() throws Exception { + assertCompletions("@Resource(name=\"<*>\")", new String[] {"bean1", "bean2"}, 0, "@Resource(name=\"bean1<*>\")"); + } + + @Test + public void testResourceCompletionInsideOfQuotesWithPrefix() throws Exception { + assertCompletions("@Resource(name=\"be<*>\")", new String[] {"bean1", "bean2"}, 0, "@Resource(name=\"bean1<*>\")"); + } + + @Test + public void testResourceCompletionInsideOfQuotesWithCompletePrefix() throws Exception { + assertCompletions("@Resource(name=\"bean1<*>\")", new String[] {"bean1"}, 0, "@Resource(name=\"bean1<*>\")"); + } + + @Test + public void testResourceCompletionInsideOfQuotesWithPrefixButWithoutMatches() throws Exception { + assertCompletions("@Resource(name=\"XXX<*>\")", 0, null); + } + + @Test + public void testResourceCompletionOutsideOfAnnotation1() throws Exception { + assertCompletions("@Resource(name=\"XXX\")<*>", 0, null); + } + + @Test + public void testResourceCompletionOutsideOfAnnotation2() throws Exception { + assertCompletions("@Resource<*>(name=\"XXX\")", 0, null); + } + + @Test + public void testQualifierCompletionInsideOfQuotesWithPrefixAndReplacedPostfix() throws Exception { + assertCompletions("@Resource(name=\"be<*>xxx\")", 2, "@Resource(name=\"bean1<*>\")"); + } + + private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + assertCompletions(completionLine, noOfExpectedCompletions, null, 0, expectedCompletedLine); + } + + private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception { + assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedCompletedLine); + } + + private void assertCompletions(String completionLine, int noOfExcpectedCompletions, String[] expectedCompletions, int chosenCompletion, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import org.springframework.stereotype.Component; + import jakarta.annotation.Resource; + + @Component + public class TestDependsOnClass { + + """ + + completionLine + "\n" + + """ + public void setTestBean(Object testBean) {} + + } + """; + + 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(""" + package org.test; + + import org.springframework.stereotype.Component; + import jakarta.annotation.Resource; + + @Component + public class TestDependsOnClass { + + """ + + expectedCompletedLine + "\n" + + """ + public void setTestBean(Object testBean) {} + + } + """, editor.getText()); + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceDefinitionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceDefinitionProviderTest.java new file mode 100644 index 0000000000..c6c1d9f57b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ResourceDefinitionProviderTest.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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 - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans.test; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; +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 Martin Lippert + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class ResourceDefinitionProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringMetamodelIndex springIndex; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + private IJavaProject project; + + @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); + } + + @Test + public void testSingleDependsOnBeanDefinitionLink() throws Exception { + String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.stereotype.Component; + import jakarta.annotation.Resource; + + @Component + @Resource(name="bean1") + public class TestDependsOnClass { + }""", tempJavaDocUri); + + String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString(); + + Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); + assertEquals(1, beans.length); + + LocationLink expectedLocation = new LocationLink(expectedDefinitionUri, + beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), + null); + + editor.assertLinkTargets("bean1", List.of(expectedLocation)); + } + + @Test + public void testDependsOnWithMultipleBeanDefinitionLinks() throws Exception { + String tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package org.test; + + import org.springframework.stereotype.Component; + import jakarta.annotation.Resource; + + @Component + @Resource(name="bean1") + public class TestDependsOnClass { + }""", tempJavaDocUri); + + String expectedDefinitionUri = directory.toPath().resolve("src/main/java/org/test/MainClass.java").toUri().toString(); + + List beansOfDoc = new ArrayList<>(List.of(springIndex.getBeansOfDocument(expectedDefinitionUri))); + beansOfDoc.add(new Bean("bean1", "type", new Location(expectedDefinitionUri, new Range(new Position(20, 1), new Position(20, 10))), null, null, null)); + springIndex.updateBeans(project.getElementName(), expectedDefinitionUri, beansOfDoc.toArray(new Bean[0])); + + Bean[] beans = springIndex.getBeansWithName(project.getElementName(), "bean1"); + assertEquals(2, beans.length); + + LocationLink expectedLocation1 = new LocationLink(expectedDefinitionUri, + beans[0].getLocation().getRange(), beans[0].getLocation().getRange(), + null); + + LocationLink expectedLocation2 = new LocationLink(expectedDefinitionUri, + beans[1].getLocation().getRange(), beans[1].getLocation().getRange(), + null); + + editor.assertLinkTargets("bean1", List.of(expectedLocation1, expectedLocation2)); + } + +}