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..1cc62a9002 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 @@ -26,6 +26,8 @@ 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.DependsOnCompletionProcessor; +import org.springframework.ide.vscode.boot.java.beans.BeanNamesCompletionProcessor; +import org.springframework.ide.vscode.boot.java.beans.BeanTypesCompletionProcessor; import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider; @@ -125,6 +127,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.CONDITIONAL_ON_BEAN, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("name", new BeanNamesCompletionProcessor(springIndex),"type", new BeanTypesCompletionProcessor(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)))); 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 e391a27bfe..6163436e1c 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 @@ -51,6 +51,7 @@ import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider; import org.springframework.ide.vscode.boot.java.conditionalonresource.ConditionalOnResourceDefinitionProvider; import org.springframework.ide.vscode.boot.java.copilot.util.ResponseModifier; +import org.springframework.ide.vscode.boot.java.beans.ConditionalOnBeanDefinitionProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; @@ -404,6 +405,7 @@ JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, Compila new ValueDefinitionProvider(), new ConditionalOnResourceDefinitionProvider(), new DependsOnDefinitionProvider(springIndex), + new ConditionalOnBeanDefinitionProvider(springIndex), new ResourceDefinitionProvider(springIndex), new QualifierDefinitionProvider(springIndex), new NamedDefinitionProvider(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 712558bf70..32a2172c02 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 @@ -88,9 +88,13 @@ else if (node instanceof StringLiteral && node.getParent() instanceof Annotation computeProposalsForStringLiteral(project, node, "value", completions, offset, doc); } } - // case: @Qualifier({"prefix<*>"}) + // case: @Qualifier({"prefix<*>"}) || @Qualifier(value={"prefix<*>"}) else if (node instanceof StringLiteral && node.getParent() instanceof ArrayInitializer) { if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + if (node.getParent().getParent() instanceof MemberValuePair) { + String attributeName = ((MemberValuePair)node.getParent().getParent()).getName().toString(); + computeProposalsForInsideArrayInitializer(project, node, attributeName, completions, offset, doc); + } computeProposalsForInsideArrayInitializer(project, node, "value", completions, offset, doc); } } @@ -106,6 +110,12 @@ else if (node instanceof StringLiteral && node.getParent() instanceof MemberValu else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotation) { computeProposalsForArrayInitializr(project, (ArrayInitializer) node, "value", completions, offset, doc); } + // case: @Qualifier(value={<*>}) + else if (node instanceof ArrayInitializer && node.getParent() instanceof MemberValuePair + && completionProviders.containsKey(((MemberValuePair)node.getParent()).getName().toString())) { + String attributeName = ((MemberValuePair)node.getParent()).getName().toString(); + computeProposalsForArrayInitializr(project, (ArrayInitializer) node, attributeName, completions, offset, doc); + } } catch (Exception e) { e.printStackTrace(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanNamesCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanNamesCompletionProcessor.java new file mode 100644 index 0000000000..c31630df8b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanNamesCompletionProcessor.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.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +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 Karthik Sankaranarayanan + */ +public class BeanNamesCompletionProcessor implements AnnotationAttributeCompletionProvider { + + private final SpringMetamodelIndex springIndex; + + public BeanNamesCompletionProcessor(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public Map getCompletionCandidates(IJavaProject project) { + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + return Arrays.stream(beans) + .map(Bean::getName) + .distinct() + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanTypesCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanTypesCompletionProcessor.java new file mode 100644 index 0000000000..7a64a63bca --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeanTypesCompletionProcessor.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 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; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Karthik Sankaranarayanan + */ +public class BeanTypesCompletionProcessor implements AnnotationAttributeCompletionProvider { + + private final SpringMetamodelIndex springIndex; + + public BeanTypesCompletionProcessor(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public Map getCompletionCandidates(IJavaProject project) { + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + return Arrays.stream(beans) + .map(Bean::getType) + .distinct() + .collect(Collectors.toMap(key -> key, value -> value, (u, v) -> u, LinkedHashMap::new)); + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConditionalOnBeanDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConditionalOnBeanDefinitionProvider.java new file mode 100644 index 0000000000..23f09a1b3a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConditionalOnBeanDefinitionProvider.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * 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.TextDocumentIdentifier; +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 Karthik Sankaranarayanan + */ +public class ConditionalOnBeanDefinitionProvider implements IJavaDefinitionProvider { + + private final SpringMetamodelIndex springIndex; + + public ConditionalOnBeanDefinitionProvider(SpringMetamodelIndex springIndex) { + this.springIndex = springIndex; + } + + @Override + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { + 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.CONDITIONAL_ON_BEAN.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/conditionalonbean/test/ConditionalOnBeanCompletionTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonbean/test/ConditionalOnBeanCompletionTest.java new file mode 100644 index 0000000000..aa1ebd8f13 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonbean/test/ConditionalOnBeanCompletionTest.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * 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.conditionalonbean.test; + +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 Karthik Sankaranarayanan + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class ConditionalOnBeanCompletionTest { + + @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-annotations/").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("bean1", "type1", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null); + bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null); + + springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2}); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithoutPrefixWithNameAttribute() throws Exception { + assertCompletions("@ConditionalOnBean(name=<*>)", 2, "@ConditionalOnBean(name=\"bean1\"<*>)"); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithoutPrefixWithTypeAttribute() throws Exception { + assertCompletions("@ConditionalOnBean(type=<*>)", 2, "@ConditionalOnBean(type=\"type1\"<*>)"); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithPrefixWithNameAttribute() throws Exception { + assertCompletions("@ConditionalOnBean(name=be<*>)", 2, "@ConditionalOnBean(name=\"bean1\"<*>)"); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithPrefixWithTypeAttribute() throws Exception { + assertCompletions("@ConditionalOnBean(type=ty<*>)", 2, "@ConditionalOnBean(type=\"type1\"<*>)"); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithPrefixWithoutMatches() throws Exception { + assertCompletions("@ConditionalOnBean(\"XXX<*>\")", 0, null); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithPrefixWithoutAttribute() throws Exception { + assertCompletions("@ConditionalOnBean(be<*>)", 0, null); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithAttributeNameAndDefaultSpaces() throws Exception { + assertCompletions("@ConditionalOnBean(name = <*>)", 2, "@ConditionalOnBean(name = \"bean1\"<*>)"); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithAttributeTypeAndDefaultSpaces() throws Exception { + assertCompletions("@ConditionalOnBean(type = <*>)", 2, "@ConditionalOnBean(type = \"type1\"<*>)"); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithAttributeNameAndManySpaces() throws Exception { + assertCompletions("@ConditionalOnBean(name = <*> )", 2, "@ConditionalOnBean(name = \"bean1\"<*> )"); + } + + @Test + public void testConditionalOnBeanCompletionInsideOfQuotesWithoutPrefix() throws Exception { + assertCompletions("@ConditionalOnBean(\"<*>\")", 0, null); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithoutPrefixInsideArray() throws Exception { + assertCompletions("@ConditionalOnBean(name={<*>})", 2, "@ConditionalOnBean(name={\"bean1\"<*>})"); + } + + @Test + public void testConditionalOnBeanCompletionWithoutQuotesWithTypeWithoutPrefixInsideArray() throws Exception { + assertCompletions("@ConditionalOnBean(type={<*>})", 2, "@ConditionalOnBean(type={\"type1\"<*>})"); + } + + @Test + public void testConditionalOnBeanCompletionInsideOfArrayBehindExistingElementWithName() throws Exception { + assertCompletions("@ConditionalOnBean(name={\"bean1\",<*>})", 1, "@ConditionalOnBean(name={\"bean1\",\"bean2\"<*>})"); + } + + @Test + public void testConditionalOnBeanCompletionInsideOfArrayBehindExistingElementWithType() throws Exception { + assertCompletions("@ConditionalOnBean(type={\"type1\",<*>})", 1, "@ConditionalOnBean(type={\"type1\",\"type2\"<*>})"); + } + + @Test + public void testConditionalOnBeanCompletionInsideOfArrayInFrontOfExistingElementWithName() throws Exception { + assertCompletions("@ConditionalOnBean(name={<*>\"bean1\"})", 1, "@ConditionalOnBean(name={\"bean2\",<*>\"bean1\"})"); + } + + @Test + public void testConditionalOnBeanCompletionInsideOfArrayInFrontOfExistingElementWithType() throws Exception { + assertCompletions("@ConditionalOnBean(type={<*>\"type1\"})", 1, "@ConditionalOnBean(type={\"type2\",<*>\"type1\"})"); + } + + @Test + public void testConditionalOnBeanCompletionInsideOfArrayBetweenExistingElements() throws Exception { + Bean bean3 = new Bean("bean3", "type3", 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}); + + assertCompletions("@ConditionalOnBean(name={\"bean1\",<*>\"bean2\"})", 1, "@ConditionalOnBean(name={\"bean1\",\"bean3\",<*>\"bean2\"})"); + } + + private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception { + String editorContent = """ + package org.test; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class TestConditionalOnBeanCompletion { + """ + + completionLine + "\n" + + """ + @Bean + public void method() { + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri); + + List completions = editor.getCompletions(); + assertEquals(noOfExpectedCompletions, completions.size()); + + if (noOfExpectedCompletions > 0) { + editor.apply(completions.get(0)); + assertEquals(""" + package org.test; + + import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class TestConditionalOnBeanCompletion { + """ + + expectedCompletedLine + "\n" + + """ + @Bean + public void method() { + } + """, editor.getText()); + + } + } + +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonbean/test/ConditionalOnBeanDefinitionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonbean/test/ConditionalOnBeanDefinitionProviderTest.java new file mode 100644 index 0000000000..7c1c39f597 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonbean/test/ConditionalOnBeanDefinitionProviderTest.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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.conditionalonbean.test; + +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.LocationLink; +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 Karthik Sankaranarayanan + */ +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class ConditionalOnBeanDefinitionProviderTest { + + @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-annotation-conditionalonbean/").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 testConditionalOnBeanWithNameRefersToBeanDefinitionLink() 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.boot.autoconfigure.condition.ConditionalOnBean; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class TestConditionalOnBeanCompletion { + @ConditionalOnBean(name="bean1") + @Bean + public void method() { + } + }""", 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 testConditionalOnBeanRefersToRandomBeanWithoutDefinitionLink() 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.boot.autoconfigure.condition.ConditionalOnBean; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + + @Configuration + public class TestConditionalOnBeanCompletion { + @ConditionalOnBean(name="bean5") + @Bean + public void method() { + } + }""", tempJavaDocUri); + + editor.assertNoLinkTargets("bean5"); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/mvnw b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/mvnw new file mode 100755 index 0000000000..02217b1ecb --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/mvnw @@ -0,0 +1,233 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} "$@" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/mvnw.cmd b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/mvnw.cmd new file mode 100644 index 0000000000..4b98b78c48 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/mvnw.cmd @@ -0,0 +1,145 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/pom.xml new file mode 100644 index 0000000000..ebf62d720d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + com.example + test-annotation-conditionalonbean + 0.0.1-SNAPSHOT + jar + + + org.springframework.boot + spring-boot-starter-parent + 3.0.5 + + + + + 5.0.0 + UTF-8 + UTF-8 + 17 + + + + + jakarta.persistence + jakarta.persistence-api + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/BeanClass1.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/BeanClass1.java new file mode 100644 index 0000000000..4e4f76fb55 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/BeanClass1.java @@ -0,0 +1,5 @@ +package org.test; + +public class BeanClass1 { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/BeanClass2.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/BeanClass2.java new file mode 100644 index 0000000000..45b3c3cba3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/BeanClass2.java @@ -0,0 +1,5 @@ +package org.test; + +public class BeanClass2 { + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/MainClass.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/MainClass.java new file mode 100644 index 0000000000..bab6478073 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-annotation-conditionalonbean/src/main/java/org/test/MainClass.java @@ -0,0 +1,33 @@ +package org.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.beans.factory.annotation.Qualifier; + +@SpringBootApplication +public class MainClass { + + public static void main(String[] args) throws Exception { + SpringApplication.run(MainClass.class, args); + } + + @Bean + BeanClass1 bean1() { + return new BeanClass1(); + } + + @Bean + BeanClass2 bean2() { + return new BeanClass2(); + } + + @Bean + @ConditionalOnBean(name="bean1") + BeanClass2 bean3() { + return new BeanClass2(); + } + +}