diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index 7fb57f0de..5202dc6ca 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -27,6 +27,7 @@ import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; +import org.springframework.ide.vscode.boot.java.beans.ProfileReferencesProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierReferencesProvider; import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; @@ -296,6 +297,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent Map providers = new HashMap<>(); providers.put(Annotations.VALUE, new ValuePropertyReferencesProvider(server)); providers.put(Annotations.QUALIFIER, new QualifierReferencesProvider(index, symbolIndex)); + providers.put(Annotations.PROFILE, new ProfileReferencesProvider(index, symbolIndex)); return new BootJavaReferencesHandler(this, cuCache, projectFinder, providers); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ProfileReferencesProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ProfileReferencesProvider.java new file mode 100644 index 000000000..2ddedf2b2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ProfileReferencesProvider.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * 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 java.util.stream.Stream; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ArrayInitializer; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; + +/** + * @author Martin Lippert + */ +public class ProfileReferencesProvider implements ReferenceProvider { + + private final SpringMetamodelIndex springIndex; + private final SpringSymbolIndex symbolIndex; + + public ProfileReferencesProvider(SpringMetamodelIndex springIndex, SpringSymbolIndex symbolIndex) { + this.springIndex = springIndex; + this.symbolIndex = symbolIndex; + } + + @Override + public List provideReferences(CancelChecker cancelToken, IJavaProject project, ASTNode node, Annotation annotation, ITypeBinding type, int offset) { + + cancelToken.checkCanceled(); + + try { + // case: @Value("prefix<*>") + if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + return provideReferences(project, ((StringLiteral) node).getLiteralValue()); + } + } + // case: @Value(value="prefix<*>") + else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair + && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + return provideReferences(project, ((StringLiteral) node).getLiteralValue()); + } + } + // case: @Qualifier({"prefix<*>"}) + else if (node instanceof StringLiteral && node.getParent() instanceof ArrayInitializer) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + return provideReferences(project, ((StringLiteral) node).getLiteralValue()); + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + private List provideReferences(IJavaProject project, String value) { + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + + // beans with name + Stream beanLocations = Arrays.stream(beans) + .filter(bean -> bean.getName().equals(value)) + .map(bean -> bean.getLocation()); + + String profileSymbolsSearch = "@Profile("; + List profileSymbols = symbolIndex.getAllSymbols(profileSymbolsSearch); + + String valuePhrase = "\"" + value + "\""; + + Stream qualifierLocations = profileSymbols.stream() + .filter(symbol -> symbol.getName().contains(valuePhrase)) + .map(symbol -> symbol.getLocation()) + .filter(location -> location.isLeft()) + .map(location -> location.getLeft()); + + return Stream.concat(qualifierLocations, beanLocations).toList(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ProfileReferencesProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ProfileReferencesProviderTest.java new file mode 100644 index 000000000..e3611c3d1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ProfileReferencesProviderTest.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * 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 static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +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.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.commons.languageserver.java.JavaProjectFinder; +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 ProfileReferencesProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + + private File directory; + + @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(); + projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + public void testProfileRefersToOtherProfiles() 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 org.springframework.context.annotation.Profile; + + @Component + @Profile("pro<*>file1") + public class TestDependsOnClass { + }""", tempJavaDocUri); + + List references = editor.getReferences(); + assertEquals(2, references.size()); + + String expectedDefinitionUri1 = directory.toPath().resolve("src/main/java/org/test/profiles/ProfilesClass1.java").toUri().toString(); + Location expectedLocation1 = new Location(expectedDefinitionUri1, + new Range(new Position(6, 0), new Position(6, 20))); + + assertTrue(references.contains(expectedLocation1)); + + String expectedDefinitionUri2 = directory.toPath().resolve("src/main/java/org/test/profiles/ProfilesClassWithArray.java").toUri().toString(); + Location expectedLocation2 = new Location(expectedDefinitionUri2, + new Range(new Position(6, 0), new Position(6, 42))); + + assertTrue(references.contains(expectedLocation2)); + } + + @Test + public void testProfileWithinArrayRefersToOtherProfiles() 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 org.springframework.context.annotation.Profile; + + @Component + @Profile(value = {"profile2", "pro<*>file1"}) + public class TestDependsOnClass { + }""", tempJavaDocUri); + + List references = editor.getReferences(); + assertEquals(2, references.size()); + + String expectedDefinitionUri1 = directory.toPath().resolve("src/main/java/org/test/profiles/ProfilesClass1.java").toUri().toString(); + Location expectedLocation1 = new Location(expectedDefinitionUri1, + new Range(new Position(6, 0), new Position(6, 20))); + + assertTrue(references.contains(expectedLocation1)); + + String expectedDefinitionUri2 = directory.toPath().resolve("src/main/java/org/test/profiles/ProfilesClassWithArray.java").toUri().toString(); + Location expectedLocation2 = new Location(expectedDefinitionUri2, + new Range(new Position(6, 0), new Position(6, 42))); + + assertTrue(references.contains(expectedLocation2)); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClass1.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClass1.java new file mode 100644 index 000000000..cdd07061a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClass1.java @@ -0,0 +1,9 @@ +package org.test.profiles; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("profile1") +public class ProfilesClass1 { +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClass2.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClass2.java new file mode 100644 index 000000000..e9f719f78 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClass2.java @@ -0,0 +1,9 @@ +package org.test.profiles; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile("profile2") +public class ProfilesClass2 { +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClassWithArray.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClassWithArray.java new file mode 100644 index 000000000..0df273f99 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/src/main/java/org/test/profiles/ProfilesClassWithArray.java @@ -0,0 +1,10 @@ +package org.test.profiles; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile(value = {"profile1", "profile2"}) +public class ProfilesClassWithArray { + +}