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 ea021fa6c..5436ee728 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 @@ -17,6 +17,7 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.lsp4j.CompletionItemKind; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareFactoryManager; 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; @@ -304,9 +305,8 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { } protected SpringIndexer createAnnotationIndexer(SimpleLanguageServer server, JavaProjectFinder projectFinder) { - HashMap providers = new HashMap<>(); - providers.put(Annotations.SPRING_REQUEST_MAPPING, - new RequestMappingSymbolProvider()); + AnnotationHierarchyAwareFactoryManager providers = new AnnotationHierarchyAwareFactoryManager<>(); + providers.put(Annotations.SPRING_REQUEST_MAPPING, new RequestMappingSymbolProvider()); providers.put(Annotations.SPRING_GET_MAPPING, new RequestMappingSymbolProvider()); providers.put(Annotations.SPRING_POST_MAPPING, @@ -319,7 +319,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { new RequestMappingSymbolProvider()); providers.put(Annotations.BEAN, new BeansSymbolProvider()); - providers.put(Annotations.COMPONENT, new ComponentSymbolProvider()); + providers.putFactory(Annotations.COMPONENT, ComponentSymbolProvider::new); return new SpringIndexer(this, projectFinder, providers); } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchies.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchies.java new file mode 100644 index 000000000..dce7a633f --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchies.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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.annotations; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; + +import com.google.common.collect.ImmutableList; + +/** + * Utility class for working with annotation and discovering / understanding their + * 'inheritance' structure. + *

+ * Provides methods to ask questions about inheritance between annotations. + + * @author Kris De Volder + */ +public class AnnotationHierarchies { + + protected boolean ignoreAnnotation(String fqname) { + return fqname.startsWith("java."); //mostly intended to capture java.lang.annotation.* types. But really it should be + //safe to ignore any type defined by the JRE since it can't possibly be inheriting from a spring annotation. + }; + + public Collection getDirectSuperAnnotations(ITypeBinding typeBinding) { + IAnnotationBinding[] annotations = typeBinding.getAnnotations(); + if (annotations!=null && annotations.length!=0) { + ImmutableList.Builder superAnnotations = ImmutableList.builder(); + for (IAnnotationBinding ab : annotations) { + ITypeBinding sa = ab.getAnnotationType(); + if (sa!=null) { + if (!ignoreAnnotation(sa.getQualifiedName())) { + superAnnotations.add(sa); + } + } + } + return superAnnotations.build(); + } + return ImmutableList.of(); + } + + public Set getTransitiveSuperAnnotations(ITypeBinding typeBinding) { + Set seen = new HashSet<>(); + findTransitiveSupers(typeBinding, seen); + return seen; + } + + private void findTransitiveSupers(ITypeBinding typeBinding, Set seen) { + String qname = typeBinding.getQualifiedName(); + if (seen.add(qname)) { + for (ITypeBinding superBinding : getDirectSuperAnnotations(typeBinding)) { + findTransitiveSupers(superBinding, seen); + } + } + } + + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareFactoryManager.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareFactoryManager.java new file mode 100644 index 000000000..f630ff868 --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareFactoryManager.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * 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.annotations; + +import java.util.Collection; + +import org.eclipse.jdt.core.dom.ITypeBinding; + +import com.google.common.collect.ImmutableList; + +import reactor.util.function.Tuple2; + +/** + * @author Kris De Volder + */ +public class AnnotationHierarchyAwareFactoryManager { + + @FunctionalInterface + public interface Factory { + T create(String fqAnnotationType); + } + + private AnnotationHierarchyAwareLookup> factories = new AnnotationHierarchyAwareLookup>(); + + /** + * Deprecated, for proper handling of annotation inheritance, use putFactory method instead. + */ + @Deprecated + public void put(String fqAnnotationType, T value) { + factories.put(fqAnnotationType, true, (actualAnnotationType) -> + actualAnnotationType.equals(fqAnnotationType) ? value : null + ); + } + + /** + * Add a 'base' factory which creates a `T` for a given type of annotation. + * The factory will be passed the fq name of the annotation 'base' annotation. + * It is acceptable for the Factory to return null. Null values will simply + * be ignored. + */ + public void putFactory(String fqAnnotationType, Factory factory) { + factories.put(fqAnnotationType, false, factory); + } + + public Collection get(ITypeBinding typeBinding) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Tuple2> entry : factories.get(typeBinding)) { + String superAnnotationName = entry.getT1(); + Factory factory = entry.getT2(); + T element = factory.create(superAnnotationName); + if (element!=null) { + builder.add(element); + } + } + return builder.build(); + } + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java new file mode 100644 index 000000000..7e15abceb --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java @@ -0,0 +1,156 @@ +/******************************************************************************* + * 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.annotations; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.function.Consumer; + +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.StringUtil; + +import com.google.common.collect.ImmutableSet; + +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +/** + * A Map-like utilyt that allows putting and getting values associated with + * annotation types. + *

+ * The lookup is 'hierarchy aware' which means that is able to associate values + * with a given type and all its subtypes all at once. + * + * @author Kris De Volder + */ +public class AnnotationHierarchyAwareLookup { + + private static final boolean DEBUG = false; + + private static class Binding { + T value; + boolean isOverriding; + public Binding(T value, boolean isOverriding) { + this.isOverriding = isOverriding; + this.value = value; + } + } + + private AnnotationHierarchies annotationHierarchies = new AnnotationHierarchies(); + + /** + * Associates fq anotation type name to a Binding. + */ + private Map> bindings = new HashMap<>(); + + /** + * Associates a value with a given annotation type (and all its subtypes implicitly). + * + * @param fqName Fully qualified type name for the annotation. + * @param overrideSuperTypes Determines whether the binding has 'override' behavior. Override behavior + * means that this binding stops the search for additional bindings associated + * with a super type. If override behavior is disabled the search will continue + * so that values associated with supertypes will also be found and returned + * in addition to the more specific binding. + * @param value + */ + public void put(String fqName, boolean overrideSuperTypes, T value) { + Assert.isLegal(bindings.get(fqName)==null, "Multiple bindings to the same fqName are not supported"); + bindings.put(fqName, new Binding<>(value, overrideSuperTypes)); + } + + /** + * Gets all associations applicable to a given annotationType. Note that a single 'put' binding for + * a supertype can result in mutiple applicable associations for single annotation type because + * a single put actually creates associations for a type and all its subtypes implicitly. + *

+ * So, for example: + * + * AnnotationHierarchyAwareLookup registry = new AnnotationHierarchyAwareLookup(); + * registry.put("spring.annotation.Component", "ComponentProvider"); + * + * Now, assuming that RestController is a sub annotation of Controller which is a subtype of Component, + * then if we call `get(...typeBinding of RestController...`) we will get back a collection of 3 elements: + * ("spring.annotation.Component", "ComponentProvider"), + * ("spring.annotation.Controller", "ComponentProvider") + * ("spring.annotation.RestController", "ComponentProvider") + *

+ * This reflects the fact that a binding for ComponentProvider also can function as a provider for Controller + * and RestController; and that RestController in turn can be interpreted as a specialized Component or Controller. + * Therefore we would expect in a situation where a concrete annotation of type RestController is found in the AST, + * a symbol provider for Components should be asked to produce symbols for Component, Controller and RestController, + * so should result in 3 separate calls to the symbols provider. + */ + public Collection> get(ITypeBinding annotationType) { + ImmutableSet.Builder> associations = ImmutableSet.builder(); + findElements(annotationType, new HashSet<>(), associations::add); + return associations.build(); + } + + private void findElements(ITypeBinding typeBinding, HashSet seen, Consumer> requestor) { + //Note: the 'seen' hashset is unneceassary if meta annotations do not annotate eachother + //in such a way as to create a cycle. Intuitively, you might expect that inheritance graphs + //do not contain cycles, but since these annotations can be coming from anywhere on + //a random project's classpath, we don't really know this for sure, so we must play it safe + //and guard the lookup against infinite looping. + String qname = typeBinding.getQualifiedName(); +// int debugIndent = debug_in("findElements "+StringUtil.simpleName(qname)); +// Consumer> requestor = (e) -> { +// debug(debugIndent, "<== "+StringUtil.simpleName(e.getT1())+" from "+StringUtil.simpleName(qname)); +// _requestor.accept(e); +// }; + + if (seen.add(qname)) { + Binding binding = bindings.get(qname); + boolean isOverriding = false; + if (binding!=null) { + requestor.accept(Tuples.of(qname, binding.value)); + isOverriding = binding.isOverriding; + } + if (!isOverriding) { + for (ITypeBinding superAnnotation : annotationHierarchies.getDirectSuperAnnotations(typeBinding)) { + findElements(superAnnotation, seen, superResult -> { + requestor.accept(superResult); + requestor.accept(Tuples.of(qname, superResult.getT2())); + }); + } + } + } +// debug_out("findElements "+qname); + } + +// private static int indent = 0; +// +// private static void debug(int indent, String msg) { +// for (int i = 0; i < indent; i++) { +// System.out.print(" "); +// } +// System.out.println(msg); +// } +// private static int debug_in(String msg) { +// for (int i = 0; i < indent; i++) { +// System.out.print(" "); +// } +// System.out.println(">> "+msg); +// return indent ++; +// } +// private static void debug_out(String msg) { +// indent --; +// for (int i = 0; i < indent; i++) { +// System.out.print(" "); +// } +// System.out.println("<< "+msg); +// } + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java index 2d06b509e..5128ae8df 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java @@ -89,7 +89,7 @@ public class BeansSymbolProvider implements SymbolProvider { // } @Override - public Collection getSymbols(Annotation node, TextDocument doc) { + public Collection getSymbols(Annotation node, ITypeBinding annotationType, TextDocument doc) { boolean isFunction = isFunctionBean(node); ImmutableList.Builder symbols = ImmutableList.builder(); String beanType = getBeanType(node); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index deb0a8417..fc092a683 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -11,45 +11,82 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Collection; -import java.util.Collections; 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.TypeDeclaration; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; +import com.google.common.collect.ImmutableList; + /** * @author Martin Lippert + * @author Kris De Volder */ public class ComponentSymbolProvider implements SymbolProvider { + private String annotationName; + private String simpleAnnotationName; + + public ComponentSymbolProvider(String annotationName) { + this.annotationName = annotationName; + this.simpleAnnotationName = StringUtil.simpleName(annotationName); + } + @Override - public Collection getSymbols(Annotation node, TextDocument doc) { + public Collection getSymbols(Annotation node, ITypeBinding annotationType, TextDocument doc) { try { - StringBuilder symbolLabel = new StringBuilder(); - symbolLabel.append("@+ "); - - String beanName = getBeanName(node); - String beanType = getBeanType(node); - - symbolLabel.append('\''); - symbolLabel.append(beanName); - symbolLabel.append('\''); - symbolLabel.append(" (@Component) "); - symbolLabel.append(beanType); - - SymbolInformation symbol = new SymbolInformation(symbolLabel.toString(), SymbolKind.Interface, - new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()))); - return Collections.singleton(symbol); + ImmutableList.Builder symbols = ImmutableList.builder(); + symbols.add(createSymbol(node, doc, false)); + if (isExactAnnotationType(annotationType)) { + symbols.add(createSymbol(node, doc, true)); + } + return symbols.build(); } catch (Exception e) { - e.printStackTrace(); + Log.log(e); } - return null; + return ImmutableList.of(); + } + + protected SymbolInformation createSymbol(Annotation node, TextDocument doc, boolean isExactAnnotationType) throws BadLocationException { + String annotationTypeName = isExactAnnotationType + ? simpleAnnotationName + : '+' + simpleAnnotationName; + String beanName = getBeanName(node); + String beanType = getBeanType(node); + + SymbolInformation symbol = new SymbolInformation( + beanLabel("+", annotationTypeName, beanName, beanType), SymbolKind.Interface, + new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()))); + return symbol; + } + + protected boolean isExactAnnotationType(ITypeBinding actualAnnotationType) { + return actualAnnotationType.getQualifiedName().equals(annotationName); + } + + protected String beanLabel(String searchPrefix, String annotationTypeName, String beanName, String beanType) { + StringBuilder symbolLabel = new StringBuilder(); + symbolLabel.append("@"); + symbolLabel.append(searchPrefix); + symbolLabel.append(' '); + symbolLabel.append('\''); + symbolLabel.append(beanName); + symbolLabel.append('\''); + symbolLabel.append(" (@"); + symbolLabel.append(annotationTypeName); + symbolLabel.append(") "); + symbolLabel.append(beanType); + return symbolLabel.toString(); } private String getBeanName(Annotation node) { @@ -76,4 +113,9 @@ public class ComponentSymbolProvider implements SymbolProvider { return null; } + @Override + public String toString() { + return "ComponentSymbolProvider("+simpleAnnotationName+")"; + } + } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java index 214e65303..bca9a6c70 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaCompletionEngine.java @@ -73,7 +73,6 @@ public class BootJavaCompletionEngine implements ICompletionEngine { ASTNode node = NodeFinder.perform(cu, offset, 0); if (node != null) { - System.out.println("AST node found: " + node.getClass().getName()); Collection completions = new ArrayList(); completions.addAll(collectCompletionsForAnnotations(node, offset, document)); completions.addAll(snippets.getCompletions(document, offset, node, cu)); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java index 37df3a7b1..21ea8b2c3 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/SymbolProvider.java @@ -13,14 +13,16 @@ package org.springframework.ide.vscode.boot.java.handlers; import java.util.Collection; import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.lsp4j.SymbolInformation; import org.springframework.ide.vscode.commons.util.text.TextDocument; /** * @author Martin Lippert + * @author Kris De Volder */ public interface SymbolProvider { - Collection getSymbols(Annotation node, TextDocument doc); + Collection getSymbols(Annotation node, ITypeBinding typeBinding, TextDocument doc); } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java index 164467b53..9ab78c7bd 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java @@ -41,7 +41,7 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; public class RequestMappingSymbolProvider implements SymbolProvider { @Override - public Collection getSymbols(Annotation node, TextDocument doc) { + public Collection getSymbols(Annotation node, ITypeBinding annotationType, TextDocument doc) { if (node.getParent() instanceof MethodDeclaration) { try { Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength())); 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 2c423a083..2733e8c1c 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 @@ -48,6 +48,7 @@ import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareFactoryManager; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -64,7 +65,7 @@ public class SpringIndexer { private BootJavaLanguageServer server; private JavaProjectFinder projectFinder; - private Map symbolProviders; + private AnnotationHierarchyAwareFactoryManager symbolProviders; private List symbols; private ConcurrentMap> symbolsByDoc; @@ -95,7 +96,7 @@ public class SpringIndexer { }; - public SpringIndexer(BootJavaLanguageServer server, JavaProjectFinder projectFinder, Map specificProviders) { + public SpringIndexer(BootJavaLanguageServer server, JavaProjectFinder projectFinder, AnnotationHierarchyAwareFactoryManager specificProviders) { this.server = server; this.projectFinder = projectFinder; this.symbolProviders = specificProviders; @@ -392,20 +393,19 @@ public class SpringIndexer { ITypeBinding typeBinding = node.resolveTypeBinding(); if (typeBinding != null) { - String qualifiedTypeName = typeBinding.getQualifiedName(); - - SymbolProvider provider = symbolProviders.get(qualifiedTypeName); - if (provider != null) { + Collection providers = symbolProviders.get(typeBinding); + if (!providers.isEmpty()) { TextDocument doc = getTempTextDocument(docURI, docRef, content); - Collection sbls = provider.getSymbols(node, doc); - if (sbls != null) { - sbls.forEach(symbol -> { - symbols.add(symbol); - symbolsByDoc.computeIfAbsent(docURI, s -> new ArrayList()).add(symbol); - }); + for (SymbolProvider provider : providers) { + Collection sbls = provider.getSymbols(node, typeBinding, doc); + if (sbls != null) { + sbls.forEach(symbol -> { + symbols.add(symbol); + symbolsByDoc.computeIfAbsent(docURI, s -> new ArrayList()).add(symbol); + }); + } } - } - else { + } else { SymbolInformation symbol = provideDefaultSymbol(node, docURI, docRef, content); if (symbol != null) { symbols.add(symbol); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java index 7cd05d2c2..863f29852 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringResource.java @@ -62,8 +62,6 @@ public class SpringResource { Path path = Paths.get(pathStr); IClasspath classpath = project.getClasspath(); Path outputFolder = classpath.getOutputFolder(); - System.out.println("outf = "+outputFolder); - System.out.println("path = "+pathStr); if (path.startsWith(outputFolder)) { return outputFolder.relativize(path).toString(); } diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java index e4c876e01..4a4c14944 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java @@ -23,6 +23,7 @@ import org.eclipse.lsp4j.SymbolInformation; import org.junit.Before; import org.junit.Test; import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareFactoryManager; 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.test.SpringIndexerHarness.TestSymbolInfo; @@ -37,15 +38,15 @@ import org.springframework.ide.vscode.project.harness.ProjectsHarness; */ public class SpringIndexerBeansTest { - private Map symbolProviders; + private AnnotationHierarchyAwareFactoryManager symbolProviders; private BootLanguageServerHarness harness; private JavaProjectFinder projectFinder; @Before public void setup() throws Exception { - symbolProviders = new HashMap<>(); + symbolProviders = new AnnotationHierarchyAwareFactoryManager<>(); symbolProviders.put(Annotations.BEAN, new BeansSymbolProvider()); - symbolProviders.put(Annotations.COMPONENT, new ComponentSymbolProvider()); + symbolProviders.putFactory(Annotations.COMPONENT, ComponentSymbolProvider::new); harness = BootLanguageServerHarness.builder().build(); projectFinder = harness.getProjectFinder(); @@ -60,13 +61,14 @@ public class SpringIndexerBeansTest { String uriPrefix = "file://" + directory.getAbsolutePath(); indexer.assertDocumentSymbols(uriPrefix + "/src/main/java/org/test/SimpleConfiguration.java", - symbol("@Configuration", "@Configuration"), + symbol("@Configuration", "@+ 'simpleConfiguration' (@+Component) SimpleConfiguration"), + symbol("@Configuration", "@+ 'simpleConfiguration' (@+Configuration) SimpleConfiguration"), + symbol("@Configuration", "@+ 'simpleConfiguration' (@Configuration) SimpleConfiguration"), symbol("@Bean", "@+ 'simpleBean' (@Bean) BeanClass") ); } - @Test - public void testScanSpecialConfigurationClass() throws Exception { + @Test public void testScanSpecialConfigurationClass() throws Exception { SpringIndexerHarness indexer = new SpringIndexerHarness(harness.getServer(), projectFinder, symbolProviders); File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-beans/").toURI()); indexer.initialize(directory.toPath()); @@ -74,7 +76,9 @@ public class SpringIndexerBeansTest { String uriPrefix = "file://" + directory.getAbsolutePath(); String docUri = uriPrefix + "/src/main/java/org/test/SpecialConfiguration.java"; indexer.assertDocumentSymbols(docUri, - symbol("@Configuration", "@Configuration"), + symbol("@Configuration", "@+ 'specialConfiguration' (@+Component) SpecialConfiguration"), + symbol("@Configuration", "@+ 'specialConfiguration' (@+Configuration) SpecialConfiguration"), + symbol("@Configuration", "@+ 'specialConfiguration' (@Configuration) SpecialConfiguration"), // @Bean("implicitNamedBean") symbol("implicitNamedBean", "@+ 'implicitNamedBean' (@Bean) BeanClass"), @@ -97,28 +101,66 @@ public class SpringIndexerBeansTest { @Test public void testScanSimpleFunctionBean() throws Exception { - SpringIndexer indexer = new SpringIndexer(harness.getServer(), projectFinder, symbolProviders); + SpringIndexerHarness indexer = new SpringIndexerHarness(harness.getServer(), projectFinder, symbolProviders); File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-beans/").toURI()); indexer.initialize(directory.toPath()); String uriPrefix = "file://" + directory.getAbsolutePath(); - List symbols = indexer.getSymbols(uriPrefix + "/src/main/java/org/test/FunctionClass.java"); - assertEquals(2, symbols.size()); - assertTrue(containsSymbol(symbols, "@> 'uppercase' (@Bean) Function", uriPrefix + "/src/main/java/org/test/FunctionClass.java", 10, 1, 10, 6)); + indexer.assertDocumentSymbols(uriPrefix + "/src/main/java/org/test/FunctionClass.java", + symbol("@Configuration", "@+ 'functionClass' (@+Component) FunctionClass"), + symbol("@Configuration", "@+ 'functionClass' (@+Configuration) FunctionClass"), + symbol("@Configuration", "@+ 'functionClass' (@Configuration) FunctionClass"), + + symbol("@Bean", "@> 'uppercase' (@Bean) Function") + ); } @Test public void testScanSimpleComponentClass() throws Exception { - SpringIndexer indexer = new SpringIndexer(harness.getServer(), projectFinder, symbolProviders); + SpringIndexerHarness indexer = new SpringIndexerHarness(harness.getServer(), projectFinder, symbolProviders); File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-beans/").toURI()); indexer.initialize(directory.toPath()); String uriPrefix = "file://" + directory.getAbsolutePath(); - List symbols = indexer.getSymbols(uriPrefix + "/src/main/java/org/test/SimpleComponent.java"); - assertEquals(1, symbols.size()); - assertTrue(containsSymbol(symbols, "@+ 'simpleComponent' (@Component) SimpleComponent", uriPrefix + "/src/main/java/org/test/SimpleComponent.java", 4, 0, 4, 10)); + indexer.assertDocumentSymbols(uriPrefix + "/src/main/java/org/test/SimpleComponent.java", + symbol("@Component", "@+ 'simpleComponent' (@+Component) SimpleComponent"), + symbol("@Component", "@+ 'simpleComponent' (@Component) SimpleComponent") + ); +// List symbols = indexer.getSymbols(uriPrefix + "/src/main/java/org/test/SimpleComponent.java"); +// assertEquals(1, symbols.size()); +// assertTrue(containsSymbol(symbols, "@+ 'simpleComponent' (@Component) SimpleComponent", uriPrefix + "/src/main/java/org/test/SimpleComponent.java", 4, 0, 4, 10)); } + @Test public void testScanSimpleControllerClass() throws Exception { + SpringIndexerHarness indexer = new SpringIndexerHarness(harness.getServer(), projectFinder, symbolProviders); + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-beans/").toURI()); + indexer.initialize(directory.toPath()); + + String uriPrefix = "file://" + directory.getAbsolutePath(); + String docUri = uriPrefix + "/src/main/java/org/test/SimpleController.java"; + indexer.assertDocumentSymbols(docUri, + symbol("@Controller", "@+ 'simpleController' (@+Component) SimpleController"), + symbol("@Controller", "@+ 'simpleController' (@+Controller) SimpleController"), + symbol("@Controller", "@+ 'simpleController' (@Controller) SimpleController") + ); + } + + @Test public void testScanRestControllerClass() throws Exception { + SpringIndexerHarness indexer = new SpringIndexerHarness(harness.getServer(), projectFinder, symbolProviders); + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-beans/").toURI()); + indexer.initialize(directory.toPath()); + + String uriPrefix = "file://" + directory.getAbsolutePath(); + String docUri = uriPrefix + "/src/main/java/org/test/SimpleRestController.java"; + indexer.assertDocumentSymbols(docUri, + symbol("@RestController", "@+ 'simpleRestController' (@+Component) SimpleRestController"), + symbol("@RestController", "@+ 'simpleRestController' (@+Controller) SimpleRestController"), + symbol("@RestController", "@+ 'simpleRestController' (@+RestController) SimpleRestController"), + symbol("@RestController", "@+ 'simpleRestController' (@RestController) SimpleRestController") + ); + } + + //////////////////////////////// // harness code diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerHarness.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerHarness.java index 7ef0c2089..8064b7252 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerHarness.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerHarness.java @@ -25,6 +25,7 @@ import org.apache.commons.io.IOUtils; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareFactoryManager; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.utils.SpringIndexer; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -104,7 +105,7 @@ public class SpringIndexerHarness { private SpringIndexer indexer; - public SpringIndexerHarness(BootJavaLanguageServer server, JavaProjectFinder projectFinder, Map symbolProviders) { + public SpringIndexerHarness(BootJavaLanguageServer server, JavaProjectFinder projectFinder, AnnotationHierarchyAwareFactoryManager symbolProviders) { this.indexer = new SpringIndexer(server, projectFinder, symbolProviders); } diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java index 0b74102ad..98c4656b2 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java @@ -65,11 +65,11 @@ public class SpringIndexerTest { List allSymbols = indexer().getAllSymbols(""); - assertEquals(6, allSymbols.size()); + assertEquals(10, allSymbols.size()); String uriPrefix = "file://" + directory.getAbsolutePath(); - assertTrue(containsSymbol(allSymbols, "@SpringBootApplication", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(allSymbols, "@+ 'mainClass' (@SpringBootApplication) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); assertTrue(containsSymbol(allSymbols, "@/embedded-foo-mapping -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 17, 1, 17, 41)); assertTrue(containsSymbol(allSymbols, "@/foo-root-mapping/embedded-foo-mapping-with-root -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 27, 1, 27, 51)); assertTrue(containsSymbol(allSymbols, "@/mapping1 -- (no method defined)", uriPrefix + "/src/main/java/org/test/SimpleMappingClass.java", 6, 1, 6, 28)); @@ -85,8 +85,12 @@ public class SpringIndexerTest { String uriPrefix = "file://" + directory.getAbsolutePath(); List symbols = indexer().getSymbols(uriPrefix + "/src/main/java/org/test/MainClass.java"); - assertEquals(3, symbols.size()); - assertTrue(containsSymbol(symbols, "@SpringBootApplication", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertEquals(7, symbols.size()); + assertTrue(containsSymbol(symbols, "@+ 'mainClass' (@SpringBootApplication) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(symbols, "@+ 'mainClass' (@+SpringBootApplication) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(symbols, "@+ 'mainClass' (@+SpringBootConfiguration) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(symbols, "@+ 'mainClass' (@+Component) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(symbols, "@+ 'mainClass' (@+Configuration) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); assertTrue(containsSymbol(symbols, "@/embedded-foo-mapping -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 17, 1, 17, 41)); assertTrue(containsSymbol(symbols, "@/foo-root-mapping/embedded-foo-mapping-with-root -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 27, 1, 27, 51)); @@ -108,11 +112,11 @@ public class SpringIndexerTest { List allSymbols = indexer().getAllSymbols(""); - assertEquals(6, allSymbols.size()); + assertEquals(10, allSymbols.size()); String uriPrefix = "file://" + directory.getAbsolutePath() + "/test-annotation-indexing"; - assertTrue(containsSymbol(allSymbols, "@SpringBootApplication", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(allSymbols, "@+ 'mainClass' (@SpringBootApplication) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); assertTrue(containsSymbol(allSymbols, "@/embedded-foo-mapping -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 17, 1, 17, 41)); assertTrue(containsSymbol(allSymbols, "@/foo-root-mapping/embedded-foo-mapping-with-root -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 27, 1, 27, 51)); assertTrue(containsSymbol(allSymbols, "@/mapping1 -- (no method defined)", uriPrefix + "/src/main/java/org/test/SimpleMappingClass.java", 6, 1, 6, 28)); @@ -141,11 +145,14 @@ public class SpringIndexerTest { // check for updated index in all symbols List allSymbols = indexer().getAllSymbols(""); - assertEquals(6, allSymbols.size()); + assertEquals(10, allSymbols.size()); String uriPrefix = "file://" + directory.getAbsolutePath(); - assertTrue(containsSymbol(allSymbols, "@SpringBootApplication", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(allSymbols, "@+ 'mainClass' (@+Component) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(allSymbols, "@+ 'mainClass' (@+SpringBootApplication) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(allSymbols, "@+ 'mainClass' (@SpringBootApplication) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); + assertTrue(containsSymbol(allSymbols, "@+ 'mainClass' (@+Configuration) MainClass", uriPrefix + "/src/main/java/org/test/MainClass.java", 6, 0, 6, 22)); assertTrue(containsSymbol(allSymbols, "@/embedded-foo-mapping -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 17, 1, 17, 41)); assertTrue(containsSymbol(allSymbols, "@/foo-root-mapping/embedded-foo-mapping-with-root -- (no method defined)", uriPrefix + "/src/main/java/org/test/MainClass.java", 27, 1, 27, 51)); assertTrue(containsSymbol(allSymbols, "@/mapping1-CHANGED -- (no method defined)", uriPrefix + "/src/main/java/org/test/SimpleMappingClass.java", 6, 1, 6, 36)); @@ -161,7 +168,7 @@ public class SpringIndexerTest { List allSymbols = indexer().getAllSymbols("mapp"); - assertEquals(5, allSymbols.size()); + assertEquals(7, allSymbols.size()); String uriPrefix = "file://" + directory.getAbsolutePath(); @@ -226,7 +233,7 @@ public class SpringIndexerTest { File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-parent/test-annotation-indexing/").toURI()); List allSymbols = indexer().getAllSymbols(""); - assertEquals(6, allSymbols.size()); + assertEquals(10, allSymbols.size()); File pomFile = directory.toPath().resolve(MavenCore.POM_XML).toFile(); @@ -237,7 +244,7 @@ public class SpringIndexerTest { allSymbols = indexer().getAllSymbols(""); assertFalse(indexer().isInitializing()); - assertEquals(6, allSymbols.size()); + assertEquals(10, allSymbols.size()); } } diff --git a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SimpleController.java b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SimpleController.java new file mode 100644 index 000000000..00dfd6b48 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SimpleController.java @@ -0,0 +1,7 @@ +package org.test; + +import org.springframework.stereotype.Controller; + +@Controller +public class SimpleController { +} diff --git a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SimpleRestController.java b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SimpleRestController.java new file mode 100644 index 000000000..6ad403c3e --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SimpleRestController.java @@ -0,0 +1,7 @@ +package org.test; + +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SimpleRestController { +} diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertiesLoader.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertiesLoader.java index 04a03ee07..5b82af3f9 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertiesLoader.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/PropertiesLoader.java @@ -25,6 +25,7 @@ import java.util.zip.ZipEntry; import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.util.Log; public class PropertiesLoader { @@ -55,6 +56,7 @@ public class PropertiesLoader { public ConfigurationMetadataRepository load(IClasspath classPath) { try { classPath.getClasspathEntries().forEach(entry -> { + Log.info("Indexing "+entry); File fileEntry = entry.toFile(); if (fileEntry.exists()) { if (fileEntry.isDirectory()) { diff --git a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java index 79830ab41..7da6c2086 100644 --- a/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java +++ b/headless-services/boot-properties-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/SpringPropertiesIndexManager.java @@ -18,6 +18,7 @@ import org.springframework.ide.vscode.boot.metadata.util.ListenerManager; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.ProgressService; import org.springframework.ide.vscode.commons.util.FuzzyMap; +import org.springframework.ide.vscode.commons.util.Log; /** * Support for Reconciling, Content Assist and Hover Text in spring properties @@ -43,6 +44,7 @@ public class SpringPropertiesIndexManager extends ListenerManager doit) { + return new Listener() { + @Override + public void created(IJavaProject project) { + doit.accept(project); + } + + + @Override + public void changed(IJavaProject project) { + doit.accept(project); + } + + @Override + public void deleted(IJavaProject project) { + doit.accept(project); + } + + }; + } + } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java index 343a2c9ca..2893bdea1 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/StringUtil.java @@ -217,4 +217,15 @@ public class StringUtil { } return Optional.empty(); } + + /** + * Obtains the 'simple' unqualified name from a fully qualified name. + */ + public static String simpleName(String fullyQualifiedName) { + int dot = fullyQualifiedName.lastIndexOf('.'); + if (dot>=0) { + return fullyQualifiedName.substring(dot+1); + } + return fullyQualifiedName; + } }