From b41cd4274183b68e104e8735fc0b3abad459c865 Mon Sep 17 00:00:00 2001 From: nsingh Date: Thu, 8 Nov 2018 16:10:11 -0800 Subject: [PATCH] PT 161583390 - Initial implementation to support context path Added support for context path for boot 1.x and 2.x when property defined via command line args. Also added some relevant junits for request mappings with context paths. --- .../boot/app/cli/AbstractSpringBootApp.java | 18 +++ .../commons/boot/app/cli/ContextPath.java | 107 ++++++++++++++++++ .../commons/boot/app/cli/SpringBootApp.java | 1 + .../LiveAppURLSymbolProvider.java | 3 +- .../RequestMappingHoverProvider.java | 7 +- .../boot/java/requestmapping/UrlUtil.java | 13 ++- .../test/RequestMappingLiveHoverTest.java | 81 +++++++++++++ .../harness/MockRunningAppProvider.java | 5 + 8 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/ContextPath.java 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;