From 3e6de7435117e12e6d94251a4b0a451f488f320e Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Mon, 30 Oct 2017 15:36:00 -0700 Subject: [PATCH] Injection report on live hover over @Bean --- .../boot/java/BootJavaLanguageServer.java | 4 +- .../autowired/AutowiredHoverProvider.java | 30 +- .../handlers/BootJavaReferencesHandler.java | 1 - .../vscode/boot/java/livehover/ASTUtils.java | 82 +++-- .../AbstractInjectedIntoHoverProvider.java} | 69 +--- .../BeanInjectedIntoHoverProvider.java | 65 ++++ .../ComponentInjectionsHoverProvider.java | 93 +++++ .../boot/java/livehover/LiveHoverUtils.java | 21 +- .../vscode/boot/java/utils/SpringIndexer.java | 5 +- .../BeanInjectedIntoHoverProviderTest.java | 334 ++++++++++++++++++ .../ComponentInjectionsHoverProviderTest.java | 8 +- .../ide/vscode/commons/util/Optionals.java | 29 ++ 12 files changed, 650 insertions(+), 91 deletions(-) rename headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/{beans/ComponentInjectionsHoverProvider.java => livehover/AbstractInjectedIntoHoverProvider.java} (61%) create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java create mode 100644 headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java rename headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/{beans => livehover}/test/ComponentInjectionsHoverProviderTest.java (98%) create mode 100644 headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Optionals.java diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java index 6d2b72a0d..6e09a7ffb 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java @@ -20,7 +20,6 @@ import org.eclipse.lsp4j.InitializeResult; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider; import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider; -import org.springframework.ide.vscode.boot.java.beans.ComponentInjectionsHoverProvider; import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; @@ -35,6 +34,8 @@ import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.livehover.ActiveProfilesProvider; +import org.springframework.ide.vscode.boot.java.livehover.BeanInjectedIntoHoverProvider; +import org.springframework.ide.vscode.boot.java.livehover.ComponentInjectionsHoverProvider; import org.springframework.ide.vscode.boot.java.requestmapping.LiveAppURLSymbolProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider; @@ -262,6 +263,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { providers.put(org.springframework.ide.vscode.boot.java.autowired.Constants.SPRING_AUTOWIRED, new AutowiredHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.beans.Constants.SPRING_COMPONENT, new ComponentInjectionsHoverProvider()); + providers.put(BeanInjectedIntoHoverProvider.ANNOTATION, new BeanInjectedIntoHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL, new ConditionalsLiveHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_BEAN, new ConditionalsLiveHoverProvider()); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index bf9309c24..014b16966 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.autowired; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -30,6 +31,8 @@ import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.Optionals; +import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; @@ -92,7 +95,7 @@ public class AutowiredHoverProvider implements HoverProvider { StringBuilder hover = new StringBuilder(); - LiveBean definedBean = ASTUtils.getDefinedBean(annotation); + LiveBean definedBean = getDefinedBean(annotation); if (definedBean != null) { hover.append("**Injection report for " + LiveHoverUtils.showBean(definedBean) + "**\n\n"); @@ -126,6 +129,31 @@ public class AutowiredHoverProvider implements HoverProvider { return null; } + private LiveBean getDefinedBean(Annotation annotation) { + TypeDeclaration declaringType = ASTUtils.findDeclaringType(annotation); + if (declaringType != null) { + ITypeBinding beanType = declaringType.resolveBinding(); + if (beanType != null) { + String id = getBeanId(annotation, beanType); + if (StringUtil.hasText(id)) { + return LiveBean.builder().id(id).type(beanType.getQualifiedName()).build(); + } + } + } + return null; + } + + private String getBeanId(Annotation annotation, ITypeBinding beanType) { + return ASTUtils.getAttribute(annotation, "value").flatMap(ASTUtils::getFirstString) + .orElseGet(() -> { + String typeName = beanType.getName(); + if (StringUtil.hasText(typeName)) { + return Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + } + return null; + }); + } + private void addAutomaticallyWired(StringBuilder hover, Annotation annotation, LiveBeansModel beans, LiveBean bean) { TypeDeclaration typeDecl = ASTUtils.findDeclaringType(annotation); if (typeDecl != null) { diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java index 1c8e4191c..a325aee1f 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaReferencesHandler.java @@ -93,7 +93,6 @@ public class BootJavaReferencesHandler implements ReferencesHandler { ASTNode node = NodeFinder.perform(cu, offset, 0); if (node != null) { - System.out.println("AST node found: " + node.getClass().getName()); return provideReferencesForAnnotation(node, offset, document); } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java index f1d3337e3..0f35d6e56 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ASTUtils.java @@ -16,9 +16,12 @@ import java.util.Optional; 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.Expression; import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.NormalAnnotation; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.TypeDeclaration; @@ -45,13 +48,51 @@ public class ASTUtils { } } - public static Optional getValueAttribute(Annotation annotation) { - if (annotation.isSingleMemberAnnotation()) { - SingleMemberAnnotation sma = (SingleMemberAnnotation) annotation; - Expression valueExp = sma.getValue(); - if (valueExp instanceof StringLiteral) { - StringLiteral stringLit = (StringLiteral) valueExp; - return Optional.ofNullable(stringLit.getLiteralValue()); + public static Optional getAttribute(Annotation annotation, String name) { + try { + if (annotation.isSingleMemberAnnotation() && name.equals("value")) { + SingleMemberAnnotation sma = (SingleMemberAnnotation) annotation; + return Optional.ofNullable(sma.getValue()); + } else if (annotation.isNormalAnnotation()) { + NormalAnnotation na = (NormalAnnotation) annotation; + Object attributeObjs = na.getStructuralProperty(NormalAnnotation.VALUES_PROPERTY); + if (attributeObjs instanceof List) { + for (Object atrObj : (List)attributeObjs) { + if (atrObj instanceof MemberValuePair) { + MemberValuePair mvPair = (MemberValuePair) atrObj; + if (name.equals(mvPair.getName().getIdentifier())) { + return Optional.ofNullable(mvPair.getValue()); + } + } + } + } + } + } catch (Exception e) { + Log.log(e); + } + return Optional.empty(); + } + + /** + * For case where a expression can be either a String or a array of Strings and + * we are interested in the first element of the array. (I.e. typical case + * when annotation attribute is of type String[] (because Java allows using a single + * value as a convenient syntax for writing an array of length 1 in that case. + */ + public static Optional getFirstString(Expression exp) { + if (exp instanceof StringLiteral) { + return Optional.ofNullable(((StringLiteral) exp).getLiteralValue()); + } else if (exp instanceof ArrayInitializer) { + ArrayInitializer array = (ArrayInitializer) exp; + Object objs = array.getStructuralProperty(ArrayInitializer.EXPRESSIONS_PROPERTY); + if (objs instanceof List) { + List list = (List) objs; + if (!list.isEmpty()) { + Object firstObj = list.get(0); + if (firstObj instanceof Expression) { + return getFirstString((Expression) firstObj); + } + } } } return Optional.empty(); @@ -79,28 +120,19 @@ public class ASTUtils { return constructors.toArray(new MethodDeclaration[constructors.size()]); } - public static LiveBean getDefinedBean(Annotation annotation) { - TypeDeclaration declaringType = ASTUtils.findDeclaringType(annotation); - if (declaringType != null) { - ITypeBinding beanType = declaringType.resolveBinding(); - if (beanType != null) { - String id = getBeanId(annotation, beanType); - if (StringUtil.hasText(id)) { - return LiveBean.builder().id(id).type(beanType.getQualifiedName()).build(); - } - } + + public static MethodDeclaration getAnnotatedMethod(Annotation annotation) { + ASTNode parent = annotation.getParent(); + if (parent instanceof MethodDeclaration) { + return (MethodDeclaration)parent; } return null; } - public static String getBeanId(Annotation annotation, ITypeBinding beanType) { - Optional explicitId = ASTUtils.getValueAttribute(annotation); - if (explicitId.isPresent()) { - return explicitId.get(); - } - String typeName = beanType.getName(); - if (StringUtil.hasText(typeName)) { - return Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + public static TypeDeclaration getAnnotatedType(Annotation annotation) { + ASTNode parent = annotation.getParent(); + if (parent instanceof TypeDeclaration) { + return (TypeDeclaration)parent; } return null; } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentInjectionsHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java similarity index 61% rename from headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentInjectionsHoverProvider.java rename to headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java index 82167630f..a62a73431 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentInjectionsHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.beans; +package org.springframework.ide.vscode.boot.java.livehover; import java.util.Collection; import java.util.List; @@ -20,16 +20,10 @@ 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.ITypeBinding; -import org.eclipse.jdt.core.dom.MarkerAnnotation; -import org.eclipse.jdt.core.dom.MethodDeclaration; -import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.springframework.ide.vscode.boot.java.autowired.Constants; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; -import org.springframework.ide.vscode.boot.java.livehover.ASTUtils; -import org.springframework.ide.vscode.boot.java.livehover.LiveHoverUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; @@ -38,14 +32,14 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; -public class ComponentInjectionsHoverProvider implements HoverProvider { +public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider { @Override public Collection getLiveHoverHints(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { // Highlight if any running app contains an instance of this component try { if (runningApps.length > 0) { - LiveBean definedBean = ASTUtils.getDefinedBean(annotation); + LiveBean definedBean = getDefinedBean(annotation); if (definedBean != null) { if (Stream.of(runningApps).anyMatch(app -> LiveHoverUtils.hasRelevantBeans(app, definedBean))) { Optional nameRange = ASTUtils.nameRange(doc, annotation); @@ -66,7 +60,7 @@ public class ComponentInjectionsHoverProvider implements HoverProvider { TextDocument doc, SpringBootApp[] runningApps) { if (runningApps.length > 0) { - LiveBean definedBean = ASTUtils.getDefinedBean(annotation); + LiveBean definedBean = getDefinedBean(annotation); if (definedBean != null) { StringBuilder hover = new StringBuilder(); hover.append("**Injection report for " + LiveHoverUtils.showBean(definedBean) + "**\n\n"); @@ -86,7 +80,7 @@ public class ComponentInjectionsHoverProvider implements HoverProvider { for (LiveBean bean : relevantBeans) { addInjectedInto(definedBean, hover, beans, bean); - addAutomaticallyWired(hover, annotation, beans, bean); + addAutomaticallyWiredContructor(hover, annotation, beans, bean); } } } @@ -100,13 +94,21 @@ public class ComponentInjectionsHoverProvider implements HoverProvider { return null; } - private void addInjectedInto(LiveBean definedBean, StringBuilder hover, LiveBeansModel beans, LiveBean bean) { + protected abstract LiveBean getDefinedBean(Annotation annotation); + + protected void addAutomaticallyWiredContructor(StringBuilder hover, Annotation annotation, LiveBeansModel beans, LiveBean bean) { + //This doesn't really belong here, but it accomodates Martin's additional logic to handle implicitly + //@Autowired constructor. + //This does nothing by default as its really only relevant to @Component annotation report. + } + + protected void addInjectedInto(LiveBean definedBean, StringBuilder hover, LiveBeansModel beans, LiveBean bean) { hover.append("\n\n"); List dependers = beans.getBeansDependingOn(bean.getId()); if (dependers.isEmpty()) { hover.append(LiveHoverUtils.showBean(bean) + " exists but is **Not injected anywhere**\n"); } else { - hover.append(LiveHoverUtils.showBean(definedBean) + " injected into:\n\n"); + hover.append(LiveHoverUtils.showBean(bean) + " injected into:\n\n"); boolean firstDependency = true; for (LiveBean dependingBean : dependers) { if (!firstDependency) { @@ -117,45 +119,4 @@ public class ComponentInjectionsHoverProvider implements HoverProvider { } } } - - private void addAutomaticallyWired(StringBuilder hover, Annotation annotation, LiveBeansModel beans, LiveBean bean) { - TypeDeclaration typeDecl = ASTUtils.findDeclaringType(annotation); - if (typeDecl != null) { - MethodDeclaration[] constructors = ASTUtils.findConstructors(typeDecl); - - if (constructors != null && constructors.length == 1 && !hasAutowiredAnnotation(constructors[0])) { - String[] dependencies = bean.getDependencies(); - - if (dependencies != null && dependencies.length > 0) { - hover.append(LiveHoverUtils.showBean(bean) + " got autowired with:\n\n"); - - boolean firstDependency = true; - for (String injectedBean : dependencies) { - if (!firstDependency) { - hover.append("\n"); - } - List dependencyBeans = beans.getBeansOfName(injectedBean); - for (LiveBean dependencyBean : dependencyBeans) { - hover.append("- " + LiveHoverUtils.showBean(dependencyBean)); - } - firstDependency = false; - } - } - } - } - } - - private boolean hasAutowiredAnnotation(MethodDeclaration constructor) { - List modifiers = constructor.modifiers(); - for (Object modifier : modifiers) { - if (modifier instanceof MarkerAnnotation) { - ITypeBinding typeBinding = ((MarkerAnnotation) modifier).resolveTypeBinding(); - if (typeBinding != null && typeBinding.getQualifiedName().equals(Constants.SPRING_AUTOWIRED)) { - return true; - } - } - } - return false; - } - } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java new file mode 100644 index 000000000..62545208b --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/BeanInjectedIntoHoverProvider.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover; + +import java.util.Optional; + +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; +import org.springframework.ide.vscode.commons.util.Optionals; + +public class BeanInjectedIntoHoverProvider extends AbstractInjectedIntoHoverProvider { + + public static final String ANNOTATION = "org.springframework.context.annotation.Bean"; + + @Override + protected LiveBean getDefinedBean(Annotation annotation) { + MethodDeclaration beanMethod = ASTUtils.getAnnotatedMethod(annotation); + if (beanMethod!=null) { + Optional beanId = getBeanId(annotation, beanMethod); + if (beanId.isPresent()) { + //TODO: we could try to be more precise here and determine the bean type from the + //ITypeBinding beanType = null; //null means unknown + // method signature, however, this will typically give us a more abstract type than + // the actual bean type at runtime. So if we we do that we have to deal with that + // somehow. Therefore, for the time being we leave the beanType as `unknown` and + // so do not use the bean type in determining relevant beans. + // Type unresolvedBeanType = beanMethod.getReturnType2(); + // if (unresolvedBeanType!=null) { + // beanType = unresolvedBeanType.resolveBinding(); + // } + return LiveBean.builder() + .id(beanId.get()) + // .type(type) + .build(); + } + } + return null; + } + + private Optional getBeanId(Annotation annotation, MethodDeclaration beanMethod) { + //Note: must handle all these cases: + // @Bean + // @Bean("beanId") + // @Bean({"beanId", "alias1"}) + // @Bean(value="beanId") + // @Bean(value={"beanId", "alias1"}) + // @Bean(name="beanId", ...) + // @Bean(name={"beanId", "alias1"}, ...) + return Optionals.tryInOrder( + () -> ASTUtils.getAttribute(annotation, "value").flatMap(ASTUtils::getFirstString), + () -> ASTUtils.getAttribute(annotation, "name").flatMap(ASTUtils::getFirstString), + () -> Optional.ofNullable(beanMethod.getName().getIdentifier()) + ); + } + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java new file mode 100644 index 000000000..3335796e9 --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover; + +import java.util.List; + +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MarkerAnnotation; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.springframework.ide.vscode.boot.java.autowired.Constants; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; +import org.springframework.ide.vscode.commons.util.StringUtil; + +public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverProvider { + + @Override protected void addAutomaticallyWiredContructor(StringBuilder hover, Annotation annotation, LiveBeansModel beans, LiveBean bean) { + TypeDeclaration typeDecl = ASTUtils.findDeclaringType(annotation); + if (typeDecl != null) { + MethodDeclaration[] constructors = ASTUtils.findConstructors(typeDecl); + + if (constructors != null && constructors.length == 1 && !hasAutowiredAnnotation(constructors[0])) { + String[] dependencies = bean.getDependencies(); + + if (dependencies != null && dependencies.length > 0) { + hover.append(LiveHoverUtils.showBean(bean) + " got autowired with:\n\n"); + + boolean firstDependency = true; + for (String injectedBean : dependencies) { + if (!firstDependency) { + hover.append("\n"); + } + List dependencyBeans = beans.getBeansOfName(injectedBean); + for (LiveBean dependencyBean : dependencyBeans) { + hover.append("- " + LiveHoverUtils.showBean(dependencyBean)); + } + firstDependency = false; + } + } + } + } + } + + private boolean hasAutowiredAnnotation(MethodDeclaration constructor) { + List modifiers = constructor.modifiers(); + for (Object modifier : modifiers) { + if (modifier instanceof MarkerAnnotation) { + ITypeBinding typeBinding = ((MarkerAnnotation) modifier).resolveTypeBinding(); + if (typeBinding != null && typeBinding.getQualifiedName().equals(Constants.SPRING_AUTOWIRED)) { + return true; + } + } + } + return false; + } + + @Override + protected LiveBean getDefinedBean(Annotation annotation) { + TypeDeclaration declaringType = ASTUtils.getAnnotatedType(annotation); + if (declaringType != null) { + ITypeBinding beanType = declaringType.resolveBinding(); + if (beanType != null) { + String id = getBeanId(annotation, beanType); + if (StringUtil.hasText(id)) { + return LiveBean.builder().id(id).type(beanType.getQualifiedName()).build(); + } + } + } + return null; + } + + private String getBeanId(Annotation annotation, ITypeBinding beanType) { + return ASTUtils.getAttribute(annotation, "value").flatMap(ASTUtils::getFirstString) + .orElseGet(() -> { + String typeName = beanType.getName(); + if (StringUtil.hasText(typeName)) { + return Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + } + return null; + }); + } + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java index b881841b5..1968b48fa 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/LiveHoverUtils.java @@ -14,11 +14,18 @@ import java.util.stream.Stream; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; public class LiveHoverUtils { public static String showBean(LiveBean bean) { - return "Bean [id: " + bean.getId() + ", type: `" + bean.getType() + "`]"; + StringBuilder buf = new StringBuilder("Bean [id: " + bean.getId()); + String type = bean.getType(); + if (type!=null) { + buf.append(", type: `"+type+"`"); + } + buf.append(']'); + return buf.toString(); } public static String niceAppName(SpringBootApp app) { @@ -30,8 +37,16 @@ public class LiveHoverUtils { } public static Stream findRelevantBeans(SpringBootApp app, LiveBean definedBean) { - return app.getBeans().getBeansOfName(definedBean.getId()).stream() - .filter(bean -> definedBean.getType().equals(bean.getType())); + LiveBeansModel beansModel = app.getBeans(); + if (beansModel!=null) { + Stream relevantBeans = beansModel.getBeansOfName(definedBean.getId()).stream(); + String type = definedBean.getType(); + if (type!=null) { + relevantBeans = relevantBeans.filter(bean -> type.equals(bean.getType())); + } + return relevantBeans; + } + return Stream.empty(); } } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java index 9189b8f99..c7b7509d8 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java @@ -105,6 +105,9 @@ public class SpringIndexer { } public CompletableFuture initialize(final Path workspaceRoot) { + if (workspaceRoot==null) { + return CompletableFuture.completedFuture(null); + } synchronized(this) { if (this.initializeTask == null) { initializing.set(true); @@ -114,9 +117,7 @@ public class SpringIndexer { this.initializeTask = CompletableFuture.runAsync(new Runnable() { @Override public void run() { - System.out.println("start initial scan..."); scanFiles(workspaceRoot.toFile()); - System.out.println("initial scan done...!!!"); SpringIndexer.this.updateQueue = new LinkedBlockingQueue<>(); SpringIndexer.this.updateWorker = new Thread(new Runnable() { diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java new file mode 100644 index 000000000..5314e5d52 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/BeanInjectedIntoHoverProviderTest.java @@ -0,0 +1,334 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.livehover.test; + +import static org.junit.Assert.assertTrue; + +import java.time.Duration; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; +import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; +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.MockRunningAppProvider; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness.CustomizableProjectContent; +import org.springframework.ide.vscode.project.harness.ProjectsHarness.ProjectCustomizer; + +public class BeanInjectedIntoHoverProviderTest { + + private static final ProjectCustomizer FOO_INTERFACE = (CustomizableProjectContent p) -> { + p.createType("hello.Foo", + "package hello;\n" + + "\n" + + "public interface Foo {\n" + + " void doSomeFoo();\n" + + "}\n" + ); + }; + + private BootLanguageServerHarness harness; + private ProjectsHarness projects = ProjectsHarness.INSTANCE; + + private MockRunningAppProvider mockAppProvider; + + @Before + public void setup() throws Exception { + mockAppProvider = new MockRunningAppProvider(); + harness = BootLanguageServerHarness.builder() + .mockDefaults() + .runningAppProvider(mockAppProvider.provider) + .watchDogInterval(Duration.ofMillis(100)) + .build(); + + MavenJavaProject jp = projects.mavenProject("empty-boot-15-web-app", FOO_INTERFACE); + assertTrue(jp.getClasspath().findType("hello.Foo").exists()); + harness.useProject(jp); + harness.intialize(null); + } + + @Test + public void beanWithNoInjections() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("myFoo") + .type("hello.FooImplementation") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "public class LocalConfig {\n" + + " \n" + + " @Bean\n" + + " Foo myFoo() {\n" + + " return new FooImplementation();\n" + + " }\n" + + "}" + ); + editor.assertHighlights("@Bean"); + editor.assertTrimmedHover("@Bean", + "**Injection report for Bean [id: myFoo]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: myFoo, type: `hello.FooImplementation`] exists but is **Not injected anywhere**\n" + ); + } + + @Test + public void explicitIdCases() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("beanId") + .type("hello.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("irrelevantBean") //wrong id + .type("hello.Foo") //right type (but should be ignored!) + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + String[] beanAnnotations = { + "@Bean(value=\"beanId\")", + "@Bean(value=\"beanId\", destroyMethod=\"cleanup\")", + "@Bean(value= {\"beanId\", \"alias\"}, destroyMethod=\"cleanup\")", + "@Bean(name=\"beanId\")", + "@Bean(name=\"beanId\", destroyMethod=\"cleanup\")", + "@Bean(name= {\"beanId\", \"alias\"}, destroyMethod=\"cleanup\")", + + "@Bean(\"beanId\")", + "@Bean({\"beanId\", \"alias\"})", + }; + for (String beanAnnotation : beanAnnotations) { + Editor editor = harness.newEditor( + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "public class LocalConfig {\n" + + " \n" + + " "+beanAnnotation+"\n" + + " Foo otherFoo() {\n" + + " return new FooImplementation();\n" + + " }\n" + + "}" + ); + editor.assertHighlights("@Bean"); + editor.assertTrimmedHover("@Bean", + "**Injection report for Bean [id: beanId]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: beanId, type: `hello.FooImplementation`] exists but is **Not injected anywhere**\n" + ); + } + } + + @Test + public void beanWithOneInjection() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("hello.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("hello.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("irrelevantBean") + .type("com.example.IrrelevantBean") + .dependencies("myController") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "public class LocalConfig {\n" + + " \n" + + " @Bean(\"fooImplementation\")\n" + + " Foo someFoo() {\n" + + " return new FooImplementation();\n" + + " }\n" + + "}" + ); + editor.assertHighlights("@Bean"); + editor.assertTrimmedHover("@Bean", + "**Injection report for Bean [id: fooImplementation]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: fooImplementation, type: `hello.FooImplementation`] injected into:\n" + + "\n" + + "- Bean [id: myController, type: `hello.MyController`]\n" + ); + } + + @Test + public void beanWithMultipleInjections() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("hello.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("hello.MyController") + .dependencies("fooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("otherBean") + .type("hello.OtherBean") + .dependencies("fooImplementation") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "public class LocalConfig {\n" + + " \n" + + " @Bean(\"fooImplementation\")\n" + + " Foo someFoo() {\n" + + " return new FooImplementation();\n" + + " }\n" + + "}" + ); + editor.assertHighlights("@Bean"); + editor.assertTrimmedHover("@Bean", + "**Injection report for Bean [id: fooImplementation]**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean [id: fooImplementation, type: `hello.FooImplementation`] injected into:\n" + + "\n" + + "- Bean [id: myController, type: `hello.MyController`]\n" + + "- Bean [id: otherBean, type: `hello.OtherBean`]\n" + ); + } + + @Test + public void noHoversWhenRunningAppDoesntHaveTheBean() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("whateverBean") + .type("com.example.UnrelatedBeanType") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("unrelated-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "public class LocalConfig {\n" + + " \n" + + " @Bean(\"fooImplementation\")\n" + + " Foo someFoo() {\n" + + " return new FooImplementation();\n" + + " }\n" + + "}" + ); + editor.assertHighlights(/*NONE*/); + editor.assertNoHover("@Bean"); + } + + @Test + public void noHoversWhenNoRunningApps() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Bean;\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "public class LocalConfig {\n" + + " \n" + + " @Bean(\"fooImplementation\")\n" + + " Foo someFoo() {\n" + + " return new FooImplementation();\n" + + " }\n" + + "}" + ); + editor.assertHighlights(/*NONE*/); + editor.assertNoHover("@Bean"); + } +} diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ComponentInjectionsHoverProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java similarity index 98% rename from headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ComponentInjectionsHoverProviderTest.java rename to headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java index 2330617ae..76047d3d4 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ComponentInjectionsHoverProviderTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ComponentInjectionsHoverProviderTest.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.beans.test; +package org.springframework.ide.vscode.boot.java.livehover.test; import static org.junit.Assert.assertTrue; @@ -29,7 +29,7 @@ import org.springframework.ide.vscode.project.harness.ProjectsHarness.ProjectCus public class ComponentInjectionsHoverProviderTest { - private static final ProjectCustomizer FOO_INTERFACE = (CustomizableProjectContent p) -> { + private static final ProjectCustomizer EXTRA_TYPES = (CustomizableProjectContent p) -> { p.createType("com.examle.Foo", "package com.example;\n" + "\n" + @@ -68,9 +68,9 @@ public class ComponentInjectionsHoverProviderTest { .watchDogInterval(Duration.ofMillis(100)) .build(); - MavenJavaProject jp = projects.mavenProject("empty-boot-15-web-app", FOO_INTERFACE); + MavenJavaProject jp = projects.mavenProject("empty-boot-15-web-app", EXTRA_TYPES); assertTrue(jp.getClasspath().findType("com.example.Foo").exists()); - harness.useProject(projects.mavenProject("empty-boot-15-web-app")); + harness.useProject(jp); harness.intialize(null); } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Optionals.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Optionals.java new file mode 100644 index 000000000..7c378e46a --- /dev/null +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Optionals.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.commons.util; + +import java.util.Optional; + +import com.google.common.base.Supplier; + +public class Optionals { + + @SafeVarargs + public static Optional tryInOrder(Supplier>... optionals) { + for (Supplier> supplier : optionals) { + Optional opt = supplier.get(); + if (opt.isPresent()) { + return opt; + } + } + return Optional.empty(); + } +}