diff --git a/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java index 94c306f0b..ec90ff86a 100644 --- a/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java +++ b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016 Pivotal, Inc. + * Copyright (c) 2016, 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 @@ -16,6 +16,7 @@ import org.eclipse.lsp4j.TextDocumentSyncKind; import org.springframework.ide.vscode.boot.java.completions.BootJavaCompletionEngine; import org.springframework.ide.vscode.boot.java.completions.BootJavaReconcileEngine; import org.springframework.ide.vscode.boot.java.hover.BootJavaHoverProvider; +import org.springframework.ide.vscode.boot.java.references.BootJavaReferencesHandler; import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.commons.gradle.GradleCore; import org.springframework.ide.vscode.commons.gradle.GradleProjectFinderStrategy; @@ -26,6 +27,7 @@ import org.springframework.ide.vscode.commons.languageserver.java.IJavaProjectFi import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; +import org.springframework.ide.vscode.commons.languageserver.util.ReferencesHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.maven.JavaProjectWithClasspathFileFinderStrategy; @@ -65,6 +67,9 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { HoverHandler hoverInfoProvider = new BootJavaHoverProvider(this, javaProjectFinder); documents.onHover(hoverInfoProvider); + + ReferencesHandler referencesHandler = new BootJavaReferencesHandler(this, javaProjectFinder); + documents.onRefeences(referencesHandler); } public void setMaxCompletionsNumber(int number) { @@ -80,6 +85,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { completionProvider.setResolveProvider(false); c.setCompletionProvider(completionProvider); c.setHoverProvider(true); + c.setReferencesProvider(true); return c; } diff --git a/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/references/BootJavaReferencesHandler.java b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/references/BootJavaReferencesHandler.java new file mode 100644 index 000000000..5b1ecb4cf --- /dev/null +++ b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/references/BootJavaReferencesHandler.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * 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.references; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.ReferenceParams; +import org.springframework.ide.vscode.boot.java.hover.ValueHoverProvider; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.languageserver.util.ReferencesHandler; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * @author Martin Lippert + */ +public class BootJavaReferencesHandler implements ReferencesHandler { + + private static final String SPRING_VALUE = "org.springframework.beans.factory.annotation.Value"; + + private JavaProjectFinder projectFinder; + private SimpleLanguageServer server; + + public BootJavaReferencesHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder) { + this.server = server; + this.projectFinder = projectFinder; + } + + @Override + public CompletableFuture> handle(ReferenceParams params) { + SimpleTextDocumentService documents = server.getTextDocumentService(); + TextDocument doc = documents.get(params).copy(); + if (doc != null) { + try { + int offset = doc.toOffset(params.getPosition()); + CompletableFuture> referencesResult = provideReferences(doc, offset); + if (referencesResult != null) { + return referencesResult; + } + } + catch (Exception e) { + } + } + + return SimpleTextDocumentService.NO_REFERENCES; + } + + private CompletableFuture> provideReferences(TextDocument document, int offset) throws Exception { + ASTParser parser = ASTParser.newParser(AST.JLS8); + Map options = JavaCore.getOptions(); + JavaCore.setComplianceOptions(JavaCore.VERSION_1_8, options); + parser.setCompilerOptions(options); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + parser.setStatementsRecovery(true); + parser.setBindingsRecovery(true); + parser.setResolveBindings(true); + + String[] classpathEntries = getClasspathEntries(document); + String[] sourceEntries = new String[] {}; + parser.setEnvironment(classpathEntries, sourceEntries, null, true); + + String docURI = document.getUri(); + String unitName = docURI.substring(docURI.lastIndexOf("/")); + parser.setUnitName(unitName); + parser.setSource(document.get(0, document.getLength()).toCharArray()); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + ASTNode node = NodeFinder.perform(cu, offset, 0); + + if (node != null) { + System.out.println("AST node found: " + node.getClass().getName()); + return provideReferencesForAnnotation(node, offset, document); + } + + return null; + } + + private CompletableFuture> provideReferencesForAnnotation(ASTNode node, int offset, TextDocument doc) { + Annotation annotation = null; + ASTNode exactNode = node; + + while (node != null && !(node instanceof Annotation)) { + node = node.getParent(); + } + + if (node != null) { + annotation = (Annotation) node; + ITypeBinding type = annotation.resolveTypeBinding(); + if (type != null) { + String qualifiedName = type.getQualifiedName(); + if (qualifiedName != null && qualifiedName.startsWith("org.springframework")) { + return provideReferencesForSpringAnnotation(exactNode, annotation, type, offset, doc); + } + } + } + + return null; + } + + private CompletableFuture> provideReferencesForSpringAnnotation(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc) { + if (type.getQualifiedName().equals(SPRING_VALUE)) { + return new ValuePropertyReferencesProvider(server).provideReferencesForValueAnnotation(node, annotation, type, offset, doc); + } + + return null; + } + + private String[] getClasspathEntries(IDocument doc) throws Exception { + IJavaProject project = this.projectFinder.find(doc); + IClasspath classpath = project.getClasspath(); + Stream classpathEntries = classpath.getClasspathEntries(); + return classpathEntries + .filter(path -> path.toFile().exists()) + .map(path -> path.toAbsolutePath().toString()).toArray(String[]::new); + } + +} diff --git a/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/references/ValuePropertyReferencesProvider.java b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/references/ValuePropertyReferencesProvider.java new file mode 100644 index 000000000..cd1fe58bd --- /dev/null +++ b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/references/ValuePropertyReferencesProvider.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * 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.references; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +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.MemberValuePair; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.lsp4j.Location; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * @author Martin Lippert + */ +public class ValuePropertyReferencesProvider { + + private SimpleLanguageServer languageServer; + + public ValuePropertyReferencesProvider(SimpleLanguageServer languageServer) { + this.languageServer = languageServer; + } + + public CompletableFuture> provideReferencesForValueAnnotation(ASTNode node, Annotation annotation, + ITypeBinding type, int offset, TextDocument doc) { + + try { + // case: @Value("prefix<*>") + if (node instanceof StringLiteral && node.getParent() instanceof Annotation) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + return provideReferences(node.toString(), offset - node.getStartPosition(), node.getStartPosition(), doc); + } + } + // case: @Value(value="prefix<*>") + else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair + && "value".equals(((MemberValuePair)node.getParent()).getName().toString())) { + if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) { + return provideReferences(node.toString(), offset - node.getStartPosition(), node.getStartPosition(), doc); + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + private CompletableFuture> provideReferences(String value, int offset, int nodeStartOffset, TextDocument doc) { + + try { + LocalRange range = getPropertyRange(value, offset); + if (range != null) { + String propertyKey = value.substring(range.getStart(), range.getEnd()); + if (propertyKey != null && propertyKey.length() > 0) { + return findReferencesFromPropertyFiles(languageServer.getWorkspaceRoot(), propertyKey); + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public CompletableFuture> findReferencesFromPropertyFiles(Path workspaceRoot, + String propertyKey) { + + try (Stream walk = Files.walk(workspaceRoot)) { + List locations = walk + .filter(path -> isPropertiesFile(path)) + .filter(path -> path.toFile().isFile()) + .map(path -> findReferences(path)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + return CompletableFuture.completedFuture(locations); + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + private boolean isPropertiesFile(Path path) { + return path.toString().endsWith("application.properties"); + } + + private List findReferences(Path path) { + String filePath = path.toString(); + if (filePath.endsWith(".properties")) { + // do the real work + } + else if (filePath.endsWith(".yml")) { + // do the real work + } + return new ArrayList(); + } + + public LocalRange getPropertyRange(String value, int offset) { + int start = -1; + int end = -1; + + for (int i = offset - 1; i >= 0; i--) { + if (value.charAt(i) == '{') { + start = i + 1; + break; + } + else if (value.charAt(i) == '}') { + break; + } + } + + for(int i = offset; i < value.length(); i++) { + if (value.charAt(i) == '{' || value.charAt(i) == '$') { + break; + } + else if (value.charAt(i) == '}') { + end = i; + break; + } + } + + if (start > 0 && start < value.length() && end > 0 && end <= value.length() && start < end) { + return new LocalRange(start, end); + } + + return null; + } + + public static class LocalRange { + private int start; + private int end; + + public LocalRange(int start, int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + } +} diff --git a/vscode-extensions/vscode-boot-java/src/test/java/org/springframework/ide/vscode/boot/java/references/test/PropertyReferenceFinderTest.java b/vscode-extensions/vscode-boot-java/src/test/java/org/springframework/ide/vscode/boot/java/references/test/PropertyReferenceFinderTest.java new file mode 100644 index 000000000..a507b2fc8 --- /dev/null +++ b/vscode-extensions/vscode-boot-java/src/test/java/org/springframework/ide/vscode/boot/java/references/test/PropertyReferenceFinderTest.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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.references.test; + +import static org.junit.Assert.assertNotNull; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.Location; +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.references.ValuePropertyReferencesProvider; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; + +/** + * @author Martin Lippert + */ +public class PropertyReferenceFinderTest { + + @Test + public void testFindReferences() throws Exception { + ValuePropertyReferencesProvider provider = new ValuePropertyReferencesProvider(null); + + Path root = Paths.get(ProjectsHarness.class.getResource("/test-property-files/simple-case/").toURI()); + CompletableFuture> locations = provider.findReferencesFromPropertyFiles(root, "test.property"); + + assertNotNull(locations); + } + +} diff --git a/vscode-extensions/vscode-boot-java/src/test/resources/test-property-files/simple-case/application.properties b/vscode-extensions/vscode-boot-java/src/test/resources/test-property-files/simple-case/application.properties new file mode 100644 index 000000000..72b67a7be --- /dev/null +++ b/vscode-extensions/vscode-boot-java/src/test/resources/test-property-files/simple-case/application.properties @@ -0,0 +1 @@ +test.property=Hey there