diff --git a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java index a5a1b1c45..bd09eea96 100644 --- a/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionals/ConditionalsLiveHoverProvider.java @@ -30,6 +30,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.json.JSONArray; import org.json.JSONObject; import org.springframework.ide.vscode.boot.java.handlers.HoverProvider; +import org.springframework.ide.vscode.boot.java.utils.HoverContentUtils; import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Log; @@ -39,8 +40,8 @@ import com.google.common.collect.ImmutableList; /** * - * Provides live hovers and hints for @ConditionalOn... Spring Boot annotations from running - * spring boot apps. + * Provides live hovers and hints for @ConditionalOn... Spring Boot annotations + * from running spring boot apps. */ public class ConditionalsLiveHoverProvider implements HoverProvider { @@ -98,14 +99,9 @@ public class ConditionalsLiveHoverProvider implements HoverProvider { List> hoverContent) throws Exception { for (int i = 0; i < conditions.size(); i++) { RunningAppConditional condition = conditions.get(i); - hoverContent.add(Either.forLeft("Condition: " + condition.condition)); - hoverContent.add(Either.forLeft("Message: " + condition.message)); - - // If there is more than one instances show process information - if (conditions.size() > 1) { - hoverContent.add(Either.forLeft("Process ID: " + condition.app.getProcessID())); - hoverContent.add(Either.forLeft("Process Name: " + condition.app.getProcessName())); - } + hoverContent.add(Either.forLeft(condition.message)); + hoverContent.add(Either + .forLeft(HoverContentUtils.getProcessInformation(condition.app))); if (i < conditions.size() - 1) { hoverContent.add(Either.forLeft("---")); 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 76af42453..81b778110 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,38 +11,36 @@ 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.List; -import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; -import org.eclipse.jdt.core.dom.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.SingleMemberAnnotation; -import org.eclipse.jdt.core.dom.StringLiteral; -import org.eclipse.jdt.core.dom.TypeDeclaration; 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.Renderable; +import org.springframework.ide.vscode.commons.util.Renderables; 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,8 +56,8 @@ public class RequestMappingHoverProvider implements HoverProvider { public Collection getLiveHoverHints(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { try { if (runningApps.length > 0) { - Optional> val = getRequestMappingMethodFromRunningApp(annotation, runningApps); - if (val.isPresent()) { + List> val = getRequestMappingMethodFromRunningApp(annotation, runningApps); + if (!val.isEmpty()) { Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); return ImmutableList.of(hoverRange); } @@ -77,10 +75,10 @@ public class RequestMappingHoverProvider implements HoverProvider { try { List> hoverContent = new ArrayList<>(); - Optional> val = getRequestMappingMethodFromRunningApp(annotation, runningApps); + List> val = getRequestMappingMethodFromRunningApp(annotation, runningApps); - if (val.isPresent()) { - addHoverContent(val.get(), hoverContent); + if (!val.isEmpty()) { + addHoverContent(val, hoverContent); } Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); @@ -97,171 +95,79 @@ public class RequestMappingHoverProvider implements HoverProvider { return null; } - private Optional> getRequestMappingMethodFromRunningApp(Annotation annotation, + private List> getRequestMappingMethodFromRunningApp(Annotation annotation, SpringBootApp[] runningApps) { + List> results = new ArrayList<>(); try { - List methods = new ArrayList<>(); 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)) { - methods.add(new RequestMappingMethod(path, parsedMethod, app)); - } - } - } + Collection mappings = app.getRequestMappings(); + if (mappings != null && !mappings.isEmpty()) { + mappings.stream() + .filter(rm -> methodMatchesAnnotation(annotation, rm)) + .map(rm -> Tuples.of(rm, app)) + .findFirst().ifPresent(t -> results.add(t)); } } - - if (!methods.isEmpty()) { - return Optional.of(methods); - } } catch (Exception e) { Log.log(e); } - return Optional.empty(); + return results; } - 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); - } else if (parent instanceof TypeDeclaration) { - TypeDeclaration typeDec = (TypeDeclaration) parent; - return typeDec.resolveBinding().getQualifiedName().equals(rqClassName); + return binding.getDeclaringClass().getQualifiedName().equals(rqClassName) + && binding.getName().equals(rm.getMethodName()) + && Arrays.equals(Arrays.stream(binding.getParameterTypes()) + .map(t -> t.getTypeDeclaration().getQualifiedName()) + .toArray(String[]::new), + rm.getMethodParameters()); +// } else if (parent instanceof TypeDeclaration) { +// TypeDeclaration typeDec = (TypeDeclaration) parent; +// return typeDec.resolveBinding().getQualifiedName().equals(rqClassName); } return false; } - private void addHoverContent(List mappingMethods, List> hoverContent) - throws Exception { - for (int i = 0; i < mappingMethods.size() ; i++) { - RequestMappingMethod method = mappingMethods.get(i); - String processId = method.app.getProcessID(); - String processName = method.app.getProcessName(); - String path = method.requestMappingPath; + private void addHoverContent(List> mappingMethods, List> hoverContent) throws Exception { + for (int i = 0; i < mappingMethods.size(); i++) { + Tuple2 mappingMethod = mappingMethods.get(i); - StringBuilder builder = new StringBuilder(); + String processId = mappingMethod.getT2().getProcessID(); + String processName = mappingMethod.getT2().getProcessName(); + String port = mappingMethod.getT2().getPort(); + String host = mappingMethod.getT2().getHost(); - String port = method.app.getPort(); - String host = method.app.getHost(); - String url = UrlUtil.createUrl(host, port, path); + List renderableUrls = Arrays.stream(mappingMethod.getT1().getSplitPath()).flatMap(path -> { + String url = UrlUtil.createUrl(host, port, path); + StringBuilder builder = new StringBuilder(); + builder.append("["); + builder.append(url); + builder.append("]"); + builder.append("("); + builder.append(url); + builder.append(")"); + return Stream.of(Renderables.text(builder.toString()), Renderables.lineBreak()); + }) + .collect(Collectors.toList()); - builder.append("["); - builder.append(url); - builder.append("]"); - builder.append("("); - builder.append(url); - builder.append(")"); + // Remove the last line break + renderableUrls.remove(renderableUrls.size() - 1); - hoverContent.add(Either.forLeft(builder.toString())); + hoverContent.add(Either.forLeft(Renderables.concat(renderableUrls).toMarkdown())); hoverContent.add(Either.forLeft("Process ID: " + processId)); hoverContent.add(Either.forLeft("Process Name: " + processName)); - if (i < mappingMethods.size() - 1) { + if (i < mappingMethods.size() - 1) { // Three dashes == line separator in Markdown hoverContent.add(Either.forLeft("---")); } - } - } - - 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"); - } - } - } - 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; - if (annotation instanceof SingleMemberAnnotation) { - Expression valueContent = ((SingleMemberAnnotation) annotation).getValue(); - if (valueContent instanceof StringLiteral) { - mappingPath = ((StringLiteral) valueContent).getLiteralValue(); - } - } - 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(); - } - } - } - } - - } - - return mappingPath != null ? jsonKey.contains(mappingPath) : false; - } - - public JSONObject[] getRequestMappingsFromProcesses(SpringBootApp[] runningApps) { - List result = new ArrayList<>(); - - try { - for (SpringBootApp app : runningApps) { - String mappings = app.getRequestMappings(); - if (mappings != null) { - JSONObject requestMappings = new JSONObject(mappings); - if (requestMappings != null) { - result.add(requestMappings); - } - } - } - } - catch (Exception e) { - Log.log(e); - } - - return result.toArray(new JSONObject[result.size()]); - } - - static class RequestMappingMethod { - - public final SpringBootApp app; - public final String requestMappingPath; - public final JLRMethod requestMappingMethod; - - public RequestMappingMethod(String requestMappingPath, JLRMethod requestMappingMethod, SpringBootApp app) { - this.requestMappingPath = requestMappingPath; - this.requestMappingMethod = requestMappingMethod; - 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/main/java/org/springframework/ide/vscode/boot/java/utils/HoverContentUtils.java b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/HoverContentUtils.java new file mode 100644 index 000000000..5b34dea3c --- /dev/null +++ b/headless-services/boot-java-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/HoverContentUtils.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp; + +public class HoverContentUtils { + + public static String getProcessInformation(SpringBootApp app) { + return "Process " + app.getProcessID() + ": " + app.getProcessName(); + } + +} diff --git a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalsLiveHoverTest.java b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalsLiveHoverTest.java index cb887f67c..ce876ca07 100644 --- a/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalsLiveHoverTest.java +++ b/headless-services/boot-java-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionals/test/ConditionalsLiveHoverTest.java @@ -74,9 +74,10 @@ public class ConditionalsLiveHoverTest { harness.intialize(directory); Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); - editor.assertHoverContains("@ConditionalOnBean", "Condition: OnBeanCondition\n" + "\n" - + "Message: @ConditionalOnBean (types: example.Hello; SearchStrategy: all) found bean 'missing'"); - + editor.assertHoverContains("@ConditionalOnBean", + "@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found bean 'missing'\n" + + "\n" + + "Process 22022: test-conditionals-live-hover"); } @Test @@ -97,8 +98,9 @@ public class ConditionalsLiveHoverTest { harness.intialize(directory); Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); - editor.assertHoverContains("@ConditionalOnMissingBean", "Condition: OnBeanCondition\n" + "\n" - + "Message: @ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans"); + editor.assertHoverContains("@ConditionalOnMissingBean", "@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\n"+ + "\n" + + "Process 22022: test-conditionals-live-hover"); } @@ -120,20 +122,26 @@ public class ConditionalsLiveHoverTest { Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); - editor.assertHoverContains("@ConditionalOnBean", "Condition: OnBeanCondition\n" + "\n" - + "Message: @ConditionalOnBean (types: example.Hello; SearchStrategy: all) found beans 'hi', 'missing'"); + editor.assertHoverContains("@ConditionalOnBean", "@ConditionalOnBean (types: example.Hello; SearchStrategy: all) found beans 'hi', 'missing'\n" + + "\n" + + "Process 22022: test-conditionals-live-hover"); - editor.assertHoverContains("@ConditionalOnWebApplication", "Condition: OnWebApplicationCondition\n" + "\n" - + "Message: @ConditionalOnWebApplication (required) found StandardServletEnvironment"); + editor.assertHoverContains("@ConditionalOnWebApplication", "@ConditionalOnWebApplication (required) found StandardServletEnvironment\n"+ + "\n" + + "Process 22022: test-conditionals-live-hover"); editor.assertHoverContains("@ConditionalOnJava(value=ConditionalOnJava.JavaVersion.EIGHT)", - "Condition: OnJavaCondition\n" + "\n" + "Message: @ConditionalOnJava (1.8 or newer) found 1.8"); + "@ConditionalOnJava (1.8 or newer) found 1.8\n" + + "\n" + + "Process 22022: test-conditionals-live-hover"); - editor.assertHoverContains("@ConditionalOnMissingClass", "Condition: OnClassCondition\n" + "\n" - + "Message: @ConditionalOnClass found required class; @ConditionalOnMissingClass did not find unwanted class"); + editor.assertHoverContains("@ConditionalOnMissingClass", "@ConditionalOnClass found required class; @ConditionalOnMissingClass did not find unwanted class\n" + + "\n" + + "Process 22022: test-conditionals-live-hover"); - editor.assertHoverContains("@ConditionalOnExpression", "Condition: OnExpressionCondition\n" + "\n" - + "Message: @ConditionalOnExpression (#{true}) resulted in true"); + editor.assertHoverContains("@ConditionalOnExpression", "@ConditionalOnExpression (#{true}) resulted in true\n" + + "\n" + + "Process 22022: test-conditionals-live-hover"); } @@ -169,30 +177,21 @@ public class ConditionalsLiveHoverTest { Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); - editor.assertHoverContains("@ConditionalOnMissingBean", "Condition: OnBeanCondition\n" + "\n" - + "Message: @ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\n" + + editor.assertHoverContains("@ConditionalOnMissingBean", "@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\n" + "\n" + - "Process ID: 70000\n" + - "\n" + - "Process Name: test-conditionals-live-hover\n" + + "Process 70000: test-conditionals-live-hover\n" + "\n" + "---\n" + "\n" + - "Condition: OnBeanCondition\n" + "\n" - + "Message: @ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\n" + + "@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\n" + "\n" + - "Process ID: 80000\n" + - "\n" + - "Process Name: test-conditionals-live-hover\n" + + "Process 80000: test-conditionals-live-hover\n" + "\n" + "---\n" + "\n" + - "Condition: OnBeanCondition\n" + "\n" - + "Message: @ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\n" + + "@ConditionalOnMissingBean (types: example.Hello; SearchStrategy: all) did not find any beans\n" + "\n" + - "Process ID: 90000\n" + - "\n" + - "Process Name: test-conditionals-live-hover"); + "Process 90000: test-conditionals-live-hover"); } 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 0ad9bd216..050227d84 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 @@ -62,7 +62,7 @@ public class RequestMappingLiveHoverTest { harness.intialize(directory); Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); - editor.assertHoverContains("@RequestMapping(\"/hello-world\")", "[http://cfapps.io:1111/hello-world](http://cfapps.io:1111/hello-world)\n" + + editor.assertHoverContains("@RequestMapping(method=RequestMethod.GET)", "[http://cfapps.io:1111/hello-world](http://cfapps.io:1111/hello-world)\n" + "\n" + "Process ID: 22022\n" + "\n" + @@ -132,6 +132,555 @@ public class RequestMappingLiveHoverTest { } + @Test + public void testSimpleMethodHoverHintMethod1() 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\")", "[http://cfapps.io:999/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=[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(int n) {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@DeleteMapping(\"/greetings\")"); + + } + + + + + @Test + public void testNoHoverHintMethod() 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 java.lang.String com.example.RestApi.updateGreetings()\"}}") + . 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.PutMapping;\n" + + "\n" + + "@Controller\n" + + "public class RestApi {\n" + + "\n" + + "@PutMapping(\"/greetings\")\n" + + "public String updateGreetings(int n) {\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertNoHover("@PutMapping(\"/greetings\")"); + + } + + @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)", "[http://cfapps.io:999/greetings](http://cfapps.io:999/greetings) \n" + + "[http://cfapps.io:999/hello](http://cfapps.io:999/hello)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @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)", "[http://cfapps.io:999/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)", "[http://cfapps.io:999/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)", "[http://cfapps.io:999/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testWildCardMethodMatchingHoverHint() 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)", "[http://cfapps.io:999/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testArrayMethodMatchingHoverHint() 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[])\"}}") + . 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 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[] p) {\n" + + "return null;\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value=\"/find\", method=GET)", "[http://cfapps.io:999/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testMultiDimensionalArrayMethodMatchingHoverHint() 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[][])\"}}") + . 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 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[][] p) {\n" + + "return null;\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value=\"/find\", method=GET)", "[http://cfapps.io:999/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } + + @Test + public void testVarArgsMethodMatchingHoverHint() 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...)\"}}") + . 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 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... p) {\n" + + "return null;\n" + + "}\n" + + "\n" + + "}", + docUri); + + editor.assertHoverContains("@RequestMapping(value=\"/find\", method=GET)", "[http://cfapps.io:999/find](http://cfapps.io:999/find)\n" + + "\n" + + "Process ID: 76543\n" + + "\n" + + "Process Name: test-request-mapping-live-hover"); + + } @Test public void testMultipleAppsLiveHover() throws Exception { 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 f7f656532..b06af7246 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,9 +19,9 @@ 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.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.util.ExceptionUtil; -import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness.Builder; import com.google.common.collect.ImmutableList; @@ -102,7 +102,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 7c20c3917..d1ce1a918 100644 --- a/headless-services/commons/commons-boot-app-cli/pom.xml +++ b/headless-services/commons/commons-boot-app-cli/pom.xml @@ -42,6 +42,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 eed3af9ea..661326865 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.boot.app.cli.livebean.LiveBeansModel; import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.StringUtil; @@ -198,17 +202,29 @@ public class SpringBootApp { } } - 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; @@ -436,4 +452,5 @@ public class SpringBootApp { 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..6dd7a2004 --- /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,180 @@ +/******************************************************************************* + * 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()) + .filter(s -> !s.isEmpty()) + .map(s -> { + if (s.charAt(0) != '/') { + return '/' + s; + } else { + return s; + } + }) + .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 55% 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..0e845fd8a 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,51 +8,51 @@ * 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]); + assertEquals("/mypath.json", splitPath[1]); } @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]); - assertEquals("/superpath/somethingelse.what", splitPath[2]); + assertEquals("/mypath.json", splitPath[1]); + assertEquals("/somethingelse.what", splitPath[2]); } } 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 4047c55c1..501ae697f 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 @@ -26,11 +26,11 @@ import java.util.stream.Collectors; import org.json.JSONObject; import org.junit.AfterClass; -import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel; +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.ExceptionUtil; import org.springframework.ide.vscode.commons.util.ExternalCommand; @@ -183,9 +183,9 @@ public class SpringBootAppTest { public void getRequestMappings() throws Exception { for (SpringBootApp testApp : getTestApps()) { ACondition.waitFor(TIMEOUT, () -> { - String result = testApp.getRequestMappings(); - assertNonEmptyJsonObject(result); - // System.out.println("requestMappings = "+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..b151b37d9 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,64 @@ 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: + currentParameter.append(ch); + } + } + if (currentParameter.length() > 0) { + parameters.add(currentParameter.toString()); + } + return parameters.stream() + .map(p -> { + int genericStart = p.indexOf('<'); + int genericEnd = p.lastIndexOf('>'); + if (genericStart < genericEnd) { + return p.substring(0, genericStart) + p.substring(genericEnd + 1); + } else { + return p; + } + }) + .map(p -> p.replaceAll("\\.\\.\\.", "[]")) + .toArray(String[]::new); } @Override @@ -77,6 +140,14 @@ public class JLRMethodParser { public String getMethodName() { return methodName; } + + public String getReturnType() { + return returnType; + } + + public String[] getParameters() { + return parameters; + } }