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 ba51658a0..94c306f0b 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 @@ -15,6 +15,7 @@ import org.eclipse.lsp4j.ServerCapabilities; 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.metadata.SpringPropertyIndexProvider; import org.springframework.ide.vscode.commons.gradle.GradleCore; import org.springframework.ide.vscode.commons.gradle.GradleProjectFinderStrategy; @@ -24,6 +25,7 @@ import org.springframework.ide.vscode.commons.languageserver.java.DefaultJavaPro import org.springframework.ide.vscode.commons.languageserver.java.IJavaProjectFinderStrategy; 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.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; import org.springframework.ide.vscode.commons.maven.JavaProjectWithClasspathFileFinderStrategy; @@ -44,11 +46,9 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { new JavaProjectWithClasspathFileFinderStrategy() }); - private final JavaProjectFinder javaProjectFinder; private final VscodeCompletionEngineAdapter completionEngine; public BootJavaLanguageServer(JavaProjectFinder javaProjectFinder, SpringPropertyIndexProvider indexProvider) { - this.javaProjectFinder = javaProjectFinder; SimpleTextDocumentService documents = getTextDocumentService(); IReconcileEngine reconcileEngine = new BootJavaReconcileEngine(); @@ -62,6 +62,9 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { completionEngine.setMaxCompletionsNumber(100); documents.onCompletion(completionEngine::getCompletions); documents.onCompletionResolve(completionEngine::resolveCompletion); + + HoverHandler hoverInfoProvider = new BootJavaHoverProvider(this, javaProjectFinder); + documents.onHover(hoverInfoProvider); } public void setMaxCompletionsNumber(int number) { @@ -76,6 +79,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { CompletionOptions completionProvider = new CompletionOptions(); completionProvider.setResolveProvider(false); c.setCompletionProvider(completionProvider); + c.setHoverProvider(true); return c; } diff --git a/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/hover/BootJavaHoverProvider.java b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/hover/BootJavaHoverProvider.java new file mode 100644 index 000000000..f6b7d3379 --- /dev/null +++ b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/hover/BootJavaHoverProvider.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * 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.hover; + +import java.nio.file.Path; +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.TextDocumentPositionParams; +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.HoverHandler; +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 BootJavaHoverProvider implements HoverHandler { + + private static final String SPRING_VALUE = "org.springframework.beans.factory.annotation.Value"; + + private JavaProjectFinder projectFinder; + private SimpleLanguageServer server; + + public BootJavaHoverProvider(SimpleLanguageServer server, JavaProjectFinder projectFinder) { + this.server = server; + this.projectFinder = projectFinder; + } + + @Override + public CompletableFuture handle(TextDocumentPositionParams params) { + SimpleTextDocumentService documents = server.getTextDocumentService(); + TextDocument doc = documents.get(params).copy(); + if (doc != null) { + try { + int offset = doc.toOffset(params.getPosition()); + CompletableFuture hoverResult = provideHover(doc, offset); + if (hoverResult != null) { + return hoverResult; + } + } + catch (Exception e) { + } + } + + return SimpleTextDocumentService.NO_HOVER; + } + + private CompletableFuture provideHover(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 provideHoverForAnnotation(node, offset, document); + } + + return null; + } + + private CompletableFuture provideHoverForAnnotation(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 provideHoverForSpringAnnotation(exactNode, annotation, type, offset, doc); + } + } + } + + return null; + } + + private CompletableFuture provideHoverForSpringAnnotation(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc) { + if (type.getQualifiedName().equals(SPRING_VALUE)) { + return new ValueHoverProvider().provideHoverForValueAnnotation(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/hover/ValueHoverProvider.java b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/hover/ValueHoverProvider.java new file mode 100644 index 000000000..08115e1d6 --- /dev/null +++ b/vscode-extensions/vscode-boot-java/src/main/java/org/springframework/ide/vscode/boot/java/hover/ValueHoverProvider.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * 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.hover; + +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.io.IOUtils; +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.Hover; +import org.eclipse.lsp4j.Range; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * @author Martin Lippert + */ +public class ValueHoverProvider { + + public CompletableFuture provideHoverForValueAnnotation(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 provideHover(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 provideHover(node.toString(), offset - node.getStartPosition(), node.getStartPosition(), doc); + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + private CompletableFuture provideHover(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()); + + JSONObject properties = getPropertiesFromProcess(); + if (propertyKey != null && properties != null) { + Iterator keys = properties.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + JSONObject props = properties.getJSONObject(key); + + if (props.has(propertyKey)) { + String propertyValue = props.getString(propertyKey); + + Range hoverRange = doc.toRange(nodeStartOffset + range.getStart(), range.getEnd() - range.getStart()); + + Hover hover = new Hover(); + List hoverContent = new ArrayList<>(); + + hoverContent.add("property value for " + propertyKey); + hoverContent.add(propertyValue); + hoverContent.add("coming from:"); + hoverContent.add(key); + + hover.setContents(hoverContent); + hover.setRange(hoverRange); + + return CompletableFuture.completedFuture(hover); + } + } + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public JSONObject getPropertiesFromProcess() { + try { + URL url = new URL("http://localhost:8080/env"); + + URLConnection con = url.openConnection(); + InputStream in = con.getInputStream(); + String encoding = con.getContentEncoding(); + encoding = encoding == null ? "UTF-8" : encoding; + String body = IOUtils.toString(in, encoding); + + JSONTokener tokener = new JSONTokener(body); + JSONObject jsonData = new JSONObject(tokener); + + return jsonData; + } + catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + public String getPropertyKey(String value, int offset) { + LocalRange range = getPropertyRange(value, offset); + if (range != null) { + return value.substring(range.getStart(), range.getEnd()); + } + return null; + } + + 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/hover/test/ValueHoverTest.java b/vscode-extensions/vscode-boot-java/src/test/java/org/springframework/ide/vscode/boot/java/hover/test/ValueHoverTest.java new file mode 100644 index 000000000..63a3401a9 --- /dev/null +++ b/vscode-extensions/vscode-boot-java/src/test/java/org/springframework/ide/vscode/boot/java/hover/test/ValueHoverTest.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * 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.hover.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.hover.ValueHoverProvider; + +/** + * @author Martin Lippert + */ +public class ValueHoverTest { + + @Test + public void testGetPropertyFromValue() { + ValueHoverProvider provider = new ValueHoverProvider(); + + assertNull(provider.getPropertyKey("${spring}", 0)); + assertNull(provider.getPropertyKey("${spring}", 1)); + assertEquals("spring", provider.getPropertyKey("${spring}", 2)); + assertEquals("spring", provider.getPropertyKey("${spring}", 3)); + assertEquals("spring", provider.getPropertyKey("${spring}", 8)); + assertNull(provider.getPropertyKey("${spring}", 9)); + + assertNull(provider.getPropertyKey("abc ${spring} and other stuff", 0)); + assertNull(provider.getPropertyKey("abc ${spring} and other stuff", 5)); + assertEquals("spring", provider.getPropertyKey("abc ${spring} and other stuff", 6)); + assertEquals("spring", provider.getPropertyKey("abc ${spring} and other stuff", 12)); + assertNull(provider.getPropertyKey("abc ${spring} and other stuff", 13)); + + assertNull(provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 5)); + assertEquals("spring", provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 6)); + assertEquals("spring", provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 12)); + assertNull(provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 13)); + + assertNull(provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 19)); + assertEquals("boot", provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 20)); + assertEquals("boot", provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 24)); + assertNull(provider.getPropertyKey("abc ${spring} and ${boot} other stuff", 25)); + } + +}