From ab81d993e6618dcab51a077e201ebd0911b6e18f Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Tue, 18 Oct 2016 17:39:35 -0700 Subject: [PATCH] Add CloudFoundryDiscoveryMvcEndpoint Update Cloud Foundry support with a discovery endpoint that shows what endpoints are available. See gh-7108 --- ...tWebMvcManagementContextConfiguration.java | 2 +- ...anagementWebSecurityAutoConfiguration.java | 3 +- .../CloudFoundryDiscoveryMvcEndpoint.java | 86 +++++++ .../CloudFoundryEndpointHandlerMapping.java | 24 +- .../mvc/AbstractEndpointHandlerMapping.java | 235 ++++++++++++++++++ .../endpoint/mvc/EndpointHandlerMapping.java | 163 +----------- .../actuate/endpoint/mvc/MvcEndpoints.java | 7 +- ...CloudFoundryDiscoveryMvcEndpointTests.java | 100 ++++++++ ...oudFoundryEndpointHandlerMappingTests.java | 43 ++++ 9 files changed, 495 insertions(+), 168 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpoint.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java create mode 100644 spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpointTests.java diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java index d4752b5441b..d1101af2fa9 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcManagementContextConfiguration.java @@ -79,7 +79,7 @@ public class EndpointWebMvcManagementContextConfiguration { @Bean @ConditionalOnMissingBean public EndpointHandlerMapping endpointHandlerMapping() { - Set endpoints = mvcEndpoints().getEndpoints(); + Set endpoints = mvcEndpoints().getEndpoints(); CorsConfiguration corsConfiguration = getCorsConfiguration(this.corsProperties); EndpointHandlerMapping mapping = new EndpointHandlerMapping(endpoints, corsConfiguration); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java index 58420253b15..609562e412f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementWebSecurityAutoConfiguration.java @@ -406,8 +406,7 @@ public class ManagementWebSecurityAutoConfiguration { EndpointHandlerMapping endpointHandlerMapping = null; ApplicationContext context = this.contextResolver.getApplicationContext(); if (context.getBeanNamesForType(EndpointHandlerMapping.class).length > 0) { - endpointHandlerMapping = context.getBean("endpointHandlerMapping", - EndpointHandlerMapping.class); + endpointHandlerMapping = context.getBean(EndpointHandlerMapping.class); } if (endpointHandlerMapping == null) { // Maybe there are actually no endpoints (e.g. management.port=-1) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpoint.java new file mode 100644 index 00000000000..adb10e79d72 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpoint.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cloudfoundry; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.boot.actuate.endpoint.mvc.AbstractMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * {@link MvcEndpoint} to expose HAL-formatted JSON for Cloud Foundry specific actuator + * endpoints. + * + * @author Madhura Bhave + */ +class CloudFoundryDiscoveryMvcEndpoint extends AbstractMvcEndpoint { + + private final Set endpoints; + + CloudFoundryDiscoveryMvcEndpoint(Set endpoints) { + super("", false); + this.endpoints = endpoints; + } + + @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public Map> links(HttpServletRequest request) { + Map links = new LinkedHashMap(); + String url = request.getRequestURL().toString(); + if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); + } + links.put("self", Link.withHref(url)); + for (NamedMvcEndpoint endpoint : this.endpoints) { + links.put(endpoint.getName(), Link.withHref(url + "/" + endpoint.getName())); + } + return Collections.singletonMap("_links", links); + } + + /** + * Details for a link in the HAL response. + */ + static class Link { + + private String href; + + public String getHref() { + return this.href; + } + + public void setHref(String href) { + this.href = href; + } + + static Link withHref(Object href) { + Link link = new Link(); + link.setHref(href.toString()); + return link; + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java index fa9f7c3cc25..efe524a8649 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMapping.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.cloudfoundry; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Set; @@ -26,7 +27,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; +import org.springframework.boot.actuate.endpoint.mvc.AbstractEndpointHandlerMapping; +import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint; import org.springframework.web.cors.CorsConfiguration; @@ -40,7 +42,8 @@ import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; * * @author Madhura Bhave */ -class CloudFoundryEndpointHandlerMapping extends EndpointHandlerMapping { +class CloudFoundryEndpointHandlerMapping + extends AbstractEndpointHandlerMapping { CloudFoundryEndpointHandlerMapping(Collection endpoints) { super(endpoints); @@ -51,6 +54,23 @@ class CloudFoundryEndpointHandlerMapping extends EndpointHandlerMapping { super(endpoints, corsConfiguration); } + @Override + protected void postProcessEndpoints(Set endpoints) { + super.postProcessEndpoints(endpoints); + Iterator iterator = endpoints.iterator(); + while (iterator.hasNext()) { + if (iterator.next() instanceof HalJsonMvcEndpoint) { + iterator.remove(); + } + } + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + detectHandlerMethods(new CloudFoundryDiscoveryMvcEndpoint(getEndpoints())); + } + @Override protected String getPath(MvcEndpoint endpoint) { if (endpoint instanceof NamedMvcEndpoint) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java new file mode 100644 index 00000000000..ef562cda332 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AbstractEndpointHandlerMapping.java @@ -0,0 +1,235 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.endpoint.mvc; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.Endpoint; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +/** + * {@link HandlerMapping} to map {@link Endpoint}s to URLs via {@link Endpoint#getId()}. + * The semantics of {@code @RequestMapping} should be identical to a normal + * {@code @Controller}, but the endpoints should not be annotated as {@code @Controller} + * (otherwise they will be mapped by the normal MVC mechanisms). + *

+ * One of the aims of the mapping is to support endpoints that work as HTTP endpoints but + * can still provide useful service interfaces when there is no HTTP server (and no Spring + * MVC on the classpath). Note that any endpoints having method signatures will break in a + * non-servlet environment. + * + * @param The endpoint type + * @author Phillip Webb + * @author Christian Dupuis + * @author Dave Syer + * @author Madhura Bhave + */ +public class AbstractEndpointHandlerMapping + extends RequestMappingHandlerMapping { + + private final Set endpoints; + + private final CorsConfiguration corsConfiguration; + + private String prefix = ""; + + private boolean disabled = false; + + /** + * Create a new {@link AbstractEndpointHandlerMapping} instance. All {@link Endpoint}s + * will be detected from the {@link ApplicationContext}. The endpoints will not accept + * CORS requests. + * @param endpoints the endpoints + */ + public AbstractEndpointHandlerMapping(Collection endpoints) { + this(endpoints, null); + } + + /** + * Create a new {@link AbstractEndpointHandlerMapping} instance. All {@link Endpoint}s + * will be detected from the {@link ApplicationContext}. The endpoints will accepts + * CORS requests based on the given {@code corsConfiguration}. + * @param endpoints the endpoints + * @param corsConfiguration the CORS configuration for the endpoints + * @since 1.3.0 + */ + public AbstractEndpointHandlerMapping(Collection endpoints, + CorsConfiguration corsConfiguration) { + this.endpoints = new HashSet(endpoints); + postProcessEndpoints(this.endpoints); + this.corsConfiguration = corsConfiguration; + // By default the static resource handler mapping is LOWEST_PRECEDENCE - 1 + // and the RequestMappingHandlerMapping is 0 (we ideally want to be before both) + setOrder(-100); + setUseSuffixPatternMatch(false); + } + + /** + * Post process the endpoint setting before they are used. Subclasses can add or + * modify the endpoints as necessary. + * @param endpoints the endpoints to post process + */ + protected void postProcessEndpoints(Set endpoints) { + } + + @Override + public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (!this.disabled) { + for (MvcEndpoint endpoint : this.endpoints) { + detectHandlerMethods(endpoint); + } + } + } + + /** + * Since all handler beans are passed into the constructor there is no need to detect + * anything here. + */ + @Override + protected boolean isHandler(Class beanType) { + return false; + } + + @Override + @Deprecated + protected void registerHandlerMethod(Object handler, Method method, + RequestMappingInfo mapping) { + if (mapping == null) { + return; + } + String[] patterns = getPatterns(handler, mapping); + if (!ObjectUtils.isEmpty(patterns)) { + super.registerHandlerMethod(handler, method, + withNewPatterns(mapping, patterns)); + } + } + + private String[] getPatterns(Object handler, RequestMappingInfo mapping) { + if (handler instanceof String) { + handler = getApplicationContext().getBean((String) handler); + } + Assert.state(handler instanceof MvcEndpoint, "Only MvcEndpoints are supported"); + String path = getPath((MvcEndpoint) handler); + return (path == null ? null : getEndpointPatterns(path, mapping)); + } + + /** + * Return the path that should be used to map the given {@link MvcEndpoint}. + * @param endpoint the endpoint to map + * @return the path to use for the endpoint or {@code null} if no mapping is required + */ + protected String getPath(MvcEndpoint endpoint) { + return endpoint.getPath(); + } + + private String[] getEndpointPatterns(String path, RequestMappingInfo mapping) { + String patternPrefix = StringUtils.hasText(this.prefix) ? this.prefix + path + : path; + Set defaultPatterns = mapping.getPatternsCondition().getPatterns(); + if (defaultPatterns.isEmpty()) { + return new String[] { patternPrefix, patternPrefix + ".json" }; + } + List patterns = new ArrayList(defaultPatterns); + for (int i = 0; i < patterns.size(); i++) { + patterns.set(i, patternPrefix + patterns.get(i)); + } + return patterns.toArray(new String[patterns.size()]); + } + + private RequestMappingInfo withNewPatterns(RequestMappingInfo mapping, + String[] patternStrings) { + PatternsRequestCondition patterns = new PatternsRequestCondition(patternStrings, + null, null, useSuffixPatternMatch(), useTrailingSlashMatch(), null); + return new RequestMappingInfo(patterns, mapping.getMethodsCondition(), + mapping.getParamsCondition(), mapping.getHeadersCondition(), + mapping.getConsumesCondition(), mapping.getProducesCondition(), + mapping.getCustomCondition()); + } + + /** + * Set the prefix used in mappings. + * @param prefix the prefix + */ + public void setPrefix(String prefix) { + Assert.isTrue("".equals(prefix) || StringUtils.startsWithIgnoreCase(prefix, "/"), + "prefix must start with '/'"); + this.prefix = prefix; + } + + /** + * Get the prefix used in mappings. + * @return the prefix + */ + public String getPrefix() { + return this.prefix; + } + + /** + * Get the path of the endpoint. + * @param endpoint the endpoint + * @return the path used in mappings + */ + public String getPath(String endpoint) { + return this.prefix + endpoint; + } + + /** + * Sets if this mapping is disabled. + * @param disabled if the mapping is disabled + */ + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + /** + * Returns if this mapping is disabled. + * @return if the mapping is disabled + */ + public boolean isDisabled() { + return this.disabled; + } + + /** + * Return the endpoints. + * @return the endpoints + */ + public Set getEndpoints() { + return Collections.unmodifiableSet(this.endpoints); + } + + @Override + protected CorsConfiguration initCorsConfiguration(Object handler, Method method, + RequestMappingInfo mappingInfo) { + return this.corsConfiguration; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java index ca5b277ea43..a6682560653 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointHandlerMapping.java @@ -16,24 +16,12 @@ package org.springframework.boot.actuate.endpoint.mvc; -import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.context.ApplicationContext; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; -import org.springframework.web.servlet.mvc.method.RequestMappingInfo; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; /** * {@link HandlerMapping} to map {@link Endpoint}s to URLs via {@link Endpoint#getId()}. @@ -50,15 +38,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl * @author Christian Dupuis * @author Dave Syer */ -public class EndpointHandlerMapping extends RequestMappingHandlerMapping { - - private final Set endpoints; - - private final CorsConfiguration corsConfiguration; - - private String prefix = ""; - - private boolean disabled = false; +public class EndpointHandlerMapping extends AbstractEndpointHandlerMapping { /** * Create a new {@link EndpointHandlerMapping} instance. All {@link Endpoint}s will be @@ -67,7 +47,7 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping { * @param endpoints the endpoints */ public EndpointHandlerMapping(Collection endpoints) { - this(endpoints, null); + super(endpoints); } /** @@ -80,144 +60,7 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping { */ public EndpointHandlerMapping(Collection endpoints, CorsConfiguration corsConfiguration) { - this.endpoints = new HashSet(endpoints); - this.corsConfiguration = corsConfiguration; - // By default the static resource handler mapping is LOWEST_PRECEDENCE - 1 - // and the RequestMappingHandlerMapping is 0 (we ideally want to be before both) - setOrder(-100); - setUseSuffixPatternMatch(false); - } - - @Override - public void afterPropertiesSet() { - super.afterPropertiesSet(); - if (!this.disabled) { - for (MvcEndpoint endpoint : this.endpoints) { - detectHandlerMethods(endpoint); - } - } - } - - /** - * Since all handler beans are passed into the constructor there is no need to detect - * anything here. - */ - @Override - protected boolean isHandler(Class beanType) { - return false; - } - - @Override - @Deprecated - protected void registerHandlerMethod(Object handler, Method method, - RequestMappingInfo mapping) { - if (mapping == null) { - return; - } - String[] patterns = getPatterns(handler, mapping); - if (!ObjectUtils.isEmpty(patterns)) { - super.registerHandlerMethod(handler, method, - withNewPatterns(mapping, patterns)); - } - } - - private String[] getPatterns(Object handler, RequestMappingInfo mapping) { - if (handler instanceof String) { - handler = getApplicationContext().getBean((String) handler); - } - Assert.state(handler instanceof MvcEndpoint, "Only MvcEndpoints are supported"); - String path = getPath((MvcEndpoint) handler); - return (path == null ? null : getEndpointPatterns(path, mapping)); - } - - /** - * Return the path that should be used to map the given {@link MvcEndpoint}. - * @param endpoint the endpoint to map - * @return the path to use for the endpoint or {@code null} if no mapping is required - */ - protected String getPath(MvcEndpoint endpoint) { - return endpoint.getPath(); - } - - private String[] getEndpointPatterns(String path, RequestMappingInfo mapping) { - String patternPrefix = StringUtils.hasText(this.prefix) ? this.prefix + path - : path; - Set defaultPatterns = mapping.getPatternsCondition().getPatterns(); - if (defaultPatterns.isEmpty()) { - return new String[] { patternPrefix, patternPrefix + ".json" }; - } - List patterns = new ArrayList(defaultPatterns); - for (int i = 0; i < patterns.size(); i++) { - patterns.set(i, patternPrefix + patterns.get(i)); - } - return patterns.toArray(new String[patterns.size()]); - } - - private RequestMappingInfo withNewPatterns(RequestMappingInfo mapping, - String[] patternStrings) { - PatternsRequestCondition patterns = new PatternsRequestCondition(patternStrings, - null, null, useSuffixPatternMatch(), useTrailingSlashMatch(), null); - return new RequestMappingInfo(patterns, mapping.getMethodsCondition(), - mapping.getParamsCondition(), mapping.getHeadersCondition(), - mapping.getConsumesCondition(), mapping.getProducesCondition(), - mapping.getCustomCondition()); - } - - /** - * Set the prefix used in mappings. - * @param prefix the prefix - */ - public void setPrefix(String prefix) { - Assert.isTrue("".equals(prefix) || StringUtils.startsWithIgnoreCase(prefix, "/"), - "prefix must start with '/'"); - this.prefix = prefix; - } - - /** - * Get the prefix used in mappings. - * @return the prefix - */ - public String getPrefix() { - return this.prefix; - } - - /** - * Get the path of the endpoint. - * @param endpoint the endpoint - * @return the path used in mappings - */ - public String getPath(String endpoint) { - return this.prefix + endpoint; - } - - /** - * Sets if this mapping is disabled. - * @param disabled if the mapping is disabled - */ - public void setDisabled(boolean disabled) { - this.disabled = disabled; - } - - /** - * Returns if this mapping is disabled. - * @return if the mapping is disabled - */ - public boolean isDisabled() { - return this.disabled; - } - - /** - * Return the endpoints. - * @return the endpoints - */ - public Set getEndpoints() { - return Collections.unmodifiableSet(this.endpoints); - } - - @Override - protected CorsConfiguration initCorsConfiguration(Object handler, Method method, - RequestMappingInfo mappingInfo) { - return this.corsConfiguration; + super(endpoints, corsConfiguration); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java index 8d60920d627..f878062b934 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpoints.java @@ -89,19 +89,20 @@ public class MvcEndpoints implements ApplicationContextAware, InitializingBean { return types; } - public Set getEndpoints() { + public Set getEndpoints() { return this.endpoints; } /** * Return the endpoints of the specified type. - * @param the Class type of the endpoints to be returned + * @param the Class type of the endpoints to be returned * @param type the endpoint type * @return the endpoints */ + @SuppressWarnings("unchecked") public Set getEndpoints(Class type) { Set result = new HashSet(this.endpoints.size()); - for (MvcEndpoint candidate: this.endpoints) { + for (MvcEndpoint candidate : this.endpoints) { if (type.isInstance(candidate)) { result.add((E) candidate); } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpointTests.java new file mode 100644 index 00000000000..1795d82c367 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryDiscoveryMvcEndpointTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.cloudfoundry; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.boot.actuate.endpoint.AbstractEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; +import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint; +import org.springframework.mock.web.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CloudFoundryDiscoveryMvcEndpoint}. + * + * @author Madhura Bhave + */ +public class CloudFoundryDiscoveryMvcEndpointTests { + + private CloudFoundryDiscoveryMvcEndpoint endpoint; + + @Before + public void setup() { + NamedMvcEndpoint testMvcEndpoint = new TestMvcEndpoint(new TestEndpoint("a")); + Set endpoints = new LinkedHashSet(); + endpoints.add(testMvcEndpoint); + this.endpoint = new CloudFoundryDiscoveryMvcEndpoint(endpoints); + } + + @Test + public void cloudfoundryHalJsonEndpointHasEmptyPath() throws Exception { + assertThat(this.endpoint.getPath()).isEmpty(); + } + + @Test + public void linksResponseWhenRequestUriHasNoTrailingSlash() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", + "/cloudfoundryapplication"); + Map links = this.endpoint + .links(request).get("_links"); + assertThat(links.get("self").getHref()) + .isEqualTo("http://localhost/cloudfoundryapplication"); + assertThat(links.get("a").getHref()) + .isEqualTo("http://localhost/cloudfoundryapplication/a"); + } + + @Test + public void linksResponseWhenRequestUriHasTrailingSlash() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", + "/cloudfoundryapplication/"); + Map links = this.endpoint + .links(request).get("_links"); + assertThat(links.get("self").getHref()) + .isEqualTo("http://localhost/cloudfoundryapplication"); + assertThat(links.get("a").getHref()) + .isEqualTo("http://localhost/cloudfoundryapplication/a"); + } + + private static class TestEndpoint extends AbstractEndpoint { + + TestEndpoint(String id) { + super(id); + } + + @Override + public Object invoke() { + return null; + } + + } + + private static class TestMvcEndpoint extends EndpointMvcAdapter { + + TestMvcEndpoint(TestEndpoint delegate) { + super(delegate); + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java index 0cd5f6e307b..7df7d3b9ef0 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cloudfoundry/CloudFoundryEndpointHandlerMappingTests.java @@ -17,12 +17,18 @@ package org.springframework.boot.actuate.cloudfoundry; import java.util.Arrays; +import java.util.Collections; import org.junit.Test; import org.springframework.boot.actuate.endpoint.AbstractEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter; +import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.ManagementServletContext; +import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint; +import org.springframework.context.support.StaticApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; @@ -57,6 +63,28 @@ public class CloudFoundryEndpointHandlerMappingTests { assertThat(handlerMapping.getPath(testMvcEndpoint)).isEqualTo("/a"); } + @Test + public void doesNotRegisterHalJsonMvcEndpoint() throws Exception { + CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping( + Collections.singleton(new TestHalJsonMvcEndpoint())); + assertThat(handlerMapping.getEndpoints()).hasSize(0); + } + + @Test + public void registersCloudFoundryDiscoveryEndpoint() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping( + Collections.emptyList()); + handlerMapping.setPrefix("/test"); + handlerMapping.setApplicationContext(context); + handlerMapping.afterPropertiesSet(); + HandlerExecutionChain handler = handlerMapping + .getHandler(new MockHttpServletRequest("GET", "/test")); + HandlerMethod handlerMethod = (HandlerMethod) handler.getHandler(); + assertThat(handlerMethod.getBean()) + .isInstanceOf(CloudFoundryDiscoveryMvcEndpoint.class); + } + private static class TestEndpoint extends AbstractEndpoint { TestEndpoint(String id) { @@ -78,4 +106,19 @@ public class CloudFoundryEndpointHandlerMappingTests { } + private static class TestHalJsonMvcEndpoint extends HalJsonMvcEndpoint { + + TestHalJsonMvcEndpoint() { + super(new ManagementServletContext() { + + @Override + public String getContextPath() { + return ""; + } + + }); + + } + } + }