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 148606493..ef3ad9629 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.profiles.ActiveProfilesProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingSymbolProvider; import org.springframework.ide.vscode.boot.java.scope.ScopeCompletionProcessor; @@ -256,6 +257,7 @@ public class BootJavaLanguageServer extends SimpleLanguageServer { providers.put(org.springframework.ide.vscode.boot.java.requestmapping.Constants.SPRING_PUT_MAPPING, new RequestMappingHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.requestmapping.Constants.SPRING_DELETE_MAPPING, new RequestMappingHoverProvider()); providers.put(org.springframework.ide.vscode.boot.java.requestmapping.Constants.SPRING_PATCH_MAPPING, new RequestMappingHoverProvider()); + 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()); diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/profiles/ActiveProfilesProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/profiles/ActiveProfilesProvider.java new file mode 100644 index 000000000..7264b0318 --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/profiles/ActiveProfilesProvider.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * 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.profiles; + +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.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.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.Log; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.google.common.collect.ImmutableList; + +/** + * @author Kris De Volder + */ +public class ActiveProfilesProvider implements HoverProvider { + + public static final String ANNOTATION = "org.springframework.context.annotation.Profile"; + + @Override + public CompletableFuture provideHover( + ASTNode node, + Annotation annotation, + ITypeBinding type, + int offset, + TextDocument doc, SpringBootApp[] runningApps + ) { + if (runningApps.length>0) { + StringBuilder markdown = new StringBuilder(); + markdown.append("**Active Profiles**\n\n"); + for (SpringBootApp app : runningApps) { + List profiles = app.getActiveProfiles(); + if (profiles==null) { + markdown.append(niceAppName(app)+" : _Unknown_\n\n"); + } else if (profiles.isEmpty()) { + markdown.append(niceAppName(app)+" : _None_\n\n"); + } else { + markdown.append(niceAppName(app)+" :\n"); + for (String profile : profiles) { + markdown.append("- "+profile+"\n"); + } + markdown.append("\n"); + } + } + return CompletableFuture.completedFuture(new Hover( + ImmutableList.of(Either.forLeft(markdown.toString())) + )); + } + return null; + } + + private String niceAppName(SpringBootApp app) { + return "Process [PID="+app.getProcessID()+", name=`"+app.getProcessName()+"`]"; + } + + @Override + public Range getLiveHoverHint(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { + try { + if (runningApps.length > 0) { + return doc.toRange(annotation.getStartPosition(), annotation.getLength()); + } + } catch (BadLocationException e) { + Log.log(e); + } + return null; + } +} 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/profile/ActiveProfilesHoverTest.java new file mode 100644 index 000000000..bdad0d330 --- /dev/null +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/profile/ActiveProfilesHoverTest.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * 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.profile; + +import org.junit.Before; +import org.junit.Test; +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; + +public class ActiveProfilesHoverTest { + + 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) + .build(); + harness.useProject(projects.mavenProject("empty-boot-15-web-app")); + harness.intialize(null); + } + + @Test + public void testActiveProfileHover() throws Exception { + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("22022") + .processName("foo.bar.RunningApp") + .profiles("testing-profile", "local-profile") + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "@Profile(\"local\")\n" + + "public class LocalConfig {\n" + + "}" + ); + editor.assertHoverContains("@Profile", "testing-profile"); + editor.assertHoverContains("@Profile", "local-profile"); + editor.assertHoverContains("@Profile", "foo.bar.RunningApp"); + editor.assertHoverContains("@Profile", "22022"); + } + + + @Test + public void testActiveProfileHover_Unknown() throws Exception { + //Sometimes its not possible to determine active profiles for an app (e.g. no actuator dependency). + //Make sure we show something sensible + + mockAppProvider.builder() + .isSpringBootApp(true) + .processId("22022") + .processName("foo.bar.RunningApp") + .profilesUnknown() + .build(); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "@Profile(\"local\")\n" + + "public class LocalConfig {\n" + + "\n" + + "}" + ); + editor.assertHoverContains("@Profile", "Process [PID=22022, name=`foo.bar.RunningApp`] : _Unknown_"); + } + + @Test + public void testNoRunningApps() throws Exception { + Editor editor = harness.newEditor(LanguageId.JAVA, + "package hello;\n" + + "\n" + + "import org.springframework.context.annotation.Configuration;\n" + + "import org.springframework.context.annotation.Profile;\n" + + "\n" + + "@Configuration\n" + + "@Profile(\"local\")\n" + + "public class LocalConfig {\n" + + "\n" + + "}" + ); + editor.assertNoHover("@Profile"); + } +} 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 82efa544f..048afeaeb 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 @@ -20,6 +20,9 @@ import org.mockito.Mockito; import org.springframework.ide.vscode.boot.java.handlers.RunningAppProvider; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.util.ExceptionUtil; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness.Builder; + +import com.google.common.collect.ImmutableList; public class MockRunningAppProvider { @@ -103,6 +106,20 @@ public class MockRunningAppProvider { return this; } + public MockAppBuilder profiles(String... names) { + when(app.getActiveProfiles()).thenReturn(ImmutableList.copyOf(names)); + return this; + } + + public MockAppBuilder profilesUnknown() { + //Note, technically, we don't have to program the mock for this case as it will return + // null by default. But it makes test code more readable. Also... how we represent the + // 'unknown' case may change in the future and having this method will help fix the tests. + when(app.getActiveProfiles()).thenReturn(null); + return this; + } + + /** * Builds the mock app and adds it to the app provider */ 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 68eb6ef3f..b1cc28225 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 @@ -12,7 +12,6 @@ package org.springframework.ide.vscode.commons.boot.app.cli; import java.io.File; import java.io.IOException; -import java.io.PrintStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -30,11 +29,11 @@ import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import org.springframework.ide.vscode.commons.util.Log; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; @@ -101,16 +100,17 @@ public class SpringBootApp { } public boolean isSpringBootApp() throws Exception { - return (isSpringBootAppClasspath() || isSpringBootAppSysprops()); + return !containsSystemProperty("sts4.languageserver.name") + && ( + isSpringBootAppClasspath() || + isSpringBootAppSysprops() + ); } private boolean isSpringBootAppSysprops() { try { Properties sysprops = this.vm.getSystemProperties(); - return sysprops.getProperty("sts4.languageserver.name") == null - // Note: java.protocol.handler.pkgs may be not be in system properties, and result in NPE (at least in Mac OS) - // To avoid NPE, just reversed the equality check - && "org.springframework.boot.loader".equals(sysprops.getProperty("java.protocol.handler.pkgs")); + return "org.springframework.boot.loader".equals(sysprops.getProperty("java.protocol.handler.pkgs")); } catch (Exception e) { Log.log(e); } @@ -373,7 +373,7 @@ public class SpringBootApp { @Override public String toString() { - return "SpringBootApp [" +vmd.id() + ", "+vmd.displayName()+"]"; + return "Process [id=" +getProcessID() + ", name=`"+getProcessName()+"`]"; } /** @@ -397,5 +397,32 @@ public class SpringBootApp { System.out.println("}"); } + public List getActiveProfiles() { + try { + String _env = getEnvironment(); + if (_env != null) { + JSONObject env = new JSONObject(_env); + Object _profiles = env.opt("activeProfiles"); //Boot 2.0 + if (_profiles==null) { + _profiles = env.opt("profiles"); //Boot 1.5 + } + if (_profiles instanceof JSONArray) { + @SuppressWarnings("unchecked") + JSONArray profiles = (JSONArray) _profiles; + ImmutableList.Builder list = ImmutableList.builder(); + for (Object object : profiles) { + if (object instanceof String) { + list.add((String) object); + } + } + return list.build(); + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + } diff --git a/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java index 3f2317e2f..3247697de 100644 --- a/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java +++ b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/SpringBootAppTest.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.boot.app.cli; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -17,19 +18,21 @@ import static org.junit.Assert.assertTrue; import java.io.File; import java.net.URL; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.Optional; import org.json.JSONObject; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.springframework.ide.vscode.commons.util.AsyncProcess; import org.springframework.ide.vscode.commons.util.ExternalCommand; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.test.ACondition; +import com.google.common.collect.ImmutableList; + public class SpringBootAppTest { // private static final String appName = "actuator-client-15-test-subject"; // Boot 1.5 test app @@ -39,6 +42,8 @@ public class SpringBootAppTest { private static final Duration TIMEOUT = Duration.ofSeconds(30); // in CI build starting the app takes longer than 10s sometimes. //Output from CI build: Started ActuatorClientTestSubjectApplication in 22.962 seconds (JVM running for 26.028) + private static final List TEST_PROFILES = ImmutableList.of("testing", "funny", "cameleon"); + private static AsyncProcess testAppRunner; private static SpringBootApp testApp; @@ -59,7 +64,8 @@ public class SpringBootAppTest { "java", "-Dserver.port=0", //let spring boot pick randomized free port "-jar", - jarFile.getAbsolutePath() + jarFile.getAbsolutePath(), + "--spring.profiles.active="+StringUtil.collectionToCommaDelimitedString(TEST_PROFILES) ), false ); @@ -117,7 +123,7 @@ public class SpringBootAppTest { ACondition.waitFor(TIMEOUT, () -> { String env = testApp.getEnvironment(); assertNonEmptyJsonObject(env); -// System.out.println("env = "+env); + System.out.println("env = "+new JSONObject(env).toString(3)); }); } @@ -148,6 +154,14 @@ public class SpringBootAppTest { }); } + @Test + public void getProfiles() throws Exception { + ACondition.waitFor(TIMEOUT, () -> { + List result = testApp.getActiveProfiles(); + assertEquals(ImmutableList.copyOf(TEST_PROFILES), result); + }); + } + private void assertNonEmptyJsonObject(String jsonData) { JSONObject parsed = new JSONObject(jsonData); assertFalse(parsed.keySet().isEmpty());