From 9f9c29d3b7229920722c501fdb5a50a79c16cfa0 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Sun, 15 Oct 2017 13:44:48 +0200 Subject: [PATCH] PT #151052858: added simple hover content for autowired annotation live hovers --- .../autowired/AutowiredHoverProvider.java | 148 ++++++++++++------ .../vscode/boot/java/autowired/LiveBean.java | 83 ++++++++++ .../boot/java/autowired/LiveBeansModel.java | 88 +++++++++++ .../test/AutowiredHoverProviderTest.java | 12 +- .../autowired/test/LiveBeansModelTest.java | 68 ++++++++ .../empty-live-beans-model.json | 8 + .../simple-live-beans-model.json | 35 +++++ .../totally-empty-live-beans-model.json | 0 8 files changed, 391 insertions(+), 51 deletions(-) create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBean.java create mode 100644 headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBeansModel.java create mode 100644 headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/LiveBeansModelTest.java create mode 100644 headless-services/boot-java-language-server/src/test/resources/live-beans-models/empty-live-beans-model.json create mode 100644 headless-services/boot-java-language-server/src/test/resources/live-beans-models/simple-live-beans-model.json create mode 100644 headless-services/boot-java-language-server/src/test/resources/live-beans-models/totally-empty-live-beans-model.json 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 6bb023702..eec224e22 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 @@ -17,6 +17,7 @@ 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.TypeDeclaration; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.MarkedString; import org.eclipse.lsp4j.Range; @@ -35,64 +36,22 @@ public class AutowiredHoverProvider implements HoverProvider { @Override public CompletableFuture provideHover(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc, SpringBootApp[] runningApps) { - return provideHover(annotation, doc, runningApps); - } - - @Override - public Range getLiveHoverHint(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { try { - if (runningApps.length > 0) { + List> hoverContent = new ArrayList<>(); - List liveBeanInfo = new ArrayList<>(); + if (runningApps.length > 0) { for (SpringBootApp bootApp : runningApps) { try { String liveBeans = bootApp.getBeans(); if (liveBeans != null && liveBeans.length() > 0) { - liveBeanInfo.add(liveBeans); + addLiveHoverContent(annotation, doc, liveBeans, bootApp, hoverContent); } } catch (Exception e) { e.printStackTrace(); } } - - if (!liveBeanInfo.isEmpty()) { - return getLiveHoverHint(annotation, doc, liveBeanInfo.toArray(new String[liveBeanInfo.size()])); - } - - // TODO: real work - Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); - return hoverRange; } - } - catch (BadLocationException e) { - Log.log(e); - } - - return null; - } - - public Range getLiveHoverHint(Annotation annotation, TextDocument doc, String[] liveBeanJSON) { - try { - if (liveBeanJSON.length > 0) { - // TODO: real work - Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); - return hoverRange; - } - } - catch (BadLocationException e) { - Log.log(e); - } - - return null; - } - - private CompletableFuture provideHover(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { - - try { - List> hoverContent = new ArrayList<>(); - - // TODO: real work Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); Hover hover = new Hover(); @@ -108,4 +67,103 @@ public class AutowiredHoverProvider implements HoverProvider { return null; } + @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; + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + } + } + } + catch (Exception e) { + Log.log(e); + } + + return null; + } + + public Range getLiveHoverHint(Annotation annotation, TextDocument doc, String liveBeansJSON) { + try { + String type = findDeclaredType(annotation); + if (type != null && liveBeansJSON != null) { + + LiveBeansModel beansModel = LiveBeansModel.parse(liveBeansJSON); + LiveBean[] beansOfType = beansModel.getBeansOfType(type); + + if (beansOfType.length > 0) { + Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); + return hoverRange; + } + } + } + catch (BadLocationException e) { + Log.log(e); + } + + return null; + } + + public void addLiveHoverContent(Annotation annotation, TextDocument doc, String liveBeansJSON, SpringBootApp bootApp, List> hoverContent) { + String type = findDeclaredType(annotation); + 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 String findDeclaredType(Annotation annotation) { + ASTNode node = annotation; + while (node != null && !(node instanceof TypeDeclaration)) { + node = node.getParent(); + } + + if (node != null) { + TypeDeclaration typeDecl = (TypeDeclaration) node; + return typeDecl.resolveBinding().getQualifiedName(); + } + else { + return null; + } + } + } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBean.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBean.java new file mode 100644 index 000000000..6e7b29cde --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBean.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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.json.JSONArray; +import org.json.JSONObject; + +/** + * @author Martin Lippert + */ +public class LiveBean { + + public static LiveBean parse(JSONObject beansJSON) { + String id = beansJSON.optString("bean"); + String type = beansJSON.optString("type"); + String scope = beansJSON.optString("scope"); + String resource = beansJSON.optString("resource"); + + JSONArray aliasesJSON = beansJSON.getJSONArray("aliases"); + String[] aliases = new String[aliasesJSON.length()]; + for (int i = 0; i < aliasesJSON.length(); i++) { + aliases[i] = aliasesJSON.optString(i); + } + + JSONArray dependenciesJSON = beansJSON.getJSONArray("dependencies"); + String[] dependencies = new String[dependenciesJSON.length()]; + for (int i = 0; i < dependenciesJSON.length(); i++) { + dependencies[i] = dependenciesJSON.optString(i); + } + + return new LiveBean(id, aliases, scope, type, resource, dependencies); + } + + private final String id; + private final String[] aliases; + private final String scope; + private final String type; + private final String resource; + private final String[] dependencies; + + protected LiveBean(String id, String[] aliases, String scope, String type, String resource, String[] dependencies) { + super(); + this.id = id; + this.aliases = aliases; + this.scope = scope; + this.type = type; + this.resource = resource; + this.dependencies = dependencies; + } + + public String getId() { + return id; + } + + public String[] getAliases() { + return aliases; + } + + public String getScope() { + return scope; + } + + public String getType() { + return type; + } + + public String getResource() { + return resource; + } + + public String[] getDependencies() { + return dependencies; + } + +} diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBeansModel.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBeansModel.java new file mode 100644 index 000000000..85071675c --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/LiveBeansModel.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * @author Martin Lippert + */ +public class LiveBeansModel { + + public static LiveBeansModel parse(String json) { + LiveBeansModel model = new LiveBeansModel(); + + try { + JSONArray mainArray = new JSONArray(json); + + for (int i = 0; i < mainArray.length(); i++) { + JSONObject appContext = mainArray.getJSONObject(i); + if (appContext == null) continue; + + JSONArray beansArray = appContext.optJSONArray("beans"); + if (beansArray == null) continue; + + for (int j = 0; j < beansArray.length(); j++) { + JSONObject beanObject = beansArray.getJSONObject(j); + if (beanObject == null) continue; + + LiveBean bean = LiveBean.parse(beanObject); + if (bean != null) { + model.add(bean); + } + } + } + } + catch (JSONException e) { + e.printStackTrace(); + } + + return model; + } + + private final ConcurrentMap> beansViaType; + private final ConcurrentMap> beansViaName; + + protected LiveBeansModel() { + this.beansViaType = new ConcurrentHashMap<>(); + this.beansViaName = new ConcurrentHashMap<>(); + } + + public LiveBean[] getBeansOfType(String fullyQualifiedType) { + List result = beansViaType.get(fullyQualifiedType); + return result != null ? result.toArray(new LiveBean[result.size()]) : new LiveBean[0]; + } + + public LiveBean[] getBeansOfName(String beanName) { + List result = beansViaName.get(beanName); + return result != null ? result.toArray(new LiveBean[result.size()]) : new LiveBean[0]; + } + + protected void add(LiveBean bean) { + String type = bean.getType(); + if (type != null) { + beansViaType.computeIfAbsent(type, (t) -> new ArrayList<>()).add(bean); + } + + String name = bean.getId(); + if (name != null) { + beansViaName.computeIfAbsent(name, (n) -> new ArrayList<>()).add(bean); + } + } + +} 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 5e722263f..93b739753 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 @@ -98,7 +98,7 @@ public class AutowiredHoverProviderTest { AutowiredHoverProvider provider = new AutowiredHoverProvider(); String beansJSON = new String(Files.readAllBytes(new File(directory, "runtime-bean-information.json").toPath())); - Range hint = provider.getLiveHoverHint((Annotation)node, document, new String[] {beansJSON}); + Range hint = provider.getLiveHoverHint((Annotation)node, document, beansJSON); assertNotNull(hint); assertEquals(11, hint.getStart().getLine()); @@ -122,10 +122,10 @@ public class AutowiredHoverProviderTest { ASTNode node = NodeFinder.perform(cu, offset, 0).getParent(); AutowiredHoverProvider provider = new AutowiredHoverProvider(); - Range hint = provider.getLiveHoverHint((Annotation)node, document, new String[] {}); + Range hint = provider.getLiveHoverHint((Annotation)node, document, (String)null); assertNull(hint); } -/** + @Test public void testNoLiveHoverHintForAutowiredOnConstructorWithWrongLiveAppData() throws Exception { File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotation-indexing-autowired/").toURI()); @@ -141,12 +141,12 @@ public class AutowiredHoverProviderTest { 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())); + String beansJSON = new String(Files.readAllBytes(new File(directory, "wrong-runtime-bean-information.json").toPath())); - Range hint = provider.getLiveHoverHint((Annotation)node, document, new String[] {beansJSON}); + Range hint = provider.getLiveHoverHint((Annotation)node, document, beansJSON); assertNull(hint); } -*/ + 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/autowired/test/LiveBeansModelTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/LiveBeansModelTest.java new file mode 100644 index 000000000..5c08df184 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/autowired/test/LiveBeansModelTest.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * 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.test; + +import static org.junit.Assert.assertEquals; + +import org.apache.commons.io.IOUtils; +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.autowired.LiveBean; +import org.springframework.ide.vscode.boot.java.autowired.LiveBeansModel; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; + +/** + * @author Martin Lippert + */ +public class LiveBeansModelTest { + + @Test + public void testSimpleModel() throws Exception { + String json = IOUtils.toString(ProjectsHarness.class.getResourceAsStream("/live-beans-models/simple-live-beans-model.json")); + LiveBeansModel model = LiveBeansModel.parse(json); + + LiveBean[] bean = model.getBeansOfType("org.test.DependencyA"); + assertEquals(1, bean.length); + assertEquals("dependencyA", bean[0].getId()); + assertEquals("singleton", bean[0].getScope()); + assertEquals("org.test.DependencyA", bean[0].getType()); + assertEquals("file [/test-projects/classes/org/test/DependencyA.class]", bean[0].getResource()); + assertEquals(0, bean[0].getAliases().length); + assertEquals(0, bean[0].getDependencies().length); + + bean = model.getBeansOfName("dependencyB"); + assertEquals(1, bean.length); + assertEquals("dependencyB", bean[0].getId()); + assertEquals("singleton", bean[0].getScope()); + assertEquals("org.test.DependencyB", bean[0].getType()); + assertEquals("file [/test-projects/classes/org/test/DependencyB.class]", bean[0].getResource()); + assertEquals(0, bean[0].getAliases().length); + assertEquals(0, bean[0].getDependencies().length); + } + + @Test + public void testEmptyModel() throws Exception { + String json = IOUtils.toString(ProjectsHarness.class.getResourceAsStream("/live-beans-models/empty-live-beans-model.json")); + LiveBeansModel model = LiveBeansModel.parse(json); + + LiveBean[] bean = model.getBeansOfType("org.test.DependencyA"); + assertEquals(0, bean.length); + } + + @Test + public void testTotallyEmptyModel() throws Exception { + String json = IOUtils.toString(ProjectsHarness.class.getResourceAsStream("/live-beans-models/totally-empty-live-beans-model.json")); + LiveBeansModel model = LiveBeansModel.parse(json); + + LiveBean[] bean = model.getBeansOfType("org.test.DependencyA"); + assertEquals(0, bean.length); + } + +} diff --git a/headless-services/boot-java-language-server/src/test/resources/live-beans-models/empty-live-beans-model.json b/headless-services/boot-java-language-server/src/test/resources/live-beans-models/empty-live-beans-model.json new file mode 100644 index 000000000..bd04ec71a --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/resources/live-beans-models/empty-live-beans-model.json @@ -0,0 +1,8 @@ +[ + { + "context": "application", + "parent": null, + "beans": [ + ] + } +] diff --git a/headless-services/boot-java-language-server/src/test/resources/live-beans-models/simple-live-beans-model.json b/headless-services/boot-java-language-server/src/test/resources/live-beans-models/simple-live-beans-model.json new file mode 100644 index 000000000..f1dc99e12 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/resources/live-beans-models/simple-live-beans-model.json @@ -0,0 +1,35 @@ +[ + { + "context": "application", + "parent": null, + "beans": [ + { + "bean": "dependencyA", + "aliases": [], + "scope": "singleton", + "type": "org.test.DependencyA", + "resource": "file [/test-projects/classes/org/test/DependencyA.class]", + "dependencies": [] + }, + { + "bean": "dependencyB", + "aliases": [], + "scope": "singleton", + "type": "org.test.DependencyB", + "resource": "file [/test-projects/classes/org/test/DependencyB.class]", + "dependencies": [] + }, + { + "bean": "myAutowiredComponent", + "aliases": [], + "scope": "singleton", + "type": "org.test.MyAutowiredComponent", + "resource": "file [/test-projects/classes/org/test/MyAutowiredComponent.class]", + "dependencies": [ + "dependencyA", + "dependencyB" + ] + } + ] + } +] \ No newline at end of file diff --git a/headless-services/boot-java-language-server/src/test/resources/live-beans-models/totally-empty-live-beans-model.json b/headless-services/boot-java-language-server/src/test/resources/live-beans-models/totally-empty-live-beans-model.json new file mode 100644 index 000000000..e69de29bb