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 162d8efb6..cfbafec33 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 @@ -101,8 +101,10 @@ public class BootJavaCompletionEngineConfigurer { @Qualifier("adHocProperties") ProjectBasedPropertyIndexProvider adHocProperties, JavaSnippetManager snippetManager, CompilationUnitCache cuCache) { + SpringPropertyIndexProvider indexProvider = params.indexProvider; JavaProjectFinder javaProjectFinder = params.projectFinder; + Map providers = new HashMap<>(); providers.put(Annotations.SCOPE, new ScopeCompletionProcessor()); 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 8bca3d69b..fda0adf5c 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 @@ -45,6 +45,7 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCache; import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDisc; import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; +import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider; 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; @@ -391,8 +392,8 @@ public class BootLanguageServerBootApp { } @Bean - JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder) { - return new JavaDefinitionHandler(cuCache, projectFinder, List.of(new PropertyValueAnnotationDefProvider())); + JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) { + return new JavaDefinitionHandler(cuCache, projectFinder, List.of(new PropertyValueAnnotationDefProvider(), new DependsOnDefinitionProvider(springIndex))); } @Bean diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index fafa97e3e..d2a9509f6 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -72,5 +72,6 @@ public class Annotations { public static final String VALUE = "org.springframework.beans.factory.annotation.Value"; public static final String SCOPE = "org.springframework.context.annotation.Scope"; + public static final String DEPENDS_ON = "org.springframework.context.annotation.DependsOn"; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnDefinitionProvider.java new file mode 100644 index 000000000..7bf47e80c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnDefinitionProvider.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * 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 DependsOnDefinitionProvider implements IJavaDefinitionProvider { + + private final SpringMetamodelIndex springIndex; + + public DependsOnDefinitionProvider(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.DEPENDS_ON.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/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index b3b9ebee4..883b14e02 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -453,7 +453,12 @@ public class ASTUtils { return result.size() > 0 ? result.toArray(new InjectionPoint[result.size()]) : DefaultValues.EMPTY_INJECTION_POINTS; } - - + + public static ASTNode getNearestAnnotationParent(ASTNode node) { + while (node != null && !(node instanceof Annotation)) { + node = node.getParent(); + } + return node; + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnDefinitionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnDefinitionProviderTest.java new file mode 100644 index 000000000..62f7459bf --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnDefinitionProviderTest.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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 DependsOnDefinitionProviderTest { + + @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.context.annotation.DependsOn; + + @Component + @DependsOn("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 testMultipleDependsOnBeanDefinitionLink() 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.context.annotation.DependsOn; + + @Component + @DependsOn({"bean1", "bean2"}) + 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.context.annotation.DependsOn; + + @Component + @DependsOn("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)); + } + +}