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 364d2215e..b812d3a83 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 @@ -34,6 +34,7 @@ import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; +import org.springframework.ide.vscode.boot.java.livehover.InjectedIntoProvider; import org.springframework.ide.vscode.boot.java.profiles.ActiveProfilesProvider; import org.springframework.ide.vscode.boot.java.requestmapping.LiveAppURLSymbolProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider; @@ -261,7 +262,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { providers.put(ActiveProfilesProvider.ANNOTATION, new ActiveProfilesProvider()); 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()); + providers.put(org.springframework.ide.vscode.boot.java.beans.Constants.SPRING_COMPONENT, new InjectedIntoProvider()); providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL, new ConditionalsLiveHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.conditionals.Constants.CONDITIONAL_ON_BEAN, new ConditionalsLiveHoverProvider()); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/InjectedIntoProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/InjectedIntoProvider.java new file mode 100644 index 000000000..e9bcd1bdc --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/InjectedIntoProvider.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * 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.livehover; + +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.TypeDeclaration; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +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.boot.app.cli.livebean.LiveBean; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.google.common.collect.ImmutableList; + +public class InjectedIntoProvider implements HoverProvider { + + @Override + public Collection getLiveHoverHints(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { + //Highlight if any running app contains an instance of this component + try { + if (runningApps.length > 0) { + String beanType = getAnnotatedType(annotation); + if (beanType!=null) { + if (Stream.of(runningApps).anyMatch(app -> !app.getBeans().getBeansOfType(beanType).isEmpty())) { + return ImmutableList.of(doc.toRange(annotation.getStartPosition(), annotation.getLength())); + } + } + } + } catch (Exception e) { + Log.log(e); + } + return ImmutableList.of(); + } + + @Override + public CompletableFuture provideHover(ASTNode node, Annotation annotation, ITypeBinding type, int offset, + TextDocument doc, SpringBootApp[] runningApps) { + if (runningApps.length>0) { + String beanType = getAnnotatedType(annotation); + if (beanType!=null) { + StringBuilder hover = new StringBuilder(); + hover.append("**Injection report for `"+beanType+"'**\n\n"); + boolean hasInterestingApp = false; + for (SpringBootApp app : runningApps) { + LiveBeansModel beans = app.getBeans(); + boolean isInteresting = !app.getBeans().getBeansOfType(beanType).isEmpty(); + if (isInteresting) { + hasInterestingApp = true; + hover.append(niceAppName(app)+":"); + for (LiveBean bean : beans.getBeansOfType(beanType)) { + hover.append("\n\n"); + hover.append("Bean with id: `"+bean.getId()+"`\n"); + List dependers = beans.getBeansDependingOn(bean.getId()); + if (dependers.isEmpty()) { + hover.append("**Not injected anywhere**\n"); + } else { + hover.append("injected into:\n\n"); + for (LiveBean dependingBean : dependers) { + hover.append("- "+dependingBean.getType()+"\n"); + } + } + } + } + } + if (hasInterestingApp) { + System.out.println(hover); + return CompletableFuture.completedFuture(new Hover( + ImmutableList.of(Either.forLeft(hover.toString())) + )); + } + } + } + return null; + } + + private String getAnnotatedType(Annotation annotation) { + ASTNode parent = annotation.getParent(); + if (parent instanceof TypeDeclaration) { + TypeDeclaration typeDecl = (TypeDeclaration) parent; + ITypeBinding binding = typeDecl.resolveBinding(); + if (binding!=null) { + return binding.getQualifiedName(); + } + } + return null; + } + + private String niceAppName(SpringBootApp app) { + return "Process [PID="+app.getProcessID()+", name=`"+app.getProcessName()+"`]"; + } + +} diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/profile/ActiveProfilesHoverTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ActiveProfilesHoverTest.java similarity index 98% rename from headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/profile/ActiveProfilesHoverTest.java rename to headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ActiveProfilesHoverTest.java index 20847d991..ff6fbea69 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/profile/ActiveProfilesHoverTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/ActiveProfilesHoverTest.java @@ -8,7 +8,7 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.profile; +package org.springframework.ide.vscode.boot.java.livehover.test; import java.time.Duration; diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/InjectedIntoProviderTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/InjectedIntoProviderTest.java new file mode 100644 index 000000000..56bb067ce --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/livehover/test/InjectedIntoProviderTest.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * 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.livehover.test; + +import static org.junit.Assert.assertTrue; + +import java.time.Duration; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBean; +import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; +import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.MockRunningAppProvider; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness.CustomizableProjectContent; +import org.springframework.ide.vscode.project.harness.ProjectsHarness.ProjectCustomizer; + +public class InjectedIntoProviderTest { + + private static final ProjectCustomizer FOO_INTERFACE = (CustomizableProjectContent p) -> { + p.createType("com.examle.Foo", + "package com.example;\n" + + "\n" + + "public interface Foo {\n" + + " void doSomeFoo();\n" + + "}\n" + ); + }; + + private BootLanguageServerHarness harness; + private ProjectsHarness projects = ProjectsHarness.INSTANCE; + + private MockRunningAppProvider mockAppProvider; + + @Before + public void setup() throws Exception { + mockAppProvider = new MockRunningAppProvider(); + harness = BootLanguageServerHarness.builder() + .mockDefaults() + .runningAppProvider(mockAppProvider.provider) + .watchDogInterval(Duration.ofMillis(100)) + .build(); + + MavenJavaProject jp = projects.mavenProject("empty-boot-15-web-app", FOO_INTERFACE); + assertTrue(jp.getClasspath().findType("com.example.Foo").exists()); + harness.useProject(projects.mavenProject("empty-boot-15-web-app")); + harness.intialize(null); + } + + @Test public void componentWithNoInjections() throws Exception { + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@Component"); + editor.assertHoverExactText("@Component", + "**Injection report for `com.example.FooImplementation'**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean with id: `fooImplementation`\n" + + "**Not injected anywhere**\n" + ); + } + + @Test public void componentWithOneInjection() throws Exception { + //Like componentWithNoInjections but checks whether we are properly using the bean id + // to determine injections. + LiveBeansModel beans = LiveBeansModel.builder() + .add(LiveBean.builder() + .id("fooImplementation") + .type("com.example.FooImplementation") + .build() + ) + .add(LiveBean.builder() + .id("myController") + .type("com.example.MyController") + .dependencies("fooImplementation") + .build() + ) + .build(); + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("111") + .processName("the-app") + .beans(beans) + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights("@Component"); + editor.assertHoverExactText("@Component", + "**Injection report for `com.example.FooImplementation'**\n" + + "\n" + + "Process [PID=111, name=`the-app`]:\n" + + "\n" + + "Bean with id: `fooImplementation`\n" + + "injected into:\n" + + "\n" + + "- com.example.MyController\n" + ); + } + + @Test public void hoHoversWhenNoRunningApps() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Component;\n" + + "\n" + + "@Component\n" + + "public class FooImplementation implements Foo {\n" + + "\n" + + " @Override\n" + + " public void doSomeFoo() {\n" + + " System.out.println(\"Foo do do do!\");\n" + + " }\n" + + "}\n" + ); + editor.assertHighlights(/*MONE*/); + editor.assertNoHover("@Component"); + } + +} diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java index d6df3851d..f7f656532 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java @@ -68,7 +68,11 @@ public class MockRunningAppProvider { } public MockAppBuilder beans(String beans) throws Exception { - when(app.getBeans()).thenReturn(LiveBeansModel.parse(beans)); + return beans(LiveBeansModel.parse(beans)); + } + + public MockAppBuilder beans(LiveBeansModel beans) { + when(app.getBeans()).thenReturn(beans); return this; } diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java index 516a0c3d4..8b8a7392c 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java @@ -10,65 +10,131 @@ *******************************************************************************/ package org.springframework.ide.vscode.project.harness; +import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeoutException; +import org.apache.commons.io.FileUtils; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.maven.MavenBuilder; import org.springframework.ide.vscode.commons.maven.MavenCore; import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; import org.springframework.ide.vscode.commons.maven.java.classpathfile.JavaProjectWithClasspathFile; +import org.springframework.ide.vscode.project.harness.ProjectsHarness.CustomizableProjectContent; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.io.Files; + +import reactor.util.function.Tuple3; +import reactor.util.function.Tuples; + +import org.springframework.ide.vscode.commons.util.IOUtil; /** * Test projects harness - * + * * @author Alex Boyko + * @author Kris De Volder */ public class ProjectsHarness { - - public static final ProjectsHarness INSTANCE = new ProjectsHarness();; - - public Cache cache = CacheBuilder.newBuilder().concurrencyLevel(1).build(); - + + public static final ProjectsHarness INSTANCE = new ProjectsHarness();; + + public Cache cache = CacheBuilder.newBuilder().concurrencyLevel(1).build(); + + /** + * A callback that is given a chance to make changes to test project contents before the test project + * is created from it. + */ + public interface ProjectCustomizer { + void customize(CustomizableProjectContent projectContent) throws Exception; + } + + /** + * Provides convenience apis to make changes to project content from a {@link ProjectCustomizer} + */ + public static class CustomizableProjectContent { + private final File projectRoot; + + public CustomizableProjectContent(File projectRoot) { + this.projectRoot = projectRoot; + } + + public void createFile(String path, String content) throws Exception { + File target = new File(projectRoot, path); + IOUtil.pipe(new ByteArrayInputStream(content.getBytes("UTF8")), target); + } + + public void createType(String fqName, String sourceCode) throws Exception { + String sourceFile = sourceFolder()+"/"+fqName.replace('.', '/')+".java"; + createFile(sourceFile, sourceCode); + } + + private String sourceFolder() { + return "src/main/java"; + } + } + private enum ProjectType { MAVEN, CLASSPATH_TXT } - + private ProjectsHarness() { } - + + public IJavaProject project(ProjectType type, String name, ProjectCustomizer customizer) throws Exception { + Tuple3 key = Tuples.of(type, name, customizer); + return cache.get(key, () -> { + Path baseProjectPath = getProjectPath(name); + File testProjectRoot = Files.createTempDir(); + FileUtils.copyDirectory(baseProjectPath.toFile(), testProjectRoot); + customizer.customize(new CustomizableProjectContent(testProjectRoot)); + return createProject(type, testProjectRoot.toPath()); + }); + } + + private IJavaProject createProject(ProjectType type, Path testProjectPath) throws Exception { + switch (type) { + case MAVEN: + MavenBuilder.newBuilder(testProjectPath).clean().pack().javadoc().skipTests().execute(); + return new MavenJavaProject(MavenCore.getDefault(), testProjectPath.resolve(MavenCore.POM_XML).toFile()); + case CLASSPATH_TXT: + MavenBuilder.newBuilder(testProjectPath).clean().pack().skipTests().execute(); + return new JavaProjectWithClasspathFile(testProjectPath.resolve(MavenCore.CLASSPATH_TXT).toFile()); + default: + throw new IllegalStateException("Bug!!! Missing case"); + } + } + public IJavaProject project(ProjectType type, String name) throws Exception { return cache.get(type + "/" + name, () -> { Path testProjectPath = getProjectPath(name); - switch (type) { - case MAVEN: - MavenBuilder.newBuilder(testProjectPath).clean().pack().javadoc().skipTests().execute(); - return new MavenJavaProject(MavenCore.getDefault(), testProjectPath.resolve(MavenCore.POM_XML).toFile()); - case CLASSPATH_TXT: - MavenBuilder.newBuilder(testProjectPath).clean().pack().skipTests().execute(); - return new JavaProjectWithClasspathFile(testProjectPath.resolve(MavenCore.CLASSPATH_TXT).toFile()); - default: - throw new IllegalStateException("Bug!!! Missing case"); - } + return createProject(type, testProjectPath); }); } protected Path getProjectPath(String name) throws URISyntaxException, IOException { return getProjectPathFromClasspath(name); } - + private Path getProjectPathFromClasspath(String name) throws URISyntaxException, IOException { URI resource = ProjectsHarness.class.getResource("/test-projects/" + name).toURI(); return Paths.get(resource); - } - + } + + public MavenJavaProject mavenProject(String name, ProjectCustomizer customizer) throws Exception { + return (MavenJavaProject) project(ProjectType.MAVEN, name, customizer); + } + public MavenJavaProject mavenProject(String name) throws Exception { return (MavenJavaProject) project(ProjectType.MAVEN, name); } diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java index 136ca67ef..eed3af9ea 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootApp.java @@ -188,12 +188,14 @@ public class SpringBootApp { return null; } - public LiveBeansModel getBeans() throws Exception { - String json = getBeansJson(); - if (StringUtil.hasText(json)) { + public LiveBeansModel getBeans() { + try { + String json = getBeansJson(); return LiveBeansModel.parse(json); + } catch (Exception e) { + Log.log(e); + return LiveBeansModel.builder().build(); } - return null; } public String getRequestMappings() throws Exception { diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java index 2b20ad27a..58e35b18c 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBean.java @@ -10,8 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.boot.app.cli.livebean; -import org.json.JSONArray; -import org.json.JSONObject; +import java.util.Arrays; /** * @author Martin Lippert @@ -26,6 +25,34 @@ public class LiveBean { private final String resource; private final String[] dependencies; + public static class Builder { + private String id; + private String[] aliases = {}; + private String scope; + private String type; + private String resource; + private String[] dependencies = {}; + + public LiveBean build() { + return new LiveBean(id, aliases, scope, type, resource, dependencies); + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder dependencies(String... deps) { + this.dependencies = deps; + return this; + } + } + protected LiveBean(String id, String[] aliases, String scope, String type, String resource, String[] dependencies) { super(); this.id = id; @@ -60,4 +87,13 @@ public class LiveBean { return dependencies; } + @Override + public String toString() { + return "LiveBean [id=" + id + ", type=" + type + ", dependencies=" + Arrays.toString(dependencies) + "]"; + } + + public static Builder builder() { + return new Builder(); + } + } diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBeansModel.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBeansModel.java index 5b7e62fd8..d1a8be153 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBeansModel.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/livebean/LiveBeansModel.java @@ -40,7 +40,7 @@ public class LiveBeansModel { return new LiveBeansModel(beansViaName.build(), beansViaType.build(), beansViaDependency.build()); } - public void add(LiveBean bean) { + public Builder add(LiveBean bean) { String type = bean.getType(); if (type != null) { beansViaType.put(type, bean); @@ -57,6 +57,7 @@ public class LiveBeansModel { beansViaDependency.put(dep, bean); } } + return this; } }