diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java index fd14519a8..f0e1efba4 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/LiveAppURLSymbolProvider.java @@ -11,15 +11,15 @@ package org.springframework.ide.vscode.boot.java.requestmapping; import java.util.ArrayList; -import java.util.Iterator; +import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.SymbolKind; -import org.json.JSONObject; 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.Log; @@ -44,7 +44,12 @@ public class LiveAppURLSymbolProvider { SpringBootApp[] runningApps = runningAppProvider.getAllRunningSpringApps().toArray(new SpringBootApp[0]); for (SpringBootApp app : runningApps) { try { - collectLiveAppSymbols(result, app); + String host = app.getHost(); + String port = app.getPort(); + Stream urls = app.getRequestMappings().stream() + .flatMap(rm -> Arrays.stream(rm.getSplitPath())) + .map(path -> UrlUtil.createUrl(host, port, path)); + 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) { Log.log(e); @@ -57,21 +62,4 @@ public class LiveAppURLSymbolProvider { return result; } - private void collectLiveAppSymbols(List result, SpringBootApp app) throws Exception { - String mappings = app.getRequestMappings(); - JSONObject requestMappings = new JSONObject(mappings); - Iterator keys = requestMappings.keys(); - while (keys.hasNext()) { - String key = keys.next(); - String extractedPath = UrlUtil.extractPath(key); - if (extractedPath != null) { - String[] splitPath = UrlUtil.splitPath(extractedPath); - for (String path : splitPath) { - String url = UrlUtil.createUrl(app.getHost(), app.getPort(), path); - result.add(new SymbolInformation(url, SymbolKind.Method, new Location(url, new Range(new Position(0, 0), new Position(0, 1))))); - } - } - } - } - } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java index c870a55a3..d39e0baff 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java @@ -11,20 +11,27 @@ package org.springframework.ide.vscode.boot.java.requestmapping; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; -import java.util.Iterator; +import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ArrayInitializer; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.jdt.core.dom.TypeDeclaration; @@ -32,17 +39,18 @@ import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.MarkedString; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.json.JSONObject; 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.java.parser.JLRMethodParser; -import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser.JLRMethod; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; 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; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + /** * @author Martin Lippert */ @@ -58,7 +66,7 @@ public class RequestMappingHoverProvider implements HoverProvider { public Collection getLiveHoverHints(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { try { if (runningApps.length > 0) { - Optional val = getRequestMappingMethodFromRunningApp(annotation, runningApps); + Optional> val = getRequestMappingMethodFromRunningApp(annotation, runningApps); if (val.isPresent()) { Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); return ImmutableList.of(hoverRange); @@ -77,7 +85,7 @@ public class RequestMappingHoverProvider implements HoverProvider { try { List> hoverContent = new ArrayList<>(); - Optional val = getRequestMappingMethodFromRunningApp(annotation, runningApps); + Optional> val = getRequestMappingMethodFromRunningApp(annotation, runningApps); if (val.isPresent()) { addHoverContent(val.get(), hoverContent); @@ -97,25 +105,18 @@ public class RequestMappingHoverProvider implements HoverProvider { return null; } - private Optional getRequestMappingMethodFromRunningApp(Annotation annotation, + private Optional> getRequestMappingMethodFromRunningApp(Annotation annotation, SpringBootApp[] runningApps) { try { for (SpringBootApp app : runningApps) { - String mappings = app.getRequestMappings(); - if (mappings!=null) { - JSONObject requestMappings = new JSONObject(mappings); - String rawPath = getRawPath(annotation, requestMappings); - if (rawPath != null) { - String path = UrlUtil.extractPath(rawPath); - if (path != null) { - String rawMethod = getRawMethod(annotation, requestMappings); - JLRMethod parsedMethod = JLRMethodParser.parse(rawMethod); - if (methodMatchesAnnotation(annotation, parsedMethod)) { - return Optional.of(new RequestMappingMethod(path, parsedMethod, app)); - } - } - } + Collection mappings = app.getRequestMappings(); + if (mappings!=null && !mappings.isEmpty()) { + return mappings.stream() + .filter(rm -> matchesAnnotation(annotation, rm)) + .filter(rm -> methodMatchesAnnotation(annotation, rm)) + .map(rm -> Tuples.of(rm, app)) + .findFirst(); } } } catch (Exception e) { @@ -124,16 +125,19 @@ public class RequestMappingHoverProvider implements HoverProvider { return Optional.empty(); } - private boolean methodMatchesAnnotation(Annotation annotation, JLRMethod requestMappingMethod) { - String rqClassName = requestMappingMethod.getFQClassName(); - String rqMethod = requestMappingMethod.getMethodName(); + private boolean methodMatchesAnnotation(Annotation annotation, RequestMapping rm) { + String rqClassName = rm.getFullyQualifiedClassName(); ASTNode parent = annotation.getParent(); if (parent instanceof MethodDeclaration) { MethodDeclaration methodDec = (MethodDeclaration) parent; IMethodBinding binding = methodDec.resolveBinding(); - return binding.getDeclaringClass().getQualifiedName().equals(rqClassName) && - binding.getName().equals(rqMethod); + return binding.getDeclaringClass().getQualifiedName().equals(rqClassName) + && binding.getName().equals(rm.getMethodName()) + && Arrays.equals(Arrays.stream(binding.getParameterTypes()) + .map(t -> t.getQualifiedName()) + .toArray(String[]::new), + rm.getMethodParameters()); } else if (parent instanceof TypeDeclaration) { TypeDeclaration typeDec = (TypeDeclaration) parent; return typeDec.resolveBinding().getQualifiedName().equals(rqClassName); @@ -141,113 +145,156 @@ public class RequestMappingHoverProvider implements HoverProvider { return false; } - private void addHoverContent(RequestMappingMethod mappingMethod, List> hoverContent) throws Exception { - String processId = mappingMethod.app.getProcessID(); - String processName = mappingMethod.app.getProcessName(); - String path = mappingMethod.requestMappingPath; - + private void addHoverContent(Tuple2 mappingMethod, List> hoverContent) throws Exception { + String processId = mappingMethod.getT2().getProcessID(); + String processName = mappingMethod.getT2().getProcessName(); + String port = mappingMethod.getT2().getPort(); + String host = mappingMethod.getT2().getHost(); StringBuilder builder = new StringBuilder(); - String port = mappingMethod.app.getPort(); - String host = mappingMethod.app.getHost(); - String url = UrlUtil.createUrl(host, port, path); + Arrays.stream(mappingMethod.getT1().getSplitPath()).forEach(path -> { - builder.append("Path: "); - builder.append("["); - builder.append(path); - builder.append("]"); - builder.append("("); - builder.append(url); - builder.append(")"); + String url = UrlUtil.createUrl(host, port, path); + + if (builder.length() > 0) { + builder.append("\n"); + } + builder.append("Path: "); + builder.append("["); + builder.append(path); + builder.append("]"); + builder.append("("); + builder.append(url); + builder.append(")"); + + }); hoverContent.add(Either.forLeft(builder.toString())); hoverContent.add(Either.forLeft("Process ID: " + processId)); hoverContent.add(Either.forLeft("Process Name: " + processName)); } - private String getRawMethod(Annotation annotation, JSONObject mappings) { - Iterator keys = mappings.keys(); - while (keys.hasNext()) { - String key = keys.next(); - if (matchesAnnotation(annotation, key)) { - Object ob= mappings.get(key); - if (ob instanceof JSONObject) { - JSONObject methodMap = (JSONObject) ob; - return methodMap.getString("method"); - } + protected String getRequestMethod(SingleMemberAnnotation annotation) { + ITypeBinding type = annotation.resolveTypeBinding(); + if (type != null) { + switch (type.getQualifiedName()) { + case Constants.SPRING_GET_MAPPING: + return "GET"; + case Constants.SPRING_POST_MAPPING: + return "POST"; + case Constants.SPRING_DELETE_MAPPING: + return "DELETE"; + case Constants.SPRING_PUT_MAPPING: + return "PUT"; + case Constants.SPRING_PATCH_MAPPING: + return "PATCH"; } } return null; } - private String getRawPath(Annotation annotation, JSONObject mappings) { - Iterator keys = mappings.keys(); - while (keys.hasNext()) { - String key = keys.next(); - if (matchesAnnotation(annotation, key)) { - return key; - } - } - return null; - } - - private boolean matchesAnnotation(Annotation annotation, String jsonKey) { - String mappingPath = null; + private boolean matchesAnnotation(Annotation annotation, RequestMapping rm) { + String[] mappingPath = null; + Set methods = null; if (annotation instanceof SingleMemberAnnotation) { - Expression valueContent = ((SingleMemberAnnotation) annotation).getValue(); + SingleMemberAnnotation singleAnnotation = (SingleMemberAnnotation) annotation; + Expression valueContent = singleAnnotation.getValue(); if (valueContent instanceof StringLiteral) { - mappingPath = ((StringLiteral)valueContent).getLiteralValue(); + mappingPath = new String[] { ((StringLiteral)valueContent).getLiteralValue() }; + } + String method = getRequestMethod(singleAnnotation); + if (method != null) { + methods = new HashSet<>(); + methods.add(method); } } else if (annotation instanceof NormalAnnotation) { List values = ((NormalAnnotation) annotation).values(); for (Object value : values) { if (value instanceof MemberValuePair) { - String name = ((MemberValuePair)value).getName().toString(); - if (name != null && name.equals("value")) { - Expression valueContent = ((MemberValuePair)value).getValue(); - if (valueContent instanceof StringLiteral) { - mappingPath = ((StringLiteral)valueContent).getLiteralValue(); - } + MemberValuePair pair = (MemberValuePair)value; + String name = pair.getName().toString(); + switch (name) { + case "value": + case "path": + mappingPath = getPaths(pair.getValue()); + break; + case "method": + methods = getRequestMethod(pair.getValue()); + break; } } } } - return mappingPath != null ? jsonKey.contains(mappingPath) : false; + if (mappingPath != null) { + if (Arrays.equals(mappingPath, rm.getSplitPath())) { + if (methods == null || methods.isEmpty()) { + return true; + } else { + return methods.equals(rm.getRequestMethods()); + } + } + } + return false; + } - public JSONObject[] getRequestMappingsFromProcesses(SpringBootApp[] runningApps) { - List result = new ArrayList<>(); + private static String getExpressionValueAsString(Expression exp) { + if (exp instanceof StringLiteral) { + return ((StringLiteral)exp).getLiteralValue(); + } else if (exp instanceof QualifiedName) { + return getExpressionValueAsString(((QualifiedName)exp).getName()); + } else if (exp instanceof SimpleName) { + return ((SimpleName)exp).getIdentifier(); + } else { + return null; + } + } - try { - for (SpringBootApp app : runningApps) { - String mappings = app.getRequestMappings(); - if (mappings != null) { - JSONObject requestMappings = new JSONObject(mappings); - if (requestMappings != null) { - result.add(requestMappings); - } - } + @SuppressWarnings("unchecked") + private static String[] getPaths(Expression exp) { + if (exp instanceof ArrayInitializer) { + ArrayInitializer array = (ArrayInitializer) exp; + return ((List)array.expressions()).stream() + .map(e -> getExpressionValueAsString(e)) + .filter(Objects::nonNull) + .toArray(String[]::new); + } else { + String rm = getExpressionValueAsString(exp); + if (rm != null) { + return new String[] { rm }; } } - catch (Exception e) { - Log.log(e); - } + return null; + } - return result.toArray(new JSONObject[result.size()]); + @SuppressWarnings("unchecked") + private static Set getRequestMethod(Expression exp) { + if (exp instanceof ArrayInitializer) { + ArrayInitializer array = (ArrayInitializer) exp; + return ((List)array.expressions()).stream() + .map(e -> getExpressionValueAsString(e)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } else { + String rm = getExpressionValueAsString(exp); + if (rm != null) { + HashSet methods = new HashSet<>(); + methods.add(rm); + } + } + return null; } static class RequestMappingMethod { public final SpringBootApp app; - public final String requestMappingPath; - public final JLRMethod requestMappingMethod; + public final RequestMapping requestMapping; - public RequestMappingMethod(String requestMappingPath, JLRMethod requestMappingMethod, SpringBootApp app) { - this.requestMappingPath = requestMappingPath; - this.requestMappingMethod = requestMappingMethod; + public RequestMappingMethod(RequestMapping requestMapping, SpringBootApp app) { + this.requestMapping = requestMapping; this.app = app; } } diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java index e56501510..f961f10f7 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/UrlUtil.java @@ -10,41 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.requestmapping; -import java.util.ArrayList; -import java.util.List; -import java.util.StringTokenizer; -import java.util.regex.Pattern; -import java.util.stream.Stream; - public class UrlUtil { - - public static Stream processOrPaths(String pathExp) { - if (pathExp.contains("||")) { - String[] paths = pathExp.split(Pattern.quote("||")); - return Stream.of(paths).map(String::trim); - } else { - return Stream.of(pathExp); - } - } - - - public static String extractPath(String key) { - if (key.startsWith("{[")) { //Case 2 (see above) - //An almost json string. Unfortunately not really json so we can't - //use org.json or jackson Mapper to properly parse this. - int start = 2; //right after first '[' - int end = key.indexOf(']'); - if (end>=2) { - return key.substring(start, end); - } - } - //Case 1, or some unanticipated stuff. - //Assume the key is the path, which is right for Case 1 - // and probably more useful than null for 'unanticipated stuff'. - return key; - } - /** * Creates http URL string based on host, port and path * @param host @@ -67,32 +34,4 @@ public class UrlUtil { return null; } - - public static String[] splitPath(String path) { - if (path.contains("||")) { - List result = new ArrayList<>(); - - String basePath = path.substring(0, path.indexOf("||")).trim(); - result.add(basePath); - - if (basePath.lastIndexOf('/') > 0) { - basePath = basePath.substring(0, basePath.lastIndexOf('/')); - } - - String additionalPaths = path.substring(path.indexOf("||")); - StringTokenizer tokenizer = new StringTokenizer(additionalPaths, "||"); - while (tokenizer.hasMoreTokens()) { - String token = tokenizer.nextToken().trim(); - if (token.length() > 0) { - result.add(basePath + "/" + token); - } - } - - return result.toArray(new String[result.size()]); - } - else { - return new String[] {path}; - } - } - } diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java index 0fd57e088..8ab9ea978 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java @@ -132,4 +132,802 @@ public class RequestMappingLiveHoverTest { } + @Test + public void testDeleteMappingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[DELETE]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public void com.example.RestApi.deleteGreetings()\"}}") + . 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.DeleteMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@DeleteMapping(\"/greetings\")\n" + + "public void deleteGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@DeleteMapping(\"/greetings\")", "Path: [/greetings](http://cfapps.io:999/greetings)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + + @Test + public void testDeleteMappingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.String com.example.RestApi.deleteGreetings()\"}}") + . 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.DeleteMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@DeleteMapping(\"/greetings\")\n" + + "public void deleteGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@DeleteMapping(\"/greetings\")"); + + } + + @Test + public void testGetMappingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],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 java.lang.String;\n" + + "import org.springframework.stereotype.Controller;\n" + + "import org.springframework.web.bind.annotation.GetMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@GetMapping(\"/greetings\")\n" + + "public String greetings() {\n" + + "return \"Greetings!\";\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@GetMapping(\"/greetings\")", "Path: [/greetings](http://cfapps.io:999/greetings)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testGetMappingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[DELETE]}\": {\"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 java.lang.String;\n" + + "import org.springframework.stereotype.Controller;\n" + + "import org.springframework.web.bind.annotation.GetMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@GetMapping(\"/greetings\")\n" + + "public String greetings() {\n" + + "return \"Greetings!\";\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@GetMapping(\"/greetings\")"); + + } + + @Test + public void testPostMappingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[POST]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public void com.example.RestApi.createGreetings()\"}}") + . 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.PostMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@PostMapping(\"/greetings\")\n" + + "public void createGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@PostMapping(\"/greetings\")", "Path: [/greetings](http://cfapps.io:999/greetings)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testPostMappingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.String com.example.RestApi.createGreetings()\"}}") + . 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.PostMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@PostMapping(\"/greetings\")\n" + + "public void createGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@PostMapping(\"/greetings\")"); + + } + + @Test + public void testPutMappingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[PUT]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public void com.example.RestApi.updateGreetings()\"}}") + . 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.PutMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@PutMapping(\"/greetings\")\n" + + "public void updateGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@PutMapping(\"/greetings\")", "Path: [/greetings](http://cfapps.io:999/greetings)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testPutMappingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.String com.example.RestApi.updateGreetings()\"}}") + . 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.PutMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@PutMapping(\"/greetings\")\n" + + "public void updateGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@PutMapping(\"/greetings\")"); + + } + + @Test + public void testPatchMappingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[PATCH]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public void com.example.RestApi.patchGreetings()\"}}") + . 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.PatchMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@PatchMapping(\"/greetings\")\n" + + "public void patchGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@PatchMapping(\"/greetings\")", "Path: [/greetings](http://cfapps.io:999/greetings)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testPatchMappingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.String com.example.RestApi.patchGreetings()\"}}") + . 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.PatchMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@PatchMapping(\"/greetings\")\n" + + "public void patchGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@PatchMapping(\"/greetings\")"); + + } + + @Test + public void testMultiRequestMethodMappingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[POST,PUT]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public void com.example.RestApi.updateGreetings()\"}}") + . 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\", method={POST, PUT})\n" + + "public void updateGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value=\"/greetings\", method={POST, PUT})", "Path: [/greetings](http://cfapps.io:999/greetings)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testMultiRequestMethodMappingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings],methods=[POST,PUT,PATCH]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.String com.example.RestApi.updateGreetings()\"}}") + . 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\", method={POST, PUT})\n" + + "public void updateGreetings() {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@RequestMapping(value=\"/greetings\", method={POST, PUT})"); + + } + + @Test + public void testMultiPathMappingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/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)", "Path: [/greetings](http://cfapps.io:999/greetings)\n" + + "Path: [/hello](http://cfapps.io:999/hello)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testMultiPathMappingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/greetings || /hello || /helloAgain],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.assertNoHover("@RequestMapping(value={\"/greetings\", \"/hello\"}, method=GET)"); + + } + + @Test + public void testMethodMatchingHoverHintMethod1() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/find],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public org.springframework.http.ResponseEntity com.example.RestApi.find(java.lang.String,java.util.Date,java.lang.String)\"}}") + . build(); + + harness.intialize(directory); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.http.ResponseEntity;\n" + + "import java.lang.String;\n" + + "import java.util.Date;\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=\"/find\", method=GET)\n" + + "public ResponseEntity find(String p1, Date p2, String p3) {\n" + + "return null;\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value=\"/find\", method=GET)", "Path: [/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testMethodMatchingHoverHintMethod2() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/find],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.Object com.example.RestApi.set(java.lang.String,java.util.Map)\"}}") + . build(); + + harness.intialize(directory); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.http.ResponseEntity;\n" + + "import java.lang.String;\n" + + "import java.util.Map;\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=\"/find\", method=GET)\n" + + "public Object set(String p1, Map p2) {\n" + + "return null;\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value=\"/find\", method=GET)", "Path: [/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testMethodMatchingHoverHintMethod3() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = "file://" +directory.getAbsolutePath() + "/src/main/java/example/RestApi.java"; + + + // Build a mock running boot app + mockAppProvider.builder() + .isSpringBootApp(true) + .port("999") + .processId("76543") + .host("cfapps.io") + .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 + .getRequestMappings( + "{\"{[/find],methods=[GET]}\": {\"bean\": \"requestMappingHandlerMapping\", \"method\":\"public java.lang.Object com.example.RestApi.set(java.lang.String,java.util.Map>)\"}}") + . build(); + + harness.intialize(directory); + + Editor editor = harness.newEditor(LanguageId.JAVA, + "package com.example;\n" + + "\n" + + "import org.springframework.http.ResponseEntity;\n" + + "import java.lang.String;\n" + + "import java.lang.Integer;\n" + + "import java.util.Map;\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=\"/find\", method=GET)\n" + + "public Object set(String p1, Map> p2) {\n" + + "return null;\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value=\"/find\", method=GET)", "Path: [/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + } 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 048afeaeb..718e6319f 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 @@ -19,8 +19,8 @@ import java.util.Collection; 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.boot.app.cli.requestmappings.RequestMapping; import org.springframework.ide.vscode.commons.util.ExceptionUtil; -import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness.Builder; import com.google.common.collect.ImmutableList; @@ -97,7 +97,8 @@ public class MockRunningAppProvider { } public MockAppBuilder getRequestMappings(String mappings) throws Exception { - when(app.getRequestMappings()).thenReturn(mappings); + Collection requestMappings = SpringBootApp.parseRequestMappingsJson(mappings); + when(app.getRequestMappings()).thenReturn(requestMappings); return this; } diff --git a/headless-services/commons/commons-boot-app-cli/pom.xml b/headless-services/commons/commons-boot-app-cli/pom.xml index 17847e03a..d5c478053 100644 --- a/headless-services/commons/commons-boot-app-cli/pom.xml +++ b/headless-services/commons/commons-boot-app-cli/pom.xml @@ -37,6 +37,11 @@ org.springframework.ide.vscode ${project.version} + + commons-java + org.springframework.ide.vscode + ${project.version} + 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 b1cc28225..d69e5d1d2 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 @@ -13,7 +13,9 @@ package org.springframework.ide.vscode.commons.boot.app.cli; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -30,6 +32,8 @@ import javax.management.remote.JMXServiceURL; import org.json.JSONArray; import org.json.JSONObject; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingImpl1; import org.springframework.ide.vscode.commons.util.Log; import com.fasterxml.jackson.databind.ObjectMapper; @@ -186,17 +190,29 @@ public class SpringBootApp { return null; } - public String getRequestMappings() throws Exception { + public static Collection parseRequestMappingsJson(String json) { + JSONObject obj = new JSONObject(json); + Iterator keys = obj.keys(); + List result = new ArrayList<>(); + while (keys.hasNext()) { + String rawKey = keys.next(); + JSONObject value = obj.getJSONObject(rawKey); + result.add(new RequestMappingImpl1(rawKey, value)); + } + return result; + } + + public Collection getRequestMappings() throws Exception { Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=requestMappingEndpoint", "Data"); if (result != null) { String mappings = new ObjectMapper().writeValueAsString(result); - return mappings; + return parseRequestMappingsJson(mappings); } result = getActuatorDataFromOperation("org.springframework.boot:type=Endpoint,name=Mappings", "mappings"); if (result != null) { String mappings = new ObjectMapper().writeValueAsString(result); - return mappings; + return parseRequestMappingsJson(mappings); } return null; diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMapping.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMapping.java new file mode 100644 index 000000000..41c7c5bd2 --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMapping.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * 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.commons.boot.app.cli.requestmappings; + +import java.util.Set; + +public interface RequestMapping { + String getPath(); + String[] getSplitPath(); + String getFullyQualifiedClassName(); + String getMethodName(); + String[] getMethodParameters(); + String getMethodString(); + Set getRequestMethods(); +} diff --git a/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMappingImpl1.java b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMappingImpl1.java new file mode 100644 index 000000000..d3d7b290d --- /dev/null +++ b/headless-services/commons/commons-boot-app-cli/src/main/java/org/springframework/ide/vscode/commons/boot/app/cli/requestmappings/RequestMappingImpl1.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * 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.commons.boot.app.cli.requestmappings; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.json.JSONObject; +import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser; +import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser.JLRMethod; +import org.springframework.ide.vscode.commons.util.Log; + +import com.google.common.base.Objects; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; + +public class RequestMappingImpl1 implements RequestMapping { + + private static final Pattern REQUEST_METHODS_PATTERN = Pattern.compile(".*methods=\\[(.*)\\].*"); + + /* +There are two styles of entries: + +1) key is a 'path' String. May contain patters like "**" + "/** /favicon.ico":{ + "bean":"faviconHandlerMapping" + } + +2) key is a 'almost json' String + "{[/bye],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}":{ + "bean":"requestMappingHandlerMapping", + "method":"public java.lang.String demo.MyController.bye()" + } + */ + + private JSONObject beanInfo; + private String pathKey; + private Supplier methodDataSupplier; + private Supplier> requestMethodsSupplier; + private Supplier requestPathSupplier; + + public RequestMappingImpl1(String pathKey, JSONObject beanInfo) { + this.pathKey = pathKey; + this.beanInfo = beanInfo; + this.requestMethodsSupplier = Suppliers.memoize(() -> parseRequestMethods()); + this.requestPathSupplier = Suppliers.memoize(() -> parseRequestPath()); + this.methodDataSupplier = Suppliers.memoize(() -> JLRMethodParser.parse(getMethodString())); + } + + @Override + public String getPath() { + return requestPathSupplier.get(); + } + + @Override + public String toString() { + return "RequestMapping("+pathKey+")"; + } + + @Override + public String getFullyQualifiedClassName() { + JLRMethod m = getMethodData(); + if (m!=null) { + return m.getFQClassName(); + } + return null; + } + + @Override + public String getMethodName() { + JLRMethod m = getMethodData(); + if (m!=null) { + return m.getMethodName(); + } + return null; + } + + /** + * Returns the raw string found in the requestmapping info. This is a 'toString' value + * of java.lang.reflect.Method object. + */ + @Override + public String getMethodString() { + try { + if (beanInfo!=null) { + if (beanInfo.has("method")) { + return beanInfo.getString("method"); + } + } + } catch (Exception e) { + Log.log(e); + } + return null; + } + + private JLRMethod getMethodData() { + return methodDataSupplier.get(); + } + + @Override + public int hashCode() { + return pathKey.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RequestMappingImpl1 other = (RequestMappingImpl1) obj; + return Objects.equal(this.pathKey, other.pathKey) + && Objects.equal(this.getMethodString(), other.getMethodString()); + } + + protected Set parseRequestMethods() { + Matcher matcher = REQUEST_METHODS_PATTERN.matcher(pathKey); + if (matcher.matches()) { + return Arrays.stream(matcher.group(1).split("\\s*,\\s*")).collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + + protected String parseRequestPath() { + if (pathKey.startsWith("{[")) { //Case 2 (see above) + //An almost json string. Unfortunately not really json so we can't + //use org.json or jackson Mapper to properly parse this. + int start = 2; //right after first '[' + int end = pathKey.indexOf(']'); + if (end>=2) { + return pathKey.substring(start, end); + } + } + //Case 1, or some unanticipated stuff. + //Assume the key is the path, which is right for Case 1 + // and probably more useful than null for 'unanticipated stuff'. + return pathKey; + } + + @Override + public Set getRequestMethods() { + return requestMethodsSupplier.get(); + } + + @Override + public String[] getSplitPath() { + String paths = requestPathSupplier.get(); + return Arrays.stream(paths.split("\\|\\|")).map(s -> s.trim()).toArray(String[]::new); + } + + @Override + public String[] getMethodParameters() { + return getMethodData().getParameters(); + } + +} \ No newline at end of file diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/UrlUtilTest.java b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/RequestMappingImp1Test.java similarity index 63% rename from headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/UrlUtilTest.java rename to headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/RequestMappingImp1Test.java index 7ddf386f9..803cff043 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/UrlUtilTest.java +++ b/headless-services/commons/commons-boot-app-cli/src/test/java/org/springframework/ide/vscode/commons/boot/app/cli/RequestMappingImp1Test.java @@ -8,38 +8,38 @@ * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.requestmapping.test; +package org.springframework.ide.vscode.commons.boot.app.cli; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import org.junit.Test; -import org.springframework.ide.vscode.boot.java.requestmapping.UrlUtil; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingImpl1; /** * @author Martin Lippert */ -public class UrlUtilTest { +public class RequestMappingImp1Test { @Test public void testSplitPathWithoutDuplicate() { - String path = "/superpath"; - String[] splitPath = UrlUtil.splitPath(path); + RequestMappingImpl1 rm = new RequestMappingImpl1("/superpath", null); + String[] splitPath = rm.getSplitPath(); assertEquals(1, splitPath.length); assertEquals("/superpath", splitPath[0]); } @Test public void testSplitPathSimpleCaseWithEmptyOr() { - String path = "/superpath/mypath || "; - String[] splitPath = UrlUtil.splitPath(path); + RequestMappingImpl1 rm = new RequestMappingImpl1("/superpath/mypath || ", null); + String[] splitPath = rm.getSplitPath(); assertEquals(1, splitPath.length); assertEquals("/superpath/mypath", splitPath[0]); } @Test public void testSplitPathSimpleCase() { - String path = "/superpath/mypath || mypath.json"; - String[] splitPath = UrlUtil.splitPath(path); + RequestMappingImpl1 rm = new RequestMappingImpl1("/superpath/mypath || mypath.json", null); + String[] splitPath = rm.getSplitPath(); assertEquals(2, splitPath.length); assertEquals("/superpath/mypath", splitPath[0]); assertEquals("/superpath/mypath.json", splitPath[1]); @@ -47,8 +47,8 @@ public class UrlUtilTest { @Test public void testSplitPathMultipleCases() { - String path = "/superpath/mypath || mypath.json || somethingelse.what"; - String[] splitPath = UrlUtil.splitPath(path); + RequestMappingImpl1 rm = new RequestMappingImpl1("/superpath/mypath || mypath.json || somethingelse.what", null); + String[] splitPath = rm.getSplitPath(); assertEquals(3, splitPath.length); assertEquals("/superpath/mypath", splitPath[0]); assertEquals("/superpath/mypath.json", splitPath[1]); 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 3247697de..29ef8566e 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 @@ -18,6 +18,7 @@ import static org.junit.Assert.assertTrue; import java.io.File; import java.net.URL; import java.time.Duration; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -26,6 +27,7 @@ import org.json.JSONObject; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping; import org.springframework.ide.vscode.commons.util.AsyncProcess; import org.springframework.ide.vscode.commons.util.ExternalCommand; import org.springframework.ide.vscode.commons.util.StringUtil; @@ -139,8 +141,8 @@ public class SpringBootAppTest { @Test public void getRequestMappings() throws Exception { ACondition.waitFor(TIMEOUT, () -> { - String result = testApp.getRequestMappings(); - assertNonEmptyJsonObject(result); + Collection result = testApp.getRequestMappings(); + assertTrue(result != null && !result.isEmpty()); // System.out.println("requestMappings = "+result); }); } diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/parser/JLRMethodParser.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/parser/JLRMethodParser.java index 406eb6b24..970480e8c 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/parser/JLRMethodParser.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/parser/JLRMethodParser.java @@ -10,9 +10,11 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.java.parser; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -32,6 +34,8 @@ public class JLRMethodParser { private String fqClass; private String methodName; + private String returnType; + private String[] parameters; //TODO: parsing arguments to handle overloading @@ -49,6 +53,7 @@ public class JLRMethodParser { while (modifiersEnd=modifiersEnd+2) { methodString = pieces[modifiersEnd+1]; int methodNameEnd = methodString.indexOf('('); @@ -63,6 +68,55 @@ public class JLRMethodParser { } } } + + int parametersStart = rawString.indexOf('('); + int parametersEnd = rawString.indexOf(')'); + if (parametersStart < parametersEnd) { + String parametersString = rawString.substring(parametersStart + 1, parametersEnd); + this.parameters = parseParameters(parametersString); + } else { + this.parameters = new String[0]; + } + } + + private String[] parseParameters(String parameterString) { + List parameters = new ArrayList<>(); + int openTemplateParameters = 0; + StringBuilder currentParameter = new StringBuilder(); + for (int i = 0; i < parameterString.length(); i++) { + char ch = parameterString.charAt(i); + switch (ch) { + case '.': + currentParameter.append(ch); + break; + case '<': + openTemplateParameters++; + currentParameter.append(ch); + break; + case '>': + openTemplateParameters--; + currentParameter.append(ch); + break; + case ',' : + if (openTemplateParameters == 0) { + if (currentParameter.length() != 0) { + parameters.add(currentParameter.toString()); + currentParameter = new StringBuilder(); + } + } else { + currentParameter.append(ch); + } + break; + default: + if (Character.isJavaIdentifierPart(ch)) { + currentParameter.append(ch); + } + } + } + if (currentParameter.length() > 0) { + parameters.add(currentParameter.toString()); + } + return parameters.toArray(new String[parameters.size()]); } @Override @@ -77,6 +131,14 @@ public class JLRMethodParser { public String getMethodName() { return methodName; } + + public String getReturnType() { + return returnType; + } + + public String[] getParameters() { + return parameters; + } }