From aef5d309c61f3ab71d889fcdb55a63d65b2f6e12 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Mon, 16 Oct 2017 18:46:08 +0200 Subject: [PATCH] PT #151991870: first rough implementation for live hovers for components without autowired annotation --- .../boot/java/BootJavaLanguageServer.java | 2 + .../autowired/AutowiredHoverProvider.java | 64 ++--- .../java/autowired/SpringBootAppProvider.java | 22 ++ .../autowired/SpringBootAppProviderImpl.java | 41 ++++ .../java/beans/ComponentHoverProvider.java | 216 ++++++++++++++++ .../test/AutowiredHoverProviderTest.java | 59 +++++ .../test/ComponentHoverProviderTest.java | 232 ++++++++++++++++++ .../test/SpringIndexerBeansTest.java | 2 +- .../test/ScopeCompletionTest.java | 2 +- .../test/ValueCompletionTest.java | 2 +- .../{hover => value}/test/ValueHoverTest.java | 2 +- ...-bean-information-automatically-wired.json | 35 +++ .../test/MyAutomaticallyWiredComponent.java | 24 ++ 13 files changed, 671 insertions(+), 32 deletions(-) create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProvider.java create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProviderImpl.java create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentHoverProvider.java create mode 100644 headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ComponentHoverProviderTest.java rename headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/{utils => beans}/test/SpringIndexerBeansTest.java (98%) rename headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/{completions => scope}/test/ScopeCompletionTest.java (98%) rename headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/{completions => value}/test/ValueCompletionTest.java (99%) rename headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/{hover => value}/test/ValueHoverTest.java (97%) create mode 100644 headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/runtime-bean-information-automatically-wired.json create mode 100644 headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/src/main/java/org/test/MyAutomaticallyWiredComponent.java diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServer.java index bc83af68e..2deaf805a 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 @@ -19,6 +19,7 @@ import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider; +import org.springframework.ide.vscode.boot.java.beans.ComponentHoverProvider; import org.springframework.ide.vscode.boot.java.beans.ComponentSymbolProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeLensEngine; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine; @@ -235,6 +236,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { providers.put(org.springframework.ide.vscode.boot.java.requestmapping.Constants.SPRING_PATCH_MAPPING, new RequestMappingHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.autowired.Constants.SPRING_AUTOWIRED, new AutowiredHoverProvider()); + providers.put(org.springframework.ide.vscode.boot.java.beans.Constants.SPRING_COMPONENT, new ComponentHoverProvider()); return new BootJavaHoverProvider(this, javaProjectFinder, providers); } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index eec224e22..e15587835 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -36,30 +36,40 @@ public class AutowiredHoverProvider implements HoverProvider { @Override public CompletableFuture provideHover(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc, SpringBootApp[] runningApps) { + + SpringBootAppProvider[] bootApps = new SpringBootAppProvider[runningApps.length]; + for (int i = 0; i < runningApps.length; i++) { + bootApps[i] = new SpringBootAppProviderImpl(runningApps[i]); + } + return provideHover(node, annotation, type, offset, doc, bootApps); + } + + public CompletableFuture provideHover(ASTNode node, Annotation annotation, + ITypeBinding type, int offset, TextDocument doc, SpringBootAppProvider[] runningApps) { try { List> hoverContent = new ArrayList<>(); - if (runningApps.length > 0) { - for (SpringBootApp bootApp : runningApps) { - try { - String liveBeans = bootApp.getBeans(); - if (liveBeans != null && liveBeans.length() > 0) { - addLiveHoverContent(annotation, doc, liveBeans, bootApp, hoverContent); - } - } - catch (Exception e) { - e.printStackTrace(); + for (SpringBootAppProvider bootApp : runningApps) { + try { + String liveBeans = bootApp.getBeans(); + if (liveBeans != null && liveBeans.length() > 0) { + addLiveHoverContent(annotation, doc, liveBeans, bootApp, hoverContent); } } + catch (Exception e) { + e.printStackTrace(); + } } - Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); - Hover hover = new Hover(); + if (hoverContent.size() > 0) { + Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); + Hover hover = new Hover(); - hover.setContents(hoverContent); - hover.setRange(hoverRange); + hover.setContents(hoverContent); + hover.setRange(hoverRange); - return CompletableFuture.completedFuture(hover); + return CompletableFuture.completedFuture(hover); + } } catch (Exception e) { Log.log(e); } @@ -70,20 +80,18 @@ public class AutowiredHoverProvider implements HoverProvider { @Override public Range getLiveHoverHint(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { try { - if (runningApps.length > 0) { - for (SpringBootApp bootApp : runningApps) { - try { - String liveBeans = bootApp.getBeans(); - if (liveBeans != null && liveBeans.length() > 0) { - Range range = getLiveHoverHint(annotation, doc, liveBeans); - if (range != null) { - return range; - } + for (SpringBootApp bootApp : runningApps) { + try { + String liveBeans = bootApp.getBeans(); + if (liveBeans != null && liveBeans.length() > 0) { + Range range = getLiveHoverHint(annotation, doc, liveBeans); + if (range != null) { + return range; } } - catch (Exception e) { - e.printStackTrace(); - } + } + catch (Exception e) { + e.printStackTrace(); } } } @@ -115,7 +123,7 @@ public class AutowiredHoverProvider implements HoverProvider { return null; } - public void addLiveHoverContent(Annotation annotation, TextDocument doc, String liveBeansJSON, SpringBootApp bootApp, List> hoverContent) { + public void addLiveHoverContent(Annotation annotation, TextDocument doc, String liveBeansJSON, SpringBootAppProvider bootApp, List> hoverContent) { String type = findDeclaredType(annotation); if (type != null && liveBeansJSON != null) { diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProvider.java new file mode 100644 index 000000000..9a4a64d3e --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProvider.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * 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.autowired; + +/** + * @author Martin Lippert + */ +public interface SpringBootAppProvider { + + public String getBeans() throws Exception; + public String getProcessID(); + public String getProcessName(); + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProviderImpl.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProviderImpl.java new file mode 100644 index 000000000..af8e94c16 --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/SpringBootAppProviderImpl.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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.autowired; + +import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; + +/** + * @author Martin Lippert + */ +public class SpringBootAppProviderImpl implements SpringBootAppProvider { + + private SpringBootApp bootApp; + + public SpringBootAppProviderImpl(SpringBootApp bootApp) { + this.bootApp = bootApp; + } + + @Override + public String getBeans() throws Exception { + return bootApp.getBeans(); + } + + @Override + public String getProcessID() { + return bootApp.getProcessID(); + } + + @Override + public String getProcessName() { + return bootApp.getProcessName(); + } + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentHoverProvider.java new file mode 100644 index 000000000..b0cafb48c --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentHoverProvider.java @@ -0,0 +1,216 @@ +/******************************************************************************* + * 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; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MarkerAnnotation; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.MarkedString; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.springframework.ide.vscode.boot.java.autowired.Constants; +import org.springframework.ide.vscode.boot.java.autowired.LiveBean; +import org.springframework.ide.vscode.boot.java.autowired.LiveBeansModel; +import org.springframework.ide.vscode.boot.java.autowired.SpringBootAppProvider; +import org.springframework.ide.vscode.boot.java.autowired.SpringBootAppProviderImpl; +import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; +import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * @author Martin Lippert + */ +public class ComponentHoverProvider implements HoverProvider { + + @Override + public CompletableFuture provideHover(ASTNode node, Annotation annotation, + ITypeBinding type, int offset, TextDocument doc, SpringBootApp[] runningApps) { + SpringBootAppProvider[] bootApps = new SpringBootAppProvider[runningApps.length]; + for (int i = 0; i < runningApps.length; i++) { + bootApps[i] = new SpringBootAppProviderImpl(runningApps[i]); + } + return provideHover(node, annotation, type, offset, doc, bootApps); + } + + public CompletableFuture provideHover(ASTNode node, Annotation annotation, + ITypeBinding type, int offset, TextDocument doc, SpringBootAppProvider[] runningApps) { + try { + Range range = null; + + TypeDeclaration typeDecl = findDeclaredType(annotation); + if (typeDecl != null) { + MethodDeclaration constructor = findConstructor(typeDecl); + + if (constructor != null && !hasAutowiredAnnotation(constructor)) { + range = doc.toRange(constructor.getName().getStartPosition(), constructor.getName().getLength()); + + List> hoverContent = new ArrayList<>(); + for (SpringBootAppProvider bootApp : runningApps) { + try { + String liveBeans = bootApp.getBeans(); + if (liveBeans != null && liveBeans.length() > 0) { + addLiveHoverContent(typeDecl, doc, liveBeans, bootApp, hoverContent); + } + } + catch (Exception e) { + e.printStackTrace(); + } + } + + if (hoverContent.size() > 0) { + Hover hover = new Hover(); + + hover.setContents(hoverContent); + hover.setRange(range); + + return CompletableFuture.completedFuture(hover); + } + } + } + + } catch (Exception e) { + Log.log(e); + } + + return null; + } + + @Override + public Range getLiveHoverHint(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { + try { + for (SpringBootApp bootApp : runningApps) { + try { + String liveBeans = bootApp.getBeans(); + if (liveBeans != null && liveBeans.length() > 0) { + Range range = getLiveHoverHint(annotation, doc, liveBeans); + if (range != null) { + return range; + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + } + } + catch (Exception e) { + Log.log(e); + } + + return null; + } + + public Range getLiveHoverHint(Annotation annotation, TextDocument doc, String liveBeansJSON) { + try { + TypeDeclaration type = findDeclaredType(annotation); + if (type != null && liveBeansJSON != null) { + String typeName = type.resolveBinding().getQualifiedName(); + + LiveBeansModel beansModel = LiveBeansModel.parse(liveBeansJSON); + LiveBean[] beansOfType = beansModel.getBeansOfType(typeName); + + if (beansOfType.length > 0) { + MethodDeclaration constructor = findConstructor(type); + if (constructor != null && !hasAutowiredAnnotation(constructor)) { + Range hoverRange = doc.toRange(constructor.getName().getStartPosition(), constructor.getName().getLength()); + return hoverRange; + } + } + } + } + catch (BadLocationException e) { + Log.log(e); + } + + return null; + } + + private boolean hasAutowiredAnnotation(MethodDeclaration constructor) { + List modifiers = constructor.modifiers(); + for (Object modifier : modifiers) { + if (modifier instanceof MarkerAnnotation) { + ITypeBinding typeBinding = ((MarkerAnnotation) modifier).resolveTypeBinding(); + if (typeBinding != null && typeBinding.getQualifiedName().equals(Constants.SPRING_AUTOWIRED)) { + return true; + } + } + } + return false; + } + + + public void addLiveHoverContent(TypeDeclaration declaringType, TextDocument doc, String liveBeansJSON, SpringBootAppProvider bootApp, List> hoverContent) { + String type = declaringType.resolveBinding().getQualifiedName(); + if (type != null && liveBeansJSON != null) { + + LiveBeansModel beansModel = LiveBeansModel.parse(liveBeansJSON); + LiveBean[] beansOfType = beansModel.getBeansOfType(type); + + if (beansOfType.length > 0) { + String processId = bootApp.getProcessID(); + String processName = bootApp.getProcessName(); + + for (LiveBean liveBean : beansOfType) { + String[] dependencies = liveBean.getDependencies(); + + if (dependencies != null && dependencies.length > 0) { + hoverContent.add(Either.forLeft("bean: " + liveBean.getId())); + hoverContent.add(Either.forLeft("injected beans:")); + + for (String dependency : dependencies) { + LiveBean[] dependencyBeans = beansModel.getBeansOfName(dependency); + for (LiveBean dependencyBean : dependencyBeans) { + hoverContent.add(Either.forLeft("- '" + dependencyBean.getId() + "' - from: " + dependencyBean.getResource())); + } + } + } + else { + // TODO: no dependencies found + } + } + + hoverContent.add(Either.forLeft("Process ID: " + processId)); + hoverContent.add(Either.forLeft("Process Name: " + processName)); + } + } + } + + private TypeDeclaration findDeclaredType(Annotation annotation) { + ASTNode node = annotation; + while (node != null && !(node instanceof TypeDeclaration)) { + node = node.getParent(); + } + + return node != null ? (TypeDeclaration) node : null; + } + + private MethodDeclaration findConstructor(TypeDeclaration typeDecl) { + MethodDeclaration[] methods = typeDecl.getMethods(); + for (MethodDeclaration methodDeclaration : methods) { + if (methodDeclaration.isConstructor()) { + return methodDeclaration; + } + } + + return null; + } + +} diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java index 93b739753..096124017 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/AutowiredHoverProviderTest.java @@ -13,14 +13,17 @@ package org.springframework.ide.vscode.boot.java.autowired.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.io.File; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import org.eclipse.jdt.core.JavaCore; @@ -30,12 +33,16 @@ 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.NodeFinder; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.MarkedString; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.junit.Before; import org.junit.Test; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider; +import org.springframework.ide.vscode.boot.java.autowired.SpringBootAppProvider; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.CompositeJavaProjectManager; @@ -147,6 +154,58 @@ public class AutowiredHoverProviderTest { assertNull(hint); } + @Test + public void testLiveHoverContentForAutowiredOnConstructor() throws Exception { + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-autowired/").toURI()); + harness.intialize(directory); + + String docURI = "file://" + directory.getAbsolutePath() + "/src/main/java/org/test/MyAutowiredComponent.java"; + IJavaProject project = projectManager.find(directory); + TextDocument document = createTempTextDocument(docURI); + + CompilationUnit cu = parse(document, project); + + int offset = document.toOffset(new Position(11, 4)); + ASTNode node = NodeFinder.perform(cu, offset, 0).getParent(); + + AutowiredHoverProvider provider = new AutowiredHoverProvider(); + String beansJSON = new String(Files.readAllBytes(new File(directory, "runtime-bean-information.json").toPath())); + + SpringBootAppProvider bootApp = new SpringBootAppProvider() { + @Override + public String getProcessName() { + return "test process name"; + } + + @Override + public String getProcessID() { + return "test process id"; + } + + @Override + public String getBeans() throws Exception { + return beansJSON; + } + }; + CompletableFuture hoverFuture = provider.provideHover(null, (Annotation)node, null, offset, document, new SpringBootAppProvider[] {bootApp}); + Hover hover = hoverFuture.get(); + assertNotNull(hover); + + assertEquals(11, hover.getRange().getStart().getLine()); + assertEquals(1, hover.getRange().getStart().getCharacter()); + assertEquals(11, hover.getRange().getEnd().getLine()); + assertEquals(11, hover.getRange().getEnd().getCharacter()); + + List> contents = hover.getContents(); + assertEquals(6, contents.size()); + + assertTrue(contents.get(0).getLeft().contains("myAutowiredComponent")); + assertTrue(contents.get(2).getLeft().contains("dependencyA")); + assertTrue(contents.get(3).getLeft().contains("dependencyB")); + assertTrue(contents.get(4).getLeft().contains("test process id")); + assertTrue(contents.get(5).getLeft().contains("test process name")); + } + private TextDocument createTempTextDocument(String docURI) throws Exception { Path path = Paths.get(new URI(docURI)); String content = new String(Files.readAllBytes(path)); diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ComponentHoverProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ComponentHoverProviderTest.java new file mode 100644 index 000000000..a574c7ca6 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/ComponentHoverProviderTest.java @@ -0,0 +1,232 @@ +/******************************************************************************* + * 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 static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +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.NodeFinder; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.MarkedString; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.junit.Before; +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.BootJavaLanguageServer; +import org.springframework.ide.vscode.boot.java.autowired.SpringBootAppProvider; +import org.springframework.ide.vscode.boot.java.beans.ComponentHoverProvider; +import org.springframework.ide.vscode.commons.java.IClasspath; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.java.CompositeJavaProjectManager; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectManager; +import org.springframework.ide.vscode.commons.maven.MavenCore; +import org.springframework.ide.vscode.commons.maven.java.MavenProjectManager; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.BasicFileObserver; +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.LanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.ide.vscode.project.harness.PropertyIndexHarness; + +/** + * @author Martin Lippert + */ +public class ComponentHoverProviderTest { + + private JavaProjectManager projectManager; + private LanguageServerHarness harness; + private PropertyIndexHarness indexHarness; + private BasicFileObserver fileObserver; + + @Before + public void setup() throws Exception { + projectManager = new CompositeJavaProjectManager(new JavaProjectManager[] {new MavenProjectManager(MavenCore.getDefault())}); + fileObserver = new BasicFileObserver(); + projectManager.setFileObserver(fileObserver); + + indexHarness = new PropertyIndexHarness(); + harness = new LanguageServerHarness(new Callable() { + @Override + public BootJavaLanguageServer call() throws Exception { + BootJavaLanguageServer server = new BootJavaLanguageServer(projectManager, indexHarness.getIndexProvider()); + return server; + } + }) { + @Override + protected String getFileExtension() { + return ".java"; + } + }; + } + + @Test + public void testLiveHoverHintForAutomaicallywiredConstructor() throws Exception { + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-autowired/").toURI()); + harness.intialize(directory); + + String docURI = "file://" + directory.getAbsolutePath() + "/src/main/java/org/test/MyAutomaticallyWiredComponent.java"; + IJavaProject project = projectManager.find(directory); + TextDocument document = createTempTextDocument(docURI); + + CompilationUnit cu = parse(document, project); + + int offset = document.toOffset(new Position(4, 2)); + ASTNode node = NodeFinder.perform(cu, offset, 0).getParent(); + + ComponentHoverProvider provider = new ComponentHoverProvider(); + String beansJSON = new String(Files.readAllBytes(new File(directory, "runtime-bean-information-automatically-wired.json").toPath())); + + Range hint = provider.getLiveHoverHint((Annotation)node, document, beansJSON); + assertNotNull(hint); + + assertEquals(10, hint.getStart().getLine()); + assertEquals(8, hint.getStart().getCharacter()); + assertEquals(10, hint.getEnd().getLine()); + assertEquals(37, hint.getEnd().getCharacter()); + } + + @Test + public void testNoLiveHoverHintForComponentWhenAutowiredAnnotationIsUsed() throws Exception { + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-autowired/").toURI()); + harness.intialize(directory); + + String docURI = "file://" + directory.getAbsolutePath() + "/src/main/java/org/test/MyAutowiredComponent.java"; + IJavaProject project = projectManager.find(directory); + TextDocument document = createTempTextDocument(docURI); + + CompilationUnit cu = parse(document, project); + + int offset = document.toOffset(new Position(5, 2)); + ASTNode node = NodeFinder.perform(cu, offset, 0).getParent(); + + ComponentHoverProvider provider = new ComponentHoverProvider(); + String beansJSON = new String(Files.readAllBytes(new File(directory, "runtime-bean-information.json").toPath())); + + Range hint = provider.getLiveHoverHint((Annotation)node, document, beansJSON); + assertNull(hint); + } + + @Test + public void testLiveHoverContentForAutomaicallywiredConstructor() throws Exception { + File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-autowired/").toURI()); + harness.intialize(directory); + + String docURI = "file://" + directory.getAbsolutePath() + "/src/main/java/org/test/MyAutomaticallyWiredComponent.java"; + IJavaProject project = projectManager.find(directory); + TextDocument document = createTempTextDocument(docURI); + + CompilationUnit cu = parse(document, project); + + int offset = document.toOffset(new Position(4, 2)); + ASTNode node = NodeFinder.perform(cu, offset, 0).getParent(); + + ComponentHoverProvider provider = new ComponentHoverProvider(); + String beansJSON = new String(Files.readAllBytes(new File(directory, "runtime-bean-information-automatically-wired.json").toPath())); + + SpringBootAppProvider bootApp = new SpringBootAppProvider() { + @Override + public String getProcessName() { + return "test process name"; + } + + @Override + public String getProcessID() { + return "test process id"; + } + + @Override + public String getBeans() throws Exception { + return beansJSON; + } + }; + CompletableFuture hoverFuture = provider.provideHover(null, (Annotation) node, null, 0, document, new SpringBootAppProvider[] {bootApp}); + Hover hover = hoverFuture.get(); + + assertNotNull(hover); + + assertEquals(10, hover.getRange().getStart().getLine()); + assertEquals(8, hover.getRange().getStart().getCharacter()); + assertEquals(10, hover.getRange().getEnd().getLine()); + assertEquals(37, hover.getRange().getEnd().getCharacter()); + + List> contents = hover.getContents(); + assertEquals(6, contents.size()); + + assertTrue(contents.get(0).getLeft().contains("myAutomaticallyWiredComponent")); + assertTrue(contents.get(2).getLeft().contains("dependencyA")); + assertTrue(contents.get(3).getLeft().contains("dependencyB")); + assertTrue(contents.get(4).getLeft().contains("test process id")); + assertTrue(contents.get(5).getLeft().contains("test process name")); + } + + private TextDocument createTempTextDocument(String docURI) throws Exception { + Path path = Paths.get(new URI(docURI)); + String content = new String(Files.readAllBytes(path)); + + TextDocument doc = new TextDocument(docURI, LanguageId.PLAINTEXT, 0, content); + return doc; + } + + private CompilationUnit parse(TextDocument document, IJavaProject project) + throws Exception, BadLocationException { + 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(project); + 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); + return cu; + } + + private String[] getClasspathEntries(IJavaProject project) throws Exception { + 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/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerBeansTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java similarity index 98% rename from headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerBeansTest.java rename to headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java index c1e150164..52e5ffc12 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerBeansTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/SpringIndexerBeansTest.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.utils.test; +package org.springframework.ide.vscode.boot.java.beans.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/completions/test/ScopeCompletionTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/scope/test/ScopeCompletionTest.java similarity index 98% rename from headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/completions/test/ScopeCompletionTest.java rename to headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/scope/test/ScopeCompletionTest.java index 98e0875b3..e241d36db 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/completions/test/ScopeCompletionTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/scope/test/ScopeCompletionTest.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.completions.test; +package org.springframework.ide.vscode.boot.java.scope.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/completions/test/ValueCompletionTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java similarity index 99% rename from headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/completions/test/ValueCompletionTest.java rename to headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java index 78ead4e6a..f61acbf66 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/completions/test/ValueCompletionTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueCompletionTest.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.completions.test; +package org.springframework.ide.vscode.boot.java.value.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/hover/test/ValueHoverTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueHoverTest.java similarity index 97% rename from headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/hover/test/ValueHoverTest.java rename to headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueHoverTest.java index 48b816b85..01ac4e463 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/hover/test/ValueHoverTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueHoverTest.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.hover.test; +package org.springframework.ide.vscode.boot.java.value.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; diff --git a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/runtime-bean-information-automatically-wired.json b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/runtime-bean-information-automatically-wired.json new file mode 100644 index 000000000..00d0c0a6b --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/runtime-bean-information-automatically-wired.json @@ -0,0 +1,35 @@ +[ + { + "context": "application", + "parent": null, + "beans": [ + { + "bean": "dependencyA", + "aliases": [], + "scope": "singleton", + "type": "org.test.DependencyA", + "resource": "file [/Users/mlippert/Entwicklung/vmware/projects/sts4/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/target/classes/org/test/DependencyA.class]", + "dependencies": [] + }, + { + "bean": "dependencyB", + "aliases": [], + "scope": "singleton", + "type": "org.test.DependencyB", + "resource": "file [/Users/mlippert/Entwicklung/vmware/projects/sts4/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/target/classes/org/test/DependencyB.class]", + "dependencies": [] + }, + { + "bean": "myAutomaticallyWiredComponent", + "aliases": [], + "scope": "singleton", + "type": "org.test.MyAutomaticallyWiredComponent", + "resource": "file [/Users/mlippert/Entwicklung/vmware/projects/sts4/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/target/classes/org/test/MyAutomaticallyWiredComponent.class]", + "dependencies": [ + "dependencyA", + "dependencyB" + ] + } + ] + } +] \ No newline at end of file diff --git a/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/src/main/java/org/test/MyAutomaticallyWiredComponent.java b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/src/main/java/org/test/MyAutomaticallyWiredComponent.java new file mode 100644 index 000000000..00d398869 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/resources/test-projects/test-annotation-indexing-autowired/src/main/java/org/test/MyAutomaticallyWiredComponent.java @@ -0,0 +1,24 @@ +package org.test; + +import org.springframework.stereotype.Component; + +@Component +public class MyAutomaticallyWiredComponent { + + private DependencyA depA; + private DependencyB depB; + + public MyAutomaticallyWiredComponent(DependencyA depA, DependencyB depB) { + this.depA = depA; + this.depB = depB; + } + + public DependencyA getDepA() { + return depA; + } + + public DependencyB getDepB() { + return depB; + } + +}