Support for changed requestmappings actuator format in Boot 2.0

This commit is contained in:
Kris De Volder
2018-01-23 11:37:33 -08:00
parent f5f79065af
commit 1b1b64dea5
14 changed files with 368 additions and 205 deletions

View File

@@ -106,7 +106,7 @@ public class MockRunningAppProvider {
}
public MockAppBuilder requestMappings(String mappings) throws Exception {
Collection<RequestMapping> requestMappings = SpringBootApp.parseRequestMappingsJson(mappings);
Collection<RequestMapping> requestMappings = SpringBootApp.parseRequestMappingsJson(mappings, "1.x");
when(app.getRequestMappings()).thenReturn(requestMappings);
return this;
}

View File

@@ -33,7 +33,8 @@ import org.json.JSONArray;
import org.json.JSONObject;
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.boot.app.cli.requestmappings.RequestMappingImpl1;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.Boot1xRequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingsParser20;
import org.springframework.ide.vscode.commons.util.CollectorUtil;
import org.springframework.ide.vscode.commons.util.Log;
@@ -195,29 +196,35 @@ public class SpringBootApp {
}
}
public static Collection<RequestMapping> parseRequestMappingsJson(String json) {
public static Collection<RequestMapping> parseRequestMappingsJson(String json, String bootVersion) {
JSONObject obj = new JSONObject(json);
Iterator<String> keys = obj.keys();
List<RequestMapping> result = new ArrayList<>();
while (keys.hasNext()) {
String rawKey = keys.next();
JSONObject value = obj.getJSONObject(rawKey);
result.add(new RequestMappingImpl1(rawKey, value));
if (bootVersion.equals("2.x")) {
return RequestMappingsParser20.parse(obj);
} else { //1.x
List<RequestMapping> result = new ArrayList<>();
Iterator<String> keys = obj.keys();
while (keys.hasNext()) {
String rawKey = keys.next();
JSONObject value = obj.getJSONObject(rawKey);
result.add(new Boot1xRequestMapping(rawKey, value));
}
return result;
}
return result;
}
public Collection<RequestMapping> getRequestMappings() throws Exception {
//Boot 1.x
Object result = getActuatorDataFromAttribute("org.springframework.boot:type=Endpoint,name=requestMappingEndpoint", "Data");
if (result != null) {
String mappings = new ObjectMapper().writeValueAsString(result);
return parseRequestMappingsJson(mappings);
return parseRequestMappingsJson(mappings, "1.x");
}
//Boot 2.x
result = getActuatorDataFromOperation("org.springframework.boot:type=Endpoint,name=Mappings", "mappings");
if (result != null) {
String mappings = new ObjectMapper().writeValueAsString(result);
return parseRequestMappingsJson(mappings);
return parseRequestMappingsJson(mappings, "2.x");
}
return null;

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.boot.app.cli.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.springframework.ide.vscode.commons.java.parser.JLRMethodParser;
import org.springframework.ide.vscode.commons.java.parser.JLRMethodParser.JLRMethod;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
public abstract class AbstractRequestMapping implements RequestMapping {
private static final Pattern REQUEST_METHODS_PATTERN = Pattern.compile(".*methods=\\[(.*)\\].*");
final private Supplier<JLRMethod> methodDataSupplier;
final private Supplier<Set<String>> requestMethodsSupplier;
final private Supplier<String[]> pathsSuplier;
public AbstractRequestMapping() {
this.requestMethodsSupplier = Suppliers.memoize(() -> parseRequestMethods());
this.methodDataSupplier = Suppliers.memoize(() -> JLRMethodParser.parse(getMethodString()));
this.pathsSuplier = Suppliers.memoize(() -> computePaths());
}
protected Set<String> parseRequestMethods() {
Matcher matcher = REQUEST_METHODS_PATTERN.matcher(getPredicateString());
if (matcher.matches()) {
return Arrays.stream(matcher.group(1).split("\\s*,\\s*")).collect(Collectors.toSet());
}
return Collections.emptySet();
}
@Override
public Set<String> getRequestMethods() {
return requestMethodsSupplier.get();
}
@Override
public final String getFullyQualifiedClassName() {
JLRMethod m = getMethodData();
if (m!=null) {
return m.getFQClassName();
}
return null;
}
protected JLRMethod getMethodData() {
return methodDataSupplier.get();
}
@Override
public final String getMethodName() {
JLRMethod m = getMethodData();
if (m!=null) {
return m.getMethodName();
}
return null;
}
@Override
public String[] getMethodParameters() {
return getMethodData().getParameters();
}
protected String[] computePaths() {
//Two cases we know about:
// 1: the 'predicate' is a path string
// 2: the 'predicate' looks something like:
// "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}
String predicate = getPredicateString();
if (predicate.startsWith("{[")) {
//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 = predicate.indexOf(']');
if (end>=2) {
String pathString = predicate.substring(start, end);
return ParseUtil.splitPaths(pathString);
}
}
//Case 1, or some unanticipated stuff.
//Assume the key is the paths strk g, which is right for Case 1
// and probably more useful than null for 'unanticipated stuff'.
return ParseUtil.splitPaths(predicate);
}
protected abstract String getPredicateString();
@Override
public String[] getSplitPath() {
return pathsSuplier.get();
}
}

View File

@@ -0,0 +1,70 @@
/*******************************************************************************
* 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 org.json.JSONObject;
import com.google.common.base.Objects;
public class Boot1xRequestMapping extends AbstractRequestMapping {
/*
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;
public Boot1xRequestMapping(String pathKey, JSONObject beanInfo) {
this.pathKey = pathKey;
this.beanInfo = beanInfo;
}
@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;
Boot1xRequestMapping other = (Boot1xRequestMapping) obj;
return Objects.equal(this.pathKey, other.pathKey)
&& Objects.equal(this.getMethodString(), other.getMethodString());
}
@Override
public String getMethodString() {
return beanInfo.optString("method");
}
@Override
protected String getPredicateString() {
return pathKey;
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.boot.app.cli.requestmappings;
import org.json.JSONObject;
public class Boot20DispatcherServletMapping extends AbstractRequestMapping {
/*
Some example entries:
[
{
"handler":"ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@7a63363d]]",
"predicate":"/** /favicon.ico"
},
{
"handler":"public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)",
"predicate":"{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"
},
{
"handler":"public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)",
"predicate":"{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"
},
{
"handler":"protected java.util.Map<java.lang.String, java.util.Map<java.lang.String, org.springframework.boot.actuate.endpoint.web.Link>> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)",
"predicate":"{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}"
},
{
"handler":"public com.example.SomeData com.example.ActuatorClientTestSubjectApplication.getMethodName(java.lang.String)",
"predicate":"{[/path],methods=[GET]}"
},
{
"handler":"public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)",
"predicate":"{[/error],produces=[text/html]}"
},
{
"handler":"public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)",
"predicate":"{[/error]}"
},
{
"handler":"ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/webjars/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@78837bf]]",
"predicate":"/webjars/**"
},
{
"handler":"ResourceHttpRequestHandler [locations=[class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/]], resolvers=[org.springframework.web.servlet.resource.PathResourceResolver@64b93bad]]",
"predicate":"/**"
}
]
*/
private JSONObject data;
public Boot20DispatcherServletMapping(JSONObject data) {
this.data = data;
}
@Override
public String getMethodString() {
return data.optString("handler");
}
@Override
protected String getPredicateString() {
return data.optString("predicate");
}
}

View File

@@ -0,0 +1,31 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.boot.app.cli.requestmappings;
import java.util.Arrays;
public class ParseUtil {
public static String[] splitPaths(String paths) {
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);
}
}

View File

@@ -13,7 +13,7 @@ package org.springframework.ide.vscode.commons.boot.app.cli.requestmappings;
import java.util.Set;
public interface RequestMapping {
String getPath();
// String getPath(); commented... because... not used??
String[] getSplitPath();
String getFullyQualifiedClassName();
String getMethodName();

View File

@@ -1,180 +0,0 @@
/*******************************************************************************
* 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<JLRMethod> methodDataSupplier;
private Supplier<Set<String>> requestMethodsSupplier;
private Supplier<String> 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<String> 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<String> 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();
}
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.boot.app.cli.requestmappings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
public class RequestMappingsParser20 {
public static Collection<RequestMapping> parse(JSONObject obj) {
obj = obj.getJSONObject("contexts");
List<RequestMapping> result = new ArrayList<>();
for (String contextId : obj.keySet()) {
//Contains 3 different keys now ('dispatcherServlets', 'servletFilters' and 'servlets'.
// Each with their own kind of data inside. Looks like 'dispatcherServlets' contains stuff similar to what we
// know from Boot 1.x but in slighly different form. We only parse that stuff for now.
JSONObject dispatcherServlets = obj
.getJSONObject(contextId)
.getJSONObject("mappings")
.getJSONObject("dispatcherServlets");
for (String servletId : dispatcherServlets.keySet()) {
JSONArray servlets = dispatcherServlets.getJSONArray(servletId);
for (Object _servlet : servlets) {
JSONObject servlet = (JSONObject)_servlet;
result.add(new Boot20DispatcherServletMapping(servlet));
}
}
}
return result;
}
}

View File

@@ -13,7 +13,8 @@ package org.springframework.ide.vscode.commons.boot.app.cli;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingImpl1;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.AbstractRequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.Boot1xRequestMapping;
/**
* @author Martin Lippert
@@ -22,7 +23,7 @@ public class RequestMappingImp1Test {
@Test
public void testSplitPathWithoutDuplicate() {
RequestMappingImpl1 rm = new RequestMappingImpl1("/superpath", null);
AbstractRequestMapping rm = new Boot1xRequestMapping("/superpath", null);
String[] splitPath = rm.getSplitPath();
assertEquals(1, splitPath.length);
assertEquals("/superpath", splitPath[0]);
@@ -30,7 +31,7 @@ public class RequestMappingImp1Test {
@Test
public void testSplitPathSimpleCaseWithEmptyOr() {
RequestMappingImpl1 rm = new RequestMappingImpl1("/superpath/mypath || ", null);
AbstractRequestMapping rm = new Boot1xRequestMapping("/superpath/mypath || ", null);
String[] splitPath = rm.getSplitPath();
assertEquals(1, splitPath.length);
assertEquals("/superpath/mypath", splitPath[0]);
@@ -38,7 +39,7 @@ public class RequestMappingImp1Test {
@Test
public void testSplitPathSimpleCase() {
RequestMappingImpl1 rm = new RequestMappingImpl1("{[/superpath/mypath || mypath.json]}", null);
AbstractRequestMapping rm = new Boot1xRequestMapping("{[/superpath/mypath || mypath.json]}", null);
String[] splitPath = rm.getSplitPath();
assertEquals(2, splitPath.length);
assertEquals("/superpath/mypath", splitPath[0]);
@@ -47,7 +48,7 @@ public class RequestMappingImp1Test {
@Test
public void testSplitPathMultipleCases() {
RequestMappingImpl1 rm = new RequestMappingImpl1("{[/superpath/mypath || mypath.json || somethingelse.what]}", null);
AbstractRequestMapping rm = new Boot1xRequestMapping("{[/superpath/mypath || mypath.json || somethingelse.what]}", null);
String[] splitPath = rm.getSplitPath();
assertEquals(3, splitPath.length);
assertEquals("/superpath/mypath", splitPath[0]);

View File

@@ -181,11 +181,15 @@ public class SpringBootAppTest {
@Test
public void getRequestMappings() throws Exception {
for (SpringBootApp testApp : getTestApps()) {
ACondition.waitFor(TIMEOUT, () -> {
Collection<RequestMapping> result = testApp.getRequestMappings();
assertTrue(result != null && !result.isEmpty());
// System.out.println("requestMappings = "+result);
});
try {
ACondition.waitFor(TIMEOUT, () -> {
Collection<RequestMapping> result = testApp.getRequestMappings();
assertTrue(result != null && result.size()>4);
// System.out.println("requestMappings = "+result);
});
} catch (Exception e) {
throw new RuntimeException("Failed for: "+testApp, e);
}
}
}

View File

@@ -151,7 +151,7 @@ public class JLRMethodParser {
}
private static final Set<String> MODIFIERS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
public static final Set<String> MODIFIERS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
"public", "protected", "private", "abstract",
"static", "final", "synchronized", "native", "strictfp"
)));

View File

@@ -50,7 +50,6 @@ public class HtmlBuffer {
public String toString() {
return buffer.toString();
}