From 09407cb3ad5943668cccc1c5cab59fcaed448a8b Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Fri, 10 Nov 2017 13:54:30 -0800 Subject: [PATCH] Use annotation attributes in creation of @Bean symbols --- .../dialogs/GotoSymbolDialogModel.java | 1 - .../boot/java/beans/BeansSymbolProvider.java | 57 +++++-- .../ide/vscode/boot/java/utils/ASTUtils.java | 39 ++++- .../beans/test/SpringIndexerBeansTest.java | 46 +++++- .../java/beans/test/SpringIndexerHarness.java | 144 ++++++++++++++++++ .../java/org/test/SpecialConfiguration.java | 21 ++- .../languageserver/util/DocumentRegion.java | 23 ++- .../languageserver/testharness/Editor.java | 2 +- 8 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerHarness.java diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java index 8e63a5b81..58fc81902 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.gotosymbol/src/org/springframework/tooling/ls/eclipse/gotosymbol/dialogs/GotoSymbolDialogModel.java @@ -15,7 +15,6 @@ import java.util.List; import java.util.stream.Collectors; import org.eclipse.core.runtime.Assert; -import org.eclipse.core.runtime.Platform; import org.eclipse.lsp4j.SymbolInformation; import org.springframework.tooling.ls.eclipse.gotosymbol.GotoSymbolPlugin; import org.springsource.ide.eclipse.commons.livexp.core.AsyncLiveExpression.AsyncMode; 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 1b46377f1..c6c399054 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 @@ -21,14 +21,18 @@ import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.ParameterizedType; +import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.Type; 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.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.TextDocument; +import com.google.common.collect.ImmutableList; + /** * @author Martin Lippert */ @@ -37,6 +41,7 @@ public class BeansSymbolProvider implements SymbolProvider { private static final String FUNCTION_FUNCTION_TYPE = Function.class.getName(); private static final String FUNCTION_CONSUMER_TYPE = Consumer.class.getName(); private static final String FUNCTION_SUPPLIER_TYPE = Supplier.class.getName(); + private static final String[] NAME_ATTRIBUTES = {"value", "name"}; @Override public Collection getSymbols(Annotation node, TextDocument doc) { @@ -45,13 +50,13 @@ public class BeansSymbolProvider implements SymbolProvider { return Collections.singleton(createFunctionSymbol(node, doc)); } else { - return Collections.singleton(createRegularBeanSymbol(node, doc)); + return createRegularBeanSymbols(node, doc); } } catch (Exception e) { e.printStackTrace(); } - return null; + return ImmutableList.of(); } private boolean isFunctionBean(Annotation node) { @@ -94,25 +99,51 @@ public class BeansSymbolProvider implements SymbolProvider { return symbol; } - private SymbolInformation createRegularBeanSymbol(Annotation node, TextDocument doc) throws BadLocationException { + private Collection createRegularBeanSymbols(Annotation node, TextDocument doc) throws BadLocationException { + + Collection beanNameNodes = getBeanNameLiterals(node); + if (beanNameNodes!=null && !beanNameNodes.isEmpty()) { + ImmutableList.Builder symbols = ImmutableList.builder(); + for (StringLiteral nameNode : beanNameNodes) { + String name = ASTUtils.getLiteralValue(nameNode); + String type = getBeanType(node); + symbols.add(new SymbolInformation( + beanLabel(name, type), + SymbolKind.Interface, + new Location(doc.getUri(), doc.toRange(ASTUtils.stringRegion(doc, nameNode))) + )); + } + return symbols.build(); + } else { + return ImmutableList.of(new SymbolInformation( + beanLabel(getBeanName(node), getBeanType(node)), + SymbolKind.Interface, + new Location(doc.getUri(), doc.toRange(ASTUtils.nameRegion(doc, node))) + )); + } + } + + protected String beanLabel(String beanName, String beanType) { StringBuilder symbolLabel = new StringBuilder(); symbolLabel.append("@+ "); - - String beanName = getBeanName(node); - String beanType = getBeanType(node); - symbolLabel.append('\''); symbolLabel.append(beanName); symbolLabel.append('\''); symbolLabel.append(" (@Bean) "); symbolLabel.append(beanType); - - SymbolInformation symbol = new SymbolInformation(symbolLabel.toString(), SymbolKind.Interface, - new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength()))); - return symbol; + return symbolLabel.toString(); + } + protected Collection getBeanNameLiterals(Annotation node) { + ImmutableList.Builder literals = ImmutableList.builder(); + for (String attrib : NAME_ATTRIBUTES) { + ASTUtils.getAttribute(node, attrib).ifPresent((valueExp) -> { + literals.addAll(ASTUtils.getExpressionValueAsListOfLiterals(valueExp)); + }); + } + return literals.build(); } - private String getBeanName(Annotation node) { + protected String getBeanName(Annotation node) { ASTNode parent = node.getParent(); if (parent instanceof MethodDeclaration) { MethodDeclaration method = (MethodDeclaration) parent; @@ -121,7 +152,7 @@ public class BeansSymbolProvider implements SymbolProvider { return null; } - private String getBeanType(Annotation node) { + protected String getBeanType(Annotation node) { ASTNode parent = node.getParent(); if (parent instanceof MethodDeclaration) { MethodDeclaration method = (MethodDeclaration) parent; diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index 6ea2823a6..47a0b7901 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; @@ -31,6 +32,7 @@ import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Range; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; +import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -47,7 +49,6 @@ public class ASTUtils { return new DocumentRegion(doc, start, end); } - public static Optional nameRange(TextDocument doc, Annotation annotation) { try { return Optional.of(nameRegion(doc, annotation).asRange()); @@ -57,6 +58,24 @@ public class ASTUtils { } } + public static DocumentRegion stringRegion(TextDocument doc, StringLiteral node) { + DocumentRegion nodeRegion = nodeRegion(doc, node); + if (nodeRegion.startsWith("\"")) { + nodeRegion = nodeRegion.subSequence(1); + } + if (nodeRegion.endsWith("\"")) { + nodeRegion = nodeRegion.subSequence(0, nodeRegion.getLength()-1); + } + return nodeRegion; + } + + + public static DocumentRegion nodeRegion(TextDocument doc, ASTNode node) { + int start = node.getStartPosition(); + int end = start + node.getLength(); + return new DocumentRegion(doc, start, end); + } + public static Optional getAttribute(Annotation annotation, String name) { try { if (annotation.isSingleMemberAnnotation() && name.equals("value")) { @@ -179,6 +198,22 @@ public class ASTUtils { return null; } + @SuppressWarnings("unchecked") + public static List getExpressionValueAsListOfLiterals(Expression exp) { + if (exp instanceof ArrayInitializer) { + ArrayInitializer array = (ArrayInitializer) exp; + return ((List) array.expressions()).stream() + .flatMap(e -> e instanceof StringLiteral + ? Stream.of((StringLiteral)e) + : Stream.empty() + ) + .collect(CollectorUtil.toImmutableList()); + } else if (exp instanceof StringLiteral){ + return ImmutableList.of((StringLiteral)exp); + } + return ImmutableList.of(); + } + public static Collection getAnnotations(TypeDeclaration declaringType) { Object modifiersObj = declaringType.getStructuralProperty(TypeDeclaration.MODIFIERS2_PROPERTY); @@ -203,4 +238,6 @@ public class ASTUtils { return null; } + + } 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 93e1b3cc8..e4c876e01 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 @@ -25,6 +25,7 @@ import org.junit.Test; import org.springframework.ide.vscode.boot.java.Annotations; 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; 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; @@ -53,14 +54,45 @@ public class SpringIndexerBeansTest { @Test public void testScanSimpleConfigurationClass() 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/SimpleConfiguration.java"); - assertEquals(2, symbols.size()); - assertTrue(containsSymbol(symbols, "@+ 'simpleBean' (@Bean) BeanClass", uriPrefix + "/src/main/java/org/test/SimpleConfiguration.java", 8, 1, 8, 8)); + indexer.assertDocumentSymbols(uriPrefix + "/src/main/java/org/test/SimpleConfiguration.java", + symbol("@Configuration", "@Configuration"), + symbol("@Bean", "@+ 'simpleBean' (@Bean) BeanClass") + ); + } + + @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()); + + String uriPrefix = "file://" + directory.getAbsolutePath(); + String docUri = uriPrefix + "/src/main/java/org/test/SpecialConfiguration.java"; + indexer.assertDocumentSymbols(docUri, + symbol("@Configuration", "@Configuration"), + + // @Bean("implicitNamedBean") + symbol("implicitNamedBean", "@+ 'implicitNamedBean' (@Bean) BeanClass"), + + // @Bean(value="valueBean") + symbol("valueBean", "@+ 'valueBean' (@Bean) BeanClass"), + + // @Bean(value= {"valueBean1", "valueBean2"}) + symbol("valueBean1", "@+ 'valueBean1' (@Bean) BeanClass"), + symbol("valueBean2", "@+ 'valueBean2' (@Bean) BeanClass"), + + // @Bean(name="namedBean") + symbol("namedBean", "@+ 'namedBean' (@Bean) BeanClass"), + + // @Bean(name= {"namedBean1", "namedBean2"}) + symbol("namedBean1", "@+ 'namedBean1' (@Bean) BeanClass"), + symbol("namedBean2", "@+ 'namedBean2' (@Bean) BeanClass") + ); } @Test @@ -87,6 +119,9 @@ public class SpringIndexerBeansTest { assertTrue(containsSymbol(symbols, "@+ 'simpleComponent' (@Component) SimpleComponent", uriPrefix + "/src/main/java/org/test/SimpleComponent.java", 4, 0, 4, 10)); } + //////////////////////////////// + // harness code + private boolean containsSymbol(List symbols, String name, String uri, int startLine, int startCHaracter, int endLine, int endCharacter) { for (Iterator iterator = symbols.iterator(); iterator.hasNext();) { SymbolInformation symbol = iterator.next(); @@ -104,4 +139,7 @@ public class SpringIndexerBeansTest { return false; } + private TestSymbolInfo symbol(String coveredText, String label) { + return new TestSymbolInfo(coveredText, label); + } } 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 new file mode 100644 index 000000000..7ef0c2089 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerHarness.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * 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.beans.test; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +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.handlers.SymbolProvider; +import org.springframework.ide.vscode.boot.java.utils.SpringIndexer; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.languageserver.testharness.Editor; + +import com.google.common.collect.ImmutableList; + +public class SpringIndexerHarness { + + public static class TestSymbolInfo { + + private String coveredText; + private String label; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((coveredText == null) ? 0 : coveredText.hashCode()); + result = prime * result + ((label == null) ? 0 : label.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestSymbolInfo other = (TestSymbolInfo) obj; + if (coveredText == null) { + if (other.coveredText != null) + return false; + } else if (!coveredText.equals(other.coveredText)) + return false; + if (label == null) { + if (other.label != null) + return false; + } else if (!label.equals(other.label)) + return false; + return true; + } + + + + @Override + public String toString() { + return "["+coveredText+"] => " + label; + } + + public TestSymbolInfo(String coveredText, String label) { + this.coveredText = coveredText; + this.label = label; + } + + } + + private static final Comparator RANGE_COMPARATOR = Editor.RANGE_COMPARATOR; + private static final Comparator SYMBOL_COMPARATOR = new Comparator() { + + @Override + public int compare(SymbolInformation o1, SymbolInformation o2) { + int r = o1.getLocation().getUri().compareTo(o2.getLocation().getUri()); + if (r!=0) return r; + + r = RANGE_COMPARATOR.compare(o1.getLocation().getRange(), o2.getLocation().getRange()); + if (r!=0) return r; + + return o1.getName().compareTo(o2.getName()); + } + + }; + + private SpringIndexer indexer; + + public SpringIndexerHarness(BootJavaLanguageServer server, JavaProjectFinder projectFinder, Map symbolProviders) { + this.indexer = new SpringIndexer(server, projectFinder, symbolProviders); + } + + public void assertDocumentSymbols(String documentUri, TestSymbolInfo... expectedSymbols) throws Exception { + List actualSymbols = getSymbolsInFile(documentUri); + assertEquals(symbolsString(Arrays.asList(expectedSymbols)), symbolsString(actualSymbols)); + } + + private String symbolsString(List symbols) { + StringBuilder buf = new StringBuilder(); + for (TestSymbolInfo s : symbols) { + buf.append(s+"\n"); + } + return buf.toString(); + } + + public List getSymbolsInFile(String docURI) throws Exception { + List symbols = indexer.getSymbols(docURI); + if (symbols!=null) { + symbols = new ArrayList<>(symbols); + Collections.sort(symbols, SYMBOL_COMPARATOR); + TextDocument doc = new TextDocument(docURI, LanguageId.JAVA, 1, IOUtils.toString(new URI(docURI))); + ImmutableList.Builder symbolInfos = ImmutableList.builder(); + for (SymbolInformation s : symbols) { + int start = doc.toOffset(s.getLocation().getRange().getStart()); + int end = doc.toOffset(s.getLocation().getRange().getEnd()); + symbolInfos.add(new TestSymbolInfo(doc.textBetween(start, end), s.getName())); + } + return symbolInfos.build(); + } + return ImmutableList.of(); + } + + public void initialize(Path wsRoot) { + indexer.initialize(wsRoot); + } +} diff --git a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java index b7ae509bd..c24beecae 100644 --- a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java +++ b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-beans/src/main/java/org/test/SpecialConfiguration.java @@ -6,16 +6,29 @@ import org.springframework.context.annotation.Configuration; @Configuration public class SpecialConfiguration { - @Bean("namedBean") + @Bean("implicitNamedBean") public BeanClass specialBean() { return new BeanClass(); } - - @Bean(name= {"beanName1","beanName2"}) - public BeanClass beanWithAlias() { + + @Bean(value="valueBean") + public BeanClass specialBean2() { return new BeanClass(); } + @Bean(value= {"valueBean1", "valueBean2"}) + public BeanClass specialBean3() { + return new BeanClass(); + } + @Bean(name="namedBean") + public BeanClass specialBean4() { + return new BeanClass(); + } + + @Bean(name= {"namedBean1", "namedBean2"}) + public BeanClass specialBean5() { + return new BeanClass(); + } } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java index 56fa28f60..b1899f294 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/DocumentRegion.java @@ -34,7 +34,7 @@ import org.springframework.ide.vscode.commons.util.text.Region; * * @author Kris De Volder */ -public class DocumentRegion implements CharSequence { +public class DocumentRegion implements CharSequence, IRegion { final IDocument doc; final int start; final int end; @@ -279,6 +279,7 @@ public class DocumentRegion implements CharSequence { return offset-start; } + @Override public int getLength() { return length(); } @@ -297,6 +298,20 @@ public class DocumentRegion implements CharSequence { return false; } + public boolean startsWith(CharSequence string) { + int myLen = length(); + int strLen = string.length(); + if (myLen>=strLen) { + for (int i = 0; i < strLen; i++) { + if (charAt(i)!=string.charAt(i)) { + return false; + } + } + return true; + } + return false; + } + /** * Take documentRegion at the end of */ @@ -318,4 +333,10 @@ public class DocumentRegion implements CharSequence { return subSequence(0, howMany); } + @Override + public int getOffset() { + return getStart(); + } + + } \ No newline at end of file diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 48ce44d82..0ccd59881 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -117,7 +117,7 @@ public class Editor { return p1.getCharacter() - p2.getCharacter(); } }; - private static final Comparator RANGE_COMPARATOR = new Comparator() { + public static final Comparator RANGE_COMPARATOR = new Comparator() { @Override public int compare(Range o1, Range o2) { int diff = compare(o1.getStart(), o2.getStart());