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 9cbe5b078..52f1833e4 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 @@ -26,6 +26,8 @@ public class Annotations { public static final String AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired"; public static final String INJECT = "javax.inject.Inject"; + public static final String QUALIFIER = "org.springframework.beans.factory.annotation.Qualifier"; + public static final String SPRING_REQUEST_MAPPING = "org.springframework.web.bind.annotation.RequestMapping"; public static final String SPRING_GET_MAPPING = "org.springframework.web.bind.annotation.GetMapping"; public static final String SPRING_POST_MAPPING = "org.springframework.web.bind.annotation.PostMapping"; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index a0fcc5303..20e573fa2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.java.autowired; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.eclipse.jdt.core.dom.ASTNode; @@ -198,20 +199,34 @@ public class AutowiredHoverProvider implements HoverProvider { return ((List)methodDeclaration.parameters()).stream() .filter(p -> p instanceof SingleVariableDeclaration) .map(p -> (SingleVariableDeclaration)p) - .map(singleVariableDeclaration -> matchBeanByType(project, beans, singleVariableDeclaration.getType().resolveBinding())) + .map(singleVariableDeclaration -> { + // Supposed to be a list of one bean for the variable declaration + List matches = findAutowiredBeans(project, singleVariableDeclaration, beans); + return matches.isEmpty() ? null : matches.get(0); + }) .filter(matchedBean -> matchedBean != null) .collect(Collectors.toList()); } else if (declarationNode instanceof FieldDeclaration) { FieldDeclaration fieldDeclaration = (FieldDeclaration)declarationNode; - LiveBean matchedBean = matchBeanByType(project, beans, fieldDeclaration.getType().resolveBinding()); - if (matchedBean != null) { - return ImmutableList.of(matchedBean); + Optional beanId = ASTUtils.beanId(fieldDeclaration.modifiers()); + if (beanId.isPresent()) { + return beans.stream().filter(b -> beanId.get().equals(b.getId())).findFirst().map(b -> ImmutableList.of(b)).orElseGet(() -> ImmutableList.of()); + } else { + LiveBean matchedBean = matchBeanByType(project, beans, fieldDeclaration.getType().resolveBinding()); + if (matchedBean != null) { + return ImmutableList.of(matchedBean); + } } } else if (declarationNode instanceof SingleVariableDeclaration) { SingleVariableDeclaration singleVariableDeclaration = (SingleVariableDeclaration)declarationNode; - LiveBean matchedBean = matchBeanByType(project, beans, singleVariableDeclaration.getType().resolveBinding()); - if (matchedBean != null) { - return ImmutableList.of(matchedBean); + Optional beanId = ASTUtils.beanId(singleVariableDeclaration.modifiers()); + if (beanId.isPresent()) { + return beans.stream().filter(b -> beanId.get().equals(b.getId())).findFirst().map(b -> ImmutableList.of(b)).orElseGet(() -> ImmutableList.of()); + } else { + LiveBean matchedBean = matchBeanByType(project, beans, singleVariableDeclaration.getType().resolveBinding()); + if (matchedBean != null) { + return ImmutableList.of(matchedBean); + } } } return Collections.emptyList(); 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 567c8b87a..5ec58d6c5 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 @@ -30,8 +30,10 @@ import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Range; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.commons.util.CollectorUtil; -import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.DocumentRegion; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -39,6 +41,8 @@ import com.google.common.collect.ImmutableList; public class ASTUtils { + private static final Logger log = LoggerFactory.getLogger(ASTUtils.class); + public static DocumentRegion nameRegion(TextDocument doc, Annotation annotation) { int start = annotation.getTypeName().getStartPosition(); int end = start + annotation.getTypeName().getLength(); @@ -52,7 +56,7 @@ public class ASTUtils { try { return Optional.of(nameRegion(doc, annotation).asRange()); } catch (Exception e) { - Log.log(e); + log.error("", e); return Optional.empty(); } } @@ -96,7 +100,7 @@ public class ASTUtils { } } } catch (Exception e) { - Log.log(e); + log.error("", e); } } return Optional.empty(); @@ -239,6 +243,23 @@ public class ASTUtils { return null; } - + public static Optional beanId(List modifiers) { + return modifiers.stream() + .filter(m -> m instanceof SingleMemberAnnotation) + .map(m -> (SingleMemberAnnotation) m) + .filter(m -> { + ITypeBinding typeBinding = m.resolveTypeBinding(); + if (typeBinding != null) { + return Annotations.QUALIFIER.equals(typeBinding.getQualifiedName()); + } + return false; + }) + .findFirst() + .map(a -> a.getValue()) + .filter(e -> e != null) + .map(e -> e.resolveConstantExpressionValue()) + .filter(o -> o instanceof String) + .map(o -> (String) o); + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java index 76a882859..6f2681d33 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java @@ -667,6 +667,163 @@ public class AutowiredHoverProviderTest { ); } + @Test + public void qualifierParameterWiredBean() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("someComponent") + .type("com.example.SomeComponent") + .dependencies("dependencyA", "dependencyB") + .build() + ) + .add(LiveBean.builder() + .id("dependencyA") + .type("com.example.DependencyA") + .build() + ) + .add(LiveBean.builder() + .id("dependencyB") + .type("com.example.DependencyB") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.beans.factory.annotation.Qualifier;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class SomeComponent {\n" + + "\n" + + " private IDependency a;\n" + + " private IDependency b;\n" + + "\n" + + " public SomeComponent(@Qualifier(\"dependencyA\") IDependency a, @Qualifier(\"dependencyB\") IDependency b) {\n" + + " this.a = a;\n" + + " this.b = b;\n" + + " }\n" + + "\n" + + "}\n" + ); + + editor.assertHighlights("@Component", "SomeComponent"); + editor.assertTrimmedHover("SomeComponent", 2, + "**Autowired `someComponent` ← `dependencyA` `dependencyB`**\n" + + "- Bean: `dependencyA` \n" + + " Type: `com.example.DependencyA`\n" + + "- Bean: `dependencyB` \n" + + " Type: `com.example.DependencyB`\n" + + " \n" + + "Process [PID=111, name=`the-app`]\n" + ); + } + + @Test + public void qualifierFieldWiredBean() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("someComponent") + .type("com.example.SomeComponent") + .dependencies("dependencyA", "dependencyB") + .build() + ) + .add(LiveBean.builder() + .id("dependencyA") + .type("com.example.DependencyA") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.beans.factory.annotation.Autowired;\n" + + "import org.springframework.beans.factory.annotation.Qualifier;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class SomeComponent {\n" + + "\n" + + " @Autowired\n" + + " @Qualifier(\"dependencyA\")\n" + + " private IDependency a;\n" + + "\n" + + " public SomeComponent() {\n" + + " }\n" + + "\n" + + "}\n" + ); + + editor.assertHighlights("@Component", "@Autowired"); + editor.assertTrimmedHover("@Autowired", 1, + "**Autowired `someComponent` ← `dependencyA`**\n" + + "- Bean: `dependencyA` \n" + + " Type: `com.example.DependencyA`\n" + + " \n" + + "Process [PID=111, name=`the-app`]\n" + ); + } + + @Test + public void qualifierNotFoundWiredBean() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("someComponent") + .type("com.example.SomeComponent") + .dependencies("dependencyA", "dependencyB") + .build() + ) + .add(LiveBean.builder() + .id("dependencyA") + .type("com.example.DependencyA") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.beans.factory.annotation.Autowired;\n" + + "import org.springframework.beans.factory.annotation.Qualifier;\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class SomeComponent {\n" + + "\n" + + " @Autowired\n" + + " @Qualifier(\"X\")\n" + + " private IDependency a;\n" + + "\n" + + " public SomeComponent() {\n" + + " }\n" + + "\n" + + "}\n" + ); + + editor.assertHighlights("@Component"); + editor.assertNoHover("@Autowired"); + } + @Test public void anonymousInnerClassBeanWiring() throws Exception { LiveBeansModel beans = LiveBeansModel.builder()