diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index a56e2bda8..f50664126 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -556,6 +556,26 @@ public class Editor { assertContains(snippet, hoverString(hover)); } + public void assertLiveCodeLensContains(String codeLensOver, int occurrence, String snippet) throws Exception { + int cmPosition = getHoverPosition(codeLensOver, occurrence); + for (CodeLens cm : getLiveDataCodeLenses()) { + if (cmPosition >= doc.toOffset(cm.getRange().getStart()) && cmPosition <= doc.toOffset(cm.getRange().getEnd())) { + if (cm.getData() instanceof String) { + if (((String) cm.getData()).contains(snippet)) { + return; + } + } else { + fail("Live Data CodeLens data field is not a string"); + } + } + } + fail("Cannot find '" + snippet + "' in Live Data Code Lenses"); + } + + public void assertLiveCodeLensContains(String codeLensOver, String snippet) throws Exception { + assertLiveCodeLensContains(codeLensOver, 1, snippet); + } + public String hoverString(Hover hover) { StringBuilder buf = new StringBuilder(); boolean first = true; @@ -619,6 +639,16 @@ public class Editor { Hover hover = harness.getHover(doc, doc.toPosition(hoverPosition)); assertEquals(expectedHover.trim(), hoverString(hover).trim()); } + + public List getCodeLenses() throws Exception { + return harness.getCodeLenses(doc); + } + + public List getLiveDataCodeLenses() throws Exception { + HighlightParams highlights = this.highlightsFuture.get(HIGHLIGHTS_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); + return highlights != null ? highlights.getCodeLenses() : ImmutableList.of(); + } + public void assertNoHover(String hoverOver, int occurence) throws Exception { int hoverPosition = getHoverPosition(hoverOver,occurence); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java index 2b3202a5a..adf6a1fe1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java @@ -13,8 +13,6 @@ package org.springframework.ide.vscode.boot.java.livehover.v2; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -38,7 +36,6 @@ import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.commons.util.StringUtil; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableList.Builder; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -129,13 +126,12 @@ public class SpringProcessLiveDataExtractorOverJMX { return new LiveMetricsModel() { @Override - public RequestMappingMetrics getRequestMappingMetrics(String[] uris, String[] requestMethods) { + public RequestMappingMetrics getRequestMappingMetrics(String[] paths, String[] requestMethods) { try { List tags = new ArrayList<>(); - if (uris.length == 0) { + if (paths.length == 0) { return null; } - String[] paths = getPaths(uris); tags.add("uri:" + String.join(",", paths)); if (requestMethods.length > 0) { tags.add("method:" + String.join(",", requestMethods)); @@ -161,22 +157,6 @@ public class SpringProcessLiveDataExtractorOverJMX { return null; } - private String[] getPaths(String[] uris) { - Builder builder = ImmutableList.builder(); - if (uris != null) { - for (String val : uris) { - try { - URI uri = new URI(val); - builder.add(uri.getPath()); - } catch (URISyntaxException e) { - log.error("", e); - } - - } - } - - return builder.build().toArray(new String[0]); - } }; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java index 00b6df6fa..374eab7b8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingHoverProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2019 Pivotal, Inc. + * Copyright (c) 2017, 2020 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 @@ -140,10 +140,18 @@ public class RequestMappingHoverProvider implements HoverProvider { int remaining = 0; for (Tuple2 dataEntry : data) { - for (String url : getUrls(dataEntry)) { + for (Tuple2 urlWithPath : getUrlsWithPath(dataEntry)) { if (lenses.size() <= CODE_LENS_LIMIT) { - Set requestMethods = dataEntry.getT1().getRequestMethods(); - RequestMappingMetrics metrics = dataEntry.getT2().getLiveMterics().getRequestMappingMetrics(new String[] { url }, requestMethods.toArray(new String[requestMethods.size()])); + SpringProcessLiveData liveData = dataEntry.getT2(); + LiveRequestMapping requestMapping = dataEntry.getT1(); + String url = urlWithPath.getT1(); + String path = urlWithPath.getT2(); + Set requestMethods = requestMapping.getRequestMethods(); + + RequestMappingMetrics metrics = liveData.getLiveMterics() == null ? null + : liveData.getLiveMterics().getRequestMappingMetrics(new String[] { path }, + requestMethods.toArray(new String[requestMethods.size()])); + CodeLens codeLens = createCodeLensForRequestMapping(range, url, metrics); lenses.add(codeLens); } else { @@ -232,8 +240,8 @@ public class RequestMappingHoverProvider implements HoverProvider { return false; } - private List getUrls(Tuple2 mappingMethod) { - List urls = new ArrayList<>(); + private List> getUrlsWithPath(Tuple2 mappingMethod) { + List> urls = new ArrayList<>(); SpringProcessLiveData liveData = mappingMethod.getT2(); String contextPath = liveData.getContextPath(); @@ -241,7 +249,8 @@ public class RequestMappingHoverProvider implements HoverProvider { String port = liveData.getPort(); String host = liveData.getHost(); - String[] paths = mappingMethod.getT1().getSplitPath(); + LiveRequestMapping requestMapping = mappingMethod.getT1(); + String[] paths = requestMapping.getSplitPath(); if (paths==null || paths.length==0) { //Technically, this means the path 'predicate' is unconstrained, meaning any path matches. //So this is not quite the same as the case where path=""... but... @@ -251,7 +260,7 @@ public class RequestMappingHoverProvider implements HoverProvider { } for (String path : paths) { String url = UrlUtil.createUrl(urlScheme, host, port, path, contextPath); - urls.add(url); + urls.add(Tuples.of(url, path)); } return urls; } @@ -286,10 +295,12 @@ public class RequestMappingHoverProvider implements HoverProvider { Renderable urlRenderables = Renderables.concat(renderableUrls); Set requestMethods = requestMapping.getRequestMethods(); - RequestMappingMetrics metrics = liveData.getLiveMterics().getRequestMappingMetrics(requestMapping.getSplitPath(), requestMethods.toArray(new String[requestMethods.size()])); + RequestMappingMetrics metrics = liveData.getLiveMterics() == null ? null + : liveData.getLiveMterics().getRequestMappingMetrics(requestMapping.getSplitPath(), + requestMethods.toArray(new String[requestMethods.size()])); if (metrics != null) { Renderable metricsRenderable = Renderables.concat( - Renderables.bold("Count: " + metrics.getCallsCount() + " | Total Time: " + metrics.getTotalTime() + " | Max Time: " + metrics.getMaxTime()), + Renderables.bold(createHoverMetricsContent(metrics)), Renderables.text("\n\n")); urlRenderables = Renderables.concat(urlRenderables, Renderables.text("\n\n"), metricsRenderable); } @@ -313,6 +324,42 @@ public class RequestMappingHoverProvider implements HoverProvider { // being added between the content itself return new Hover(ImmutableList.of(Either.forLeft(contentVal.toString()))); } + + private String createHoverMetricsContent(RequestMappingMetrics metrics) { + char timeUnitShort = metrics.getTimeUnit().name().toLowerCase().charAt(0); + + StringBuilder metricsContent = new StringBuilder(); + metricsContent.append("Count: "); + metricsContent.append(metrics.getCallsCount()); + metricsContent.append(" | Total Time: "); + metricsContent.append(metrics.getTotalTime()); + metricsContent.append(timeUnitShort); + metricsContent.append(" | Max Time: "); + metricsContent.append(metrics.getMaxTime()); + metricsContent.append(timeUnitShort); + + return metricsContent.toString(); + } + + private String createCodeLensMetricsContent(RequestMappingMetrics metrics) { + char timeUnitShort = metrics.getTimeUnit().name().toLowerCase().charAt(0); + + StringBuilder metricsContent = new StringBuilder(); + + metricsContent.append("Count="); + metricsContent.append(metrics.getCallsCount()); + metricsContent.append(' '); + metricsContent.append("Total="); + metricsContent.append(String.format("%.2f", metrics.getTotalTime())); + metricsContent.append(timeUnitShort); + metricsContent.append(' '); + metricsContent.append("Max="); + metricsContent.append(String.format("%.2f", metrics.getMaxTime())); + metricsContent.append(timeUnitShort); + metricsContent.append(')'); + + return metricsContent.toString(); + } private CodeLens createCodeLensForRequestMapping(Range range, String content, RequestMappingMetrics metrics) { CodeLens codeLens = new CodeLens(); @@ -322,24 +369,11 @@ public class RequestMappingHoverProvider implements HoverProvider { if (StringUtil.hasText(content)) { if (metrics != null) { - char timeUnitShort = metrics.getTimeUnit().name().charAt(0); - StringBuilder codeLensContent = new StringBuilder(content); - codeLensContent.append(' '); codeLensContent.append('('); - codeLensContent.append("Count="); - codeLensContent.append(metrics.getCallsCount()); - codeLensContent.append(' '); - codeLensContent.append("Total="); - codeLensContent.append(shorten(metrics.getTotalTime())); - codeLensContent.append(timeUnitShort); - codeLensContent.append(' '); - codeLensContent.append("Max="); - codeLensContent.append(shorten(metrics.getMaxTime())); - codeLensContent.append(timeUnitShort); + codeLensContent.append(createCodeLensMetricsContent(metrics)); codeLensContent.append(')'); - content = codeLensContent.toString(); } @@ -354,10 +388,6 @@ public class RequestMappingHoverProvider implements HoverProvider { return codeLens; } - private double shorten(double val) { - return val; - } - private CodeLens createCodeLensForRemaining(Range range, int remaining) { CodeLens codeLens = new CodeLens(); codeLens.setRange(range); @@ -373,5 +403,5 @@ public class RequestMappingHoverProvider implements HoverProvider { return codeLens; } - + } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java index e84b8ee77..165635b6e 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingLiveHoverTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2018 Pivotal, Inc. + * Copyright (c) 2017, 2020 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 @@ -203,7 +203,7 @@ public class RequestMappingLiveHoverTest { @Override public double getTotalTime() { - return 3.0; + return "/hello".equals(paths[0]) ? 3.034567 : 5.43673; } @Override @@ -213,12 +213,12 @@ public class RequestMappingLiveHoverTest { @Override public double getMaxTime() { - return 0.55; + return "/hello".equals(paths[0]) ? 0.5535632 : 0.7437624; } @Override public long getCallsCount() { - return 25; + return "/hello".equals(paths[0]) ? 25 : 34; } }; } @@ -231,10 +231,72 @@ public class RequestMappingLiveHoverTest { Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); editor.assertHighlights("@RequestMapping(\"/hello\")", "@RequestMapping(\"/goodbye\")"); - editor.assertHoverContains("@RequestMapping(\"/hello\")", "[https://cfapps.io:999/hello](https://cfapps.io:999/hello) \n" + - "Process [PID=76543, name=`test-request-mapping-live-hover`]"); + editor.assertHoverContains("@RequestMapping(\"/hello\")", "[https://cfapps.io:999/hello](https://cfapps.io:999/hello) \n"); + editor.assertHoverContains("@RequestMapping(\"/hello\")", "Count: 25 | Total Time: 3.034567s | Max Time: 0.5535632s"); + editor.assertHoverContains("@RequestMapping(\"/hello\")", "Process [PID=76543, name=`test-request-mapping-live-hover`]"); - editor.assertHoverContains("@RequestMapping(\"/goodbye\")", "Count: 25 | Total Time: 3.0 | Max Time: 0.55"); + editor.assertHoverContains("@RequestMapping(\"/goodbye\")", "Count: 34 | Total Time: 5.43673s | Max Time: 0.7437624s"); + + } + + @Test + public void testLiveCodeLensMetricsSection() throws Exception { + + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = directory.toPath().resolve("src/main/java/example/RestApi.java").toUri() + .toString(); + + + // Build a mock running boot app + SpringProcessLiveData liveData = new SpringProcessLiveDataBuilder() + .port("999") + .processID("76543") + .urlScheme("https") + .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 + .requestMappingsJson( + "{\"/webjars/**\":{\"bean\":\"resourceHandlerMapping\"},\"/**\":{\"bean\":\"resourceHandlerMapping\"},\"/**/favicon.ico\":{\"bean\":\"faviconHandlerMapping\"},\"{[/hello-world],methods=[GET]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public example.Greeting example.HelloWorldController.sayHello(java.lang.String)\"},\"{[/goodbye]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.goodbye()\"},\"{[/hello]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public java.lang.String example.RestApi.hello()\"},\"{[/error]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)\"},\"{[/error],produces=[text/html]}\":{\"bean\":\"requestMappingHandlerMapping\",\"method\":\"public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)\"}}") + .liveMetrics(new LiveMetricsModel() { + + @Override + public RequestMappingMetrics getRequestMappingMetrics(String[] paths, String[] requestMethods) { + return new RequestMappingMetrics() { + + @Override + public double getTotalTime() { + return "/hello".equals(paths[0]) ? 3.034567 : 5.43673; + } + + @Override + public TimeUnit getTimeUnit() { + return TimeUnit.SECONDS; + } + + @Override + public double getMaxTime() { + return "/hello".equals(paths[0]) ? 0.5535632 : 0.7437624; + } + + @Override + public long getCallsCount() { + return "/hello".equals(paths[0]) ? 25 : 34; + } + }; + } + }) + .build(); + liveDataProvider.add("processkey", liveData); + + harness.intialize(directory); + + Editor editor = harness.newEditorFromFileUri(docUri, LanguageId.JAVA); + editor.assertHighlights("@RequestMapping(\"/hello\")", "@RequestMapping(\"/goodbye\")"); + + editor.assertLiveCodeLensContains("@RequestMapping(\"/hello\")", "Count=25 Total=3.03s Max=0.55s"); + editor.assertLiveCodeLensContains("@RequestMapping(\"/goodbye\")", "Count=34 Total=5.44s Max=0.74s"); }