diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java index e0b1e5adc..cd345e60f 100644 --- a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/AbstractSpringBootApp.java @@ -394,6 +394,24 @@ public abstract class AbstractSpringBootApp implements SpringBootApp { return getLiveConditionals(getAutoConfigReport(), getProcessID(), getProcessName()); } + @Override + public String getContextPath() throws Exception { + String environment = getEnvironment(); + String bootVersion = null; + // Boot 1.x + Object result = getActuatorDataFromAttribute(getObjectName("type=Endpoint,name=requestMappingEndpoint"), "Data"); + if (result != null) { + bootVersion = "1.x"; + } + + // Boot 2.x + result = getActuatorDataFromOperation(getObjectName("type=Endpoint,name=Mappings"), "mappings"); + if (result != null) { + bootVersion = "2.x"; + } + return bootVersion != null && environment != null ? ContextPath.getContextPath(bootVersion, environment) : null; + } + /** * Publicly visible so that it can be tested via a mock app * diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/ContextPath.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/ContextPath.java new file mode 100644 index 000000000..a3a1f6865 --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/ContextPath.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2018 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.commons.boot.app.cli; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.util.StringUtil; + +public class ContextPath { + + protected static Logger logger = LoggerFactory.getLogger(ContextPath.class); + + + public static final String[] BOOT_1X_CONTEXTPATH = { "server.context-path", "server.contextPath", "SERVER_CONTEXT_PATH" }; + public static final String[] BOOT_2X_CONTEXTPATH = { "server.servlet.context-path", "server.servlet.contextPath", "SERVER_SERVLET_CONTEXT_PATH" }; + + public static String getContextPath(String bootVersion, String environment) { + + if (environment != null) { + JSONObject env = new JSONObject(environment); + + String[] contextPathProperties = null; + if ("1.x".equals(bootVersion)) { + contextPathProperties = BOOT_1X_CONTEXTPATH; + } else if ("2.x".equals(bootVersion)) { + contextPathProperties = BOOT_2X_CONTEXTPATH; + } + + if (contextPathProperties != null) { + for (String prop : contextPathProperties) { + String contextPath = findContextPath(env, prop); + if (StringUtil.hasText(contextPath)) { + return contextPath; + } + } + } + } + + return null; + } + + private static String findContextPath(JSONObject env, String contextPathProp) { + String contextPath = null; + if (env != null) { + contextPath = findInCommandLineArgs(env, contextPathProp); + if (contextPath == null) { + contextPath = findInApplicationConfig(env, contextPathProp); + } + } + return contextPath; + } + + private static String findInApplicationConfig(JSONObject env, String contextPathProp) { + // TODO Auto-generated method stub + return null; + + } + + protected static String findInCommandLineArgs(JSONObject env, String contextPathProp) { + // boot 1.x + JSONObject commandLineArgs = env.optJSONObject("commandLineArgs"); + if (commandLineArgs != null) { + String contextPathValue = commandLineArgs.optString(contextPathProp); + // Warning: fetching value above may return empty string, so null check on the value is not enough + if (StringUtil.hasText(contextPathValue)) { + return contextPathValue; + } + } + // boot 2.x + if (commandLineArgs == null) { + //Not found as direct property value... in Boot 2.0 we must look inside the 'propertySources'. + //Similar... but structure is more complex. + JSONArray propertySources = env.optJSONArray("propertySources"); + if (propertySources!=null) { + for (Object _source : propertySources) { + if (_source instanceof JSONObject) { + JSONObject source = (JSONObject) _source; + String sourceName = source.optString("name"); + if ("commandLineArgs".equals(sourceName)) { + JSONObject props = source.optJSONObject("properties"); + // Find the contextPathProp in the command line args + JSONObject valueObject = props.optJSONObject(contextPathProp); + if (valueObject!=null) { + String contextPathValue = valueObject.optString("value"); + if (StringUtil.hasText(contextPathValue)) { + return contextPathValue; + } + } + } + } + } + } + } + return null; + } + +} 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 0be1646f3..ec2681892 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 @@ -28,6 +28,7 @@ public interface SpringBootApp extends Disposable { String getProcessID(); String getHost() throws Exception; String getPort() throws Exception; + String getContextPath() throws Exception; boolean isSpringBootApp(); String getEnvironment() throws Exception; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java index 3ccf47c09..072e31ca0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java @@ -46,9 +46,10 @@ public class LiveAppURLSymbolProvider { try { String host = app.getHost(); String port = app.getPort(); + String contextPath = app.getContextPath(); Stream urls = app.getRequestMappings().stream() .flatMap(rm -> Arrays.stream(rm.getSplitPath())) - .map(path -> UrlUtil.createUrl(host, port, path)); + .map(path -> UrlUtil.createUrl(host, port, path, contextPath)); urls.forEach(url -> result.add(new SymbolInformation(url, SymbolKind.Method, new Location(url, new Range(new Position(0, 0), new Position(0, 1)))))); } catch (Exception e) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java index ac1283ebd..e13401027 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java @@ -179,6 +179,8 @@ public class RequestMappingHoverProvider implements HoverProvider { List urls = new ArrayList<>(); for (int i = 0; i < mappingMethods.size(); i++) { Tuple2 mappingMethod = mappingMethods.get(i); + SpringBootApp app = mappingMethod.getT2(); + String contextPath = app.getContextPath(); String port = mappingMethod.getT2().getPort(); String host = mappingMethod.getT2().getHost(); @@ -192,7 +194,7 @@ public class RequestMappingHoverProvider implements HoverProvider { paths = new String[] {""}; } for (String path : paths) { - String url = UrlUtil.createUrl(host, port, path); + String url = UrlUtil.createUrl(host, port, path, contextPath); urls.add(url); } } @@ -215,8 +217,9 @@ public class RequestMappingHoverProvider implements HoverProvider { //So we'll pretend this is the same as path="" as that gives a working link. paths = new String[] {""}; } + String contextPath = app.getContextPath(); List renderableUrls = Arrays.stream(paths).flatMap(path -> { - String url = UrlUtil.createUrl(host, port, path); + String url = UrlUtil.createUrl(host, port, path, contextPath); return Stream.of(Renderables.link(url, url), Renderables.lineBreak()); }) .collect(Collectors.toList()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java index 25e9e739d..f7b3d894c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Pivotal, Inc. + * Copyright (c) 2017, 2018 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 @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.requestmapping; +import org.springframework.ide.vscode.commons.util.StringUtil; + public class UrlUtil { /** @@ -17,9 +19,10 @@ public class UrlUtil { * @param host * @param port * @param path + * @param contextPath * @return the resultant URL */ - public static String createUrl(String host, String port, String path) { + public static String createUrl(String host, String port, String path, String contextPath) { if (path==null) { path = ""; } @@ -28,6 +31,12 @@ public class UrlUtil { if (!path.startsWith("/")) { path = "/" +path; } + if (StringUtil.hasText(contextPath)) { + if (!contextPath.startsWith("/")) { + contextPath = "/" + contextPath; + } + path = contextPath + path; + } if (port.equals("80")) { return "http://"+host+path; } else { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java index f28f7cc06..9e57f4ec4 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java @@ -326,6 +326,87 @@ public class RequestMappingLiveHoverTest { editor.assertNoHover("@PutMapping(\"/greetings\")"); } + @Test + public void testLiveHoverHintWithContextPath() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = directory.toPath().resolve("src/main/java/example/HelloWorldController.java").toUri() + .toString(); + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("1111") + .processId("22022") + .host("cfapps.io") + .contextPath("/adifferentpath") + .processName("test-request-mapping-live-hover") + // Ugly, but this is real JSON copied from a real live running app. We want the + // mock app to return realistic results if possible + .requestMappingsJson( + "{\"/webjars/**\":{\"bean\":\"resourceHandlerMapping\"},\"/**\":{\"bean\":\"resourceHandlerMapping\"},\"/**/favicon.ico\":{\"bean\":\"faviconHandlerMapping\"},\"{[/hello-world],methods=[GET]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public example.Greeting example.HelloWorldController.sayHello(java.lang.String)\"},\"{[/goodbye]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.goodbye()\"},\"{[/hello]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.hello()\"},\"{[/error]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)\"},\"{[/error],produces=[text/html]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)\"}}") + .build(); + + harness.intialize(directory); + + Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); + editor.assertHighlights("@RequestMapping(method=RequestMethod.GET)"); + editor.assertHoverContains("@RequestMapping(method=RequestMethod.GET)", "[http://cfapps.io:1111/adifferentpath/hello-world](http://cfapps.io:1111/adifferentpath/hello-world) \n" + + "\n" + + "Process [PID=22022, name=`test-request-mapping-live-hover`]"); + + } + + @Test + public void testMultiPathMappingWithContextPath() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = directory.toPath().resolve("src/main/java/example/RestApi.java").toUri() + .toString(); + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .contextPath("/differentPaath") + .processName("test-request-mapping-live-hover") + // Ugly, but this is real JSON copied from a real live running app. We want the + // mock app to return realistic results if possible + .requestMappingsJson( + "{\"{[/greetings || /hello],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.String com.example.RestApi.greetings()\"}}") + . build(); + + harness.intialize(directory); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.stereotype.Controller;\n" + + "import org.springframework.web.bind.annotation.RequestMapping;\n" + + "import org.springframework.web.bind.annotation.RequestMethod.*;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@RequestMapping(value={\"/greetings\", \"/hello\"}, method=GET)\n" + + "public String greetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value={\"/greetings\", \"/hello\"}, method=GET)", "[http://cfapps.io:999/differentPaath/greetings](http://cfapps.io:999/differentPaath/greetings) \n" + + "[http://cfapps.io:999/differentPaath/hello](http://cfapps.io:999/differentPaath/hello) \n" + + "\n" + + "Process [PID=76543, name=`test-request-mapping-live-hover`]"); + + } + @Test public void testMultiPathMappingHoverHintMethod1() throws Exception { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java index 94911616d..3e6681ff8 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/MockRunningAppProvider.java @@ -92,6 +92,11 @@ public class MockRunningAppProvider { return this; } + public MockAppBuilder contextPath(String contextPath) throws Exception { + when(app.getContextPath()).thenReturn(contextPath); + return this; + } + public MockAppBuilder port(String port) throws Exception { when(app.getPort()).thenReturn(port); return this;