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 09a949600..3f9dc1d50 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 @@ -19,11 +19,14 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.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; @@ -31,6 +34,8 @@ 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.util.BadLocationException; import org.springframework.ide.vscode.commons.util.Log; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -58,7 +63,7 @@ public class RequestMappingHoverProvider implements HoverProvider { } } catch (BadLocationException e) { - e.printStackTrace(); + Log.log(e); } return null; @@ -67,13 +72,12 @@ public class RequestMappingHoverProvider implements HoverProvider { private CompletableFuture provideHover(Annotation annotation, TextDocument doc, SpringBootApp[] runningApps) { try { - - Optional val = getRequestMappingsForAnnotation(annotation, runningApps); - List> hoverContent = new ArrayList<>(); + Optional val = getRequestMappingMethodFromRunningApp(annotation, runningApps); + if (val.isPresent()) { - addHoverContent(val.get(), hoverContent, annotation); + addHoverContent(val.get(), hoverContent); } Range hoverRange = doc.toRange(annotation.getStartPosition(), annotation.getLength()); @@ -83,75 +87,107 @@ public class RequestMappingHoverProvider implements HoverProvider { hover.setRange(hoverRange); return CompletableFuture.completedFuture(hover); - } - catch (Exception e) { - e.printStackTrace(); + } catch (Exception e) { + Log.log(e); } return null; } - private Optional getRequestMappingsForAnnotation(Annotation annotation, + private Optional getRequestMappingMethodFromRunningApp(Annotation annotation, SpringBootApp[] runningApps) { try { - SpringBootApp foundApp = null; - JSONObject requestMappings = null; for (SpringBootApp app : runningApps) { String mappings = app.getRequestMappings(); - if (doesMatch(annotation, mappings)) { - requestMappings = new JSONObject(mappings); - foundApp = app; - break; + 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)); + } + } } } - if (foundApp != null && requestMappings != null) { - return Optional.of(new AppMappings(requestMappings, foundApp)); - } - } - catch (Exception e) { + } catch (Exception e) { Log.log(e); } - return Optional.empty(); - - } - private void addHoverContent(AppMappings appMappings, List> hoverContent, Annotation annotation) throws Exception { - Iterator keys = appMappings.mappings.keys(); - String processId = appMappings.app.getProcessID(); - String processName = appMappings.app.getProcessName(); - while (keys.hasNext()) { - String key = keys.next(); - if (doesMatch(annotation, key)) { - String path = UrlUtil.extractPath(key); - String port = appMappings.app.getPort(); - String host = appMappings.app.getHost(); + private boolean methodMatchesAnnotation(Annotation annotation, JLRMethod requestMappingMethod) { + String rqClassName = requestMappingMethod.getFQClassName(); + String rqMethod = requestMappingMethod.getMethodName(); - String url = UrlUtil.createUrl(host, port, path); - StringBuilder builder = new StringBuilder(); - - if (url != null) { - builder.append("Path: "); - builder.append("["); - builder.append(path); - builder.append("]"); - builder.append("("); - builder.append(url); - builder.append(")"); - } else { - builder.append("Unable to resolve URL for path: " + key); - } - - hoverContent.add(Either.forLeft(builder.toString())); - } + 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 false; + } + + private void addHoverContent(RequestMappingMethod mappingMethod, List> hoverContent) throws Exception { + String processId = mappingMethod.app.getProcessID(); + String processName = mappingMethod.app.getProcessName(); + String path = mappingMethod.requestMappingPath; + + StringBuilder builder = new StringBuilder(); + + String port = mappingMethod.app.getPort(); + String host = mappingMethod.app.getHost(); + String url = UrlUtil.createUrl(host, port, path); + + 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 boolean doesMatch(Annotation annotation, String key) { + 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(); @@ -175,7 +211,7 @@ public class RequestMappingHoverProvider implements HoverProvider { } - return mappingPath != null ? key.contains(mappingPath) : false; + return jsonKey.contains(mappingPath); } public JSONObject[] getRequestMappingsFromProcesses(SpringBootApp[] runningApps) { @@ -193,22 +229,22 @@ public class RequestMappingHoverProvider implements HoverProvider { } } catch (Exception e) { - e.printStackTrace(); + Log.log(e); } return result.toArray(new JSONObject[result.size()]); } - static class AppMappings { + static class RequestMappingMethod { - public final JSONObject mappings; public final SpringBootApp app; + public final String requestMappingPath; + public final JLRMethod requestMappingMethod; - public AppMappings(JSONObject mapping, SpringBootApp app) { - this.mappings = mapping; + public RequestMappingMethod(String requestMappingPath, JLRMethod requestMappingMethod, SpringBootApp app) { + this.requestMappingPath = requestMappingPath; + this.requestMappingMethod = requestMappingMethod; this.app = app; } - } - } 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 3b454b76e..722fe5455 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 @@ -169,6 +169,19 @@ public class SpringBootApp { return null; } + public Object getRequestMapping(String rawKey) throws Exception { + Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=requestMappingEndpoint", "Data"); + if (result == null) { + result = getActuatorDataFromOperation("org.springframework.boot:type=Endpoint,name=Mappings", "mappings"); + } + + if (result instanceof HashMap) { + return ((HashMap) result).get(rawKey); + } + + return null; + } + public String getAutoConfigReport() throws Exception { Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=autoConfigurationReportEndpoint", "Data"); if (result != null) { @@ -318,15 +331,8 @@ public class SpringBootApp { public String getHost() throws Exception { String jmxConnect = this.vm.startLocalManagementAgent(); - - JMXConnector jmxConnector = null; - try { - JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect); - return serviceUrl.getHost(); - } - finally { - if (jmxConnector != null) jmxConnector.close(); - } + JMXServiceURL serviceUrl = new JMXServiceURL(jmxConnect); + return serviceUrl.getHost(); } } 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 new file mode 100644 index 000000000..406eb6b24 --- /dev/null +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/parser/JLRMethodParser.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * 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.java.parser; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Uiltily methods for extracting info out of the 'toString' values produced by java.lang.reflect.Method + * objects. + * + * @author Kris De Volder + */ +public class JLRMethodParser { + + public static class JLRMethod { + + /** + * The whole 'raw' string (i.e. not parsed or processed in any way). + */ + private String rawString; + + private String fqClass; + private String methodName; + + //TODO: parsing arguments to handle overloading + + public JLRMethod(String method) { + this.rawString = method; + String methodString = method; + // Example: public java.lang.Object org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint.invoke(java.security.Principal) + // Example: java.util.Collection demo.ReservationRestController.reservations() + // public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)' + + //The spaces inside generics will mess this klunky parser up. So get rid of those first: + methodString = methodString.replaceAll(",\\s", ","); + String[] pieces = methodString.split("\\s"); + int modifiersEnd = 0; + while (modifiersEnd=modifiersEnd+2) { + methodString = pieces[modifiersEnd+1]; + int methodNameEnd = methodString.indexOf('('); + if (methodNameEnd>=0) { + int methodNameStart = methodString.lastIndexOf('.', methodNameEnd); + if (methodNameStart>=0) { + fqClass = methodString.substring(0, methodNameStart); + if (methodNameStart>=0) { + methodNameStart = methodNameStart +1; //+1 because actauly pointing at the '.', not the name start + } + methodName = methodString.substring(methodNameStart, methodNameEnd); + } + } + } + } + + @Override + public String toString() { + return rawString; + } + + public String getFQClassName() { + return fqClass; + } + + public String getMethodName() { + return methodName; + } + + } + + private static final Set MODIFIERS = Collections.unmodifiableSet(new HashSet(Arrays.asList( + "public", "protected", "private", "abstract", + "static", "final", "synchronized", "native", "strictfp" + ))); + + private static boolean isModifier(String string) { + return MODIFIERS.contains(string); + } + + public static JLRMethod parse(String method) { + if (method!=null) { + return new JLRMethod(method); + } + return null; + } + + public static String parseFQClassName(String data) { + JLRMethod m = parse(data); + if (m!=null) { + return m.getFQClassName(); + } + return null; + } + + public static String parseMethodName(String data) { + JLRMethod m = parse(data); + if (m!=null) { + return m.getMethodName(); + } + return null; + } + +}