Commit c06de245 authored by Andy Wilkinson's avatar Andy Wilkinson

Allow endpoints to be mapped to /

This commit removes the restriction that was added in 4a61e456 to
prevent / from being used as the management context path when the
management context was not using a different port

The management context path can now be set to / irrespective of the
configuration of the management port. To avoid a possible clash
with the application's welcome page or similar, the links "endpoint"
that is mapping to the management context path is disabled when
the management context path is /.

As part of allowing / to be used as the management context path again,
the handling of endpoint mappings and the creation of paths for
individual operations has been consolidated into a new EndpointMapping
class that is used across the three (MVC, WebFlux, and Jersey)
implementations.

See gh-9898
parent 4c0a70ce
......@@ -82,7 +82,6 @@ public class ManagementContextAutoConfiguration {
@Override
public void afterSingletonsInstantiated() {
verifySslConfiguration();
verifyContextPathConfiguration();
if (this.environment instanceof ConfigurableEnvironment) {
addLocalManagementPortPropertyAlias(
(ConfigurableEnvironment) this.environment);
......@@ -97,15 +96,6 @@ public class ManagementContextAutoConfiguration {
+ "server is not listening on a separate port");
}
private void verifyContextPathConfiguration() {
String contextPath = this.environment.getProperty("management.context-path");
if ("".equals(contextPath) || "/".equals(contextPath)) {
throw new IllegalStateException("A management context path of '"
+ contextPath + "' requires the management server to be "
+ "listening on a separate port");
}
}
/**
* Add an alias for 'local.management.port' that actually resolves using
* 'local.server.port'.
......
......@@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.jersey.JerseyEndpointResourceFactory;
import org.springframework.boot.endpoint.web.mvc.WebEndpointServletHandlerMapping;
......@@ -66,7 +67,8 @@ class WebEndpointInfrastructureManagementContextConfiguration {
ManagementServerProperties managementServerProperties) {
return (resourceConfig) -> resourceConfig.registerResources(new HashSet<>(
new JerseyEndpointResourceFactory().createEndpointResources(
managementServerProperties.getContextPath(),
new EndpointMapping(
managementServerProperties.getContextPath()),
provider.getEndpoints())));
}
......@@ -93,8 +95,8 @@ class WebEndpointInfrastructureManagementContextConfiguration {
CorsEndpointProperties corsProperties,
ManagementServerProperties managementServerProperties) {
WebEndpointServletHandlerMapping handlerMapping = new WebEndpointServletHandlerMapping(
managementServerProperties.getContextPath(), provider.getEndpoints(),
getCorsConfiguration(corsProperties));
new EndpointMapping(managementServerProperties.getContextPath()),
provider.getEndpoints(), getCorsConfiguration(corsProperties));
for (WebEndpointHandlerMappingCustomizer customizer : this.mappingCustomizers) {
customizer.customize(handlerMapping);
}
......@@ -137,7 +139,8 @@ class WebEndpointInfrastructureManagementContextConfiguration {
EndpointProvider<WebEndpointOperation> provider,
ManagementServerProperties managementServerProperties) {
return new WebEndpointReactiveHandlerMapping(
managementServerProperties.getContextPath(), provider.getEndpoints());
new EndpointMapping(managementServerProperties.getContextPath()),
provider.getEndpoints());
}
}
......
......@@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
......@@ -69,8 +70,9 @@ public class CloudFoundryActuatorAutoConfiguration {
EndpointProvider<WebEndpointOperation> provider, Environment environment,
RestTemplateBuilder builder) {
return new CloudFoundryWebEndpointServletHandlerMapping(
"/cloudfoundryapplication", provider.getEndpoints(),
getCorsConfiguration(), getSecurityInterceptor(builder, environment));
new EndpointMapping("/cloudfoundryapplication"),
provider.getEndpoints(), getCorsConfiguration(),
getSecurityInterceptor(builder, environment));
}
private CloudFoundrySecurityInterceptor getSecurityInterceptor(
......
......@@ -35,6 +35,7 @@ import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.ParameterMappingException;
import org.springframework.boot.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.Link;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.WebEndpointResponse;
......@@ -72,11 +73,11 @@ class CloudFoundryWebEndpointServletHandlerMapping
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
CloudFoundryWebEndpointServletHandlerMapping(String endpointPath,
CloudFoundryWebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration,
CloudFoundrySecurityInterceptor securityInterceptor) {
super(endpointPath, webEndpoints, corsConfiguration);
super(endpointMapping, webEndpoints, corsConfiguration);
this.securityInterceptor = securityInterceptor;
}
......
......@@ -79,7 +79,7 @@ public class CloudFoundryActuatorAutoConfigurationTests {
@Test
public void cloudFoundryPlatformActive() throws Exception {
CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping();
assertThat(handlerMapping.getEndpointPath())
assertThat(handlerMapping.getEndpointMapping().getPath())
.isEqualTo("/cloudfoundryapplication");
CorsConfiguration corsConfiguration = (CorsConfiguration) ReflectionTestUtils
.getField(handlerMapping, "corsConfiguration");
......
......@@ -31,6 +31,7 @@ import org.springframework.boot.endpoint.OperationParameterMapper;
import org.springframework.boot.endpoint.ReadOperation;
import org.springframework.boot.endpoint.Selector;
import org.springframework.boot.endpoint.WriteOperation;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
......@@ -196,7 +197,8 @@ public class CloudFoundryMvcWebEndpointIntegrationTests {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new CloudFoundryWebEndpointServletHandlerMapping("/cfApplication",
return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cfApplication"),
webEndpointDiscoverer.discoverEndpoints(), corsConfiguration,
interceptor);
}
......
......@@ -23,6 +23,7 @@ import org.junit.Test;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.OperationType;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.endpoint.web.WebEndpointHttpMethod;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
......@@ -138,7 +139,8 @@ public class RequestMappingEndpointTests {
WebEndpointOperation operation = new WebEndpointOperation(OperationType.READ,
(arguments) -> "Invoked", true, requestPredicate, "test");
WebEndpointServletHandlerMapping mapping = new WebEndpointServletHandlerMapping(
"application", Collections.singleton(new EndpointInfo<>("test", true,
new EndpointMapping("application"),
Collections.singleton(new EndpointInfo<>("test", true,
Collections.singleton(operation))));
mapping.setApplicationContext(new StaticApplicationContext());
mapping.afterPropertiesSet();
......
......@@ -223,14 +223,15 @@ property. For example, the following will disable _all_ endpoints except for `in
[[production-ready-endpoint-hypermedia]]
=== Hypermedia for actuator MVC endpoints
=== Hypermedia for actuator web endpoints
A "`discovery page`" is added with links to all the endpoints. The "`discovery page`" is
available on `/application` by default.
When a custom management context path is configured, the "`discovery page`" will
automatically move from `/application` to the root of the management context. For example,
if the management context path is `/management` then the discovery page will be available
from `/management`.
from `/management`. When the management context path is set to `/` the discovery page
is disabled to prevent the possiblility of a clash with other mappings.
......
/*
* Copyright 2012-2017 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.endpoint.web;
import org.springframework.util.StringUtils;
/**
* A value object for the base mapping for endpoints.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class EndpointMapping {
private final String path;
/**
* Creates a new {@code EndpointMapping} using the given {@code path}.
*
* @param path the path
*/
public EndpointMapping(String path) {
this.path = normalizePath(path);
}
private static String normalizePath(String path) {
if (!StringUtils.hasText(path)) {
return path;
}
String normalizedPath = path;
if (!normalizedPath.startsWith("/")) {
normalizedPath = "/" + normalizedPath;
}
if (normalizedPath.endsWith("/")) {
normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1);
}
return normalizedPath;
}
/**
* Returns the path to which endpoints should be mapped.
* @return the path
*/
public String getPath() {
return this.path;
}
public String createSubPath(String path) {
return this.path + normalizePath(path);
}
}
......@@ -27,6 +27,7 @@ import java.util.function.Function;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
......@@ -41,12 +42,14 @@ import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.ParameterMappingException;
import org.springframework.boot.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.Link;
import org.springframework.boot.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.WebEndpointResponse;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* A factory for creating Jersey {@link Resource Resources} for web endpoint operations.
......@@ -61,25 +64,29 @@ public class JerseyEndpointResourceFactory {
/**
* Creates {@link Resource Resources} for the operations of the given
* {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @return the resources for the operations
*/
public Collection<Resource> createEndpointResources(String endpointPath,
public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints) {
List<Resource> resources = new ArrayList<>();
webEndpoints.stream()
.flatMap((endpointInfo) -> endpointInfo.getOperations().stream())
.map((operation) -> createResource(endpointPath, operation))
.map((operation) -> createResource(endpointMapping, operation))
.forEach(resources::add);
resources.add(createEndpointLinksResource(endpointPath, webEndpoints));
if (StringUtils.hasText(endpointMapping.getPath())) {
resources.add(
createEndpointLinksResource(endpointMapping.getPath(), webEndpoints));
}
return resources;
}
private Resource createResource(String endpointPath, WebEndpointOperation operation) {
private Resource createResource(EndpointMapping endpointMapping,
WebEndpointOperation operation) {
OperationRequestPredicate requestPredicate = operation.getRequestPredicate();
Builder resourceBuilder = Resource.builder()
.path(endpointPath + "/" + requestPredicate.getPath());
.path(endpointMapping.createSubPath(requestPredicate.getPath()));
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
.consumes(toStringArray(requestPredicate.getConsumes()))
.produces(toStringArray(requestPredicate.getProduces()))
......@@ -95,7 +102,7 @@ public class JerseyEndpointResourceFactory {
private Resource createEndpointLinksResource(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints) {
Builder resourceBuilder = Resource.builder().path(endpointPath);
resourceBuilder.addMethod("GET").handledBy(
resourceBuilder.addMethod("GET").produces(MediaType.APPLICATION_JSON).handledBy(
new EndpointLinksInflector(webEndpoints, this.endpointLinksResolver));
return resourceBuilder.build();
}
......
......@@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.util.StringUtils;
......@@ -50,7 +51,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
public abstract class AbstractWebEndpointServletHandlerMapping
extends RequestMappingInfoHandlerMapping implements InitializingBean {
private final String endpointPath;
private final EndpointMapping endpointMapping;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
......@@ -59,25 +60,25 @@ public abstract class AbstractWebEndpointServletHandlerMapping
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints operations
*/
public AbstractWebEndpointServletHandlerMapping(String endpointPath,
public AbstractWebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointPath, collection, null);
this(endpointMapping, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public AbstractWebEndpointServletHandlerMapping(String endpointPath,
public AbstractWebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath;
this.endpointMapping = endpointMapping;
this.webEndpoints = webEndpoints;
this.corsConfiguration = corsConfiguration;
setOrder(-100);
......@@ -87,8 +88,8 @@ public abstract class AbstractWebEndpointServletHandlerMapping
return this.webEndpoints;
}
public String getEndpointPath() {
return this.endpointPath;
public EndpointMapping getEndpointMapping() {
return this.endpointMapping;
}
@Override
......@@ -96,6 +97,12 @@ public abstract class AbstractWebEndpointServletHandlerMapping
this.webEndpoints.stream()
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
if (StringUtils.hasText(this.endpointMapping.getPath())) {
registerLinksRequestMapping();
}
}
private void registerLinksRequestMapping() {
PatternsRequestCondition patterns = patternsRequestConditionForPattern("");
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.GET);
......@@ -130,8 +137,7 @@ public abstract class AbstractWebEndpointServletHandlerMapping
}
private PatternsRequestCondition patternsRequestConditionForPattern(String path) {
String[] patterns = new String[] {
this.endpointPath + (StringUtils.hasText(path) ? "/" + path : "") };
String[] patterns = new String[] { this.endpointMapping.createSubPath(path) };
return new PatternsRequestCondition(patterns, null, null, false, false);
}
......
......@@ -29,6 +29,7 @@ import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.ParameterMappingException;
import org.springframework.boot.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.Link;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.WebEndpointResponse;
......@@ -63,25 +64,25 @@ public class WebEndpointServletHandlerMapping
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints operations
*/
public WebEndpointServletHandlerMapping(String endpointPath,
public WebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointPath, collection, null);
this(endpointMapping, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param endpointMapping the base mapping for all endpoints
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public WebEndpointServletHandlerMapping(String endpointPath,
public WebEndpointServletHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
super(endpointPath, webEndpoints, corsConfiguration);
super(endpointMapping, webEndpoints, corsConfiguration);
setOrder(-100);
}
......
......@@ -33,6 +33,7 @@ import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.OperationType;
import org.springframework.boot.endpoint.ParameterMappingException;
import org.springframework.boot.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.Link;
import org.springframework.boot.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
......@@ -42,6 +43,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
......@@ -80,7 +82,7 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final String endpointPath;
private final EndpointMapping endpointMapping;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
......@@ -89,25 +91,25 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param endpointMapping the base mapping for all endpoints
* @param collection the web endpoints
*/
public WebEndpointReactiveHandlerMapping(String endpointPath,
public WebEndpointReactiveHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointPath, collection, null);
this(endpointMapping, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param endpointMapping the path beneath which all endpoints should be mapped
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuration for the endpoints
*/
public WebEndpointReactiveHandlerMapping(String endpointPath,
public WebEndpointReactiveHandlerMapping(EndpointMapping endpointMapping,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath;
this.endpointMapping = endpointMapping;
this.webEndpoints = webEndpoints;
this.corsConfiguration = corsConfiguration;
setOrder(-100);
......@@ -118,8 +120,15 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler
this.webEndpoints.stream()
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
if (StringUtils.hasText(this.endpointMapping.getPath())) {
registerLinksMapping();
}
}
private void registerLinksMapping() {
registerMapping(new RequestMappingInfo(
new PatternsRequestCondition(pathPatternParser.parse(this.endpointPath)),
new PatternsRequestCondition(
pathPatternParser.parse(this.endpointMapping.getPath())),
new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null,
null, null), this, this.links);
}
......@@ -148,7 +157,7 @@ public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandler
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
PatternsRequestCondition patterns = new PatternsRequestCondition(pathPatternParser
.parse(this.endpointPath + "/" + requestPredicate.getPath()));
.parse(this.endpointMapping.createSubPath(requestPredicate.getPath())));
RequestMethodsRequestCondition methods = new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name()));
ConsumesRequestCondition consumes = new ConsumesRequestCondition(
......
......@@ -26,6 +26,7 @@ import java.util.function.Consumer;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.boot.endpoint.CachingConfiguration;
import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper;
import org.springframework.boot.endpoint.DeleteOperation;
......@@ -40,6 +41,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
......@@ -71,6 +73,14 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
.isEqualTo(true));
}
@Test
public void readOperationWithEndpointsMappedToTheRoot() {
load(TestEndpointConfiguration.class, "",
(client) -> client.get().uri("/test").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody().jsonPath("All")
.isEqualTo(true));
}
@Test
public void readOperationWithSelector() {
load(TestEndpointConfiguration.class,
......@@ -101,6 +111,13 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
.jsonPath("_links.test-part.templated").isEqualTo(true));
}
@Test
public void linksMappingIsDisabledWhenEndpointPathIsEmpty() {
load(TestEndpointConfiguration.class, "",
(client) -> client.get().uri("").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isNotFound());
}
@Test
public void readOperationWithSingleQueryParameters() {
load(QueryEndpointConfiguration.class,
......@@ -268,12 +285,20 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
private void load(Class<?> configuration,
BiConsumer<ApplicationContext, WebTestClient> consumer) {
load(configuration, "/endpoints", consumer);
}
private void load(Class<?> configuration, String endpointPath,
BiConsumer<ApplicationContext, WebTestClient> consumer) {
T context = createApplicationContext(configuration, this.exporterConfiguration);
context.getEnvironment().getPropertySources().addLast(new MapPropertySource(
"test", Collections.singletonMap("endpointPath", endpointPath)));
context.refresh();
try {
consumer.accept(context,
WebTestClient.bindToServer()
.baseUrl(
"http://localhost:" + getPort(context) + "/endpoints")
"http://localhost:" + getPort(context) + endpointPath)
.build());
}
finally {
......@@ -282,7 +307,14 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
}
protected void load(Class<?> configuration, Consumer<WebTestClient> clientConsumer) {
load(configuration, (context, client) -> clientConsumer.accept(client));
load(configuration, "/endpoints",
(context, client) -> clientConsumer.accept(client));
}
protected void load(Class<?> configuration, String endpointPath,
Consumer<WebTestClient> clientConsumer) {
load(configuration, endpointPath,
(context, client) -> clientConsumer.accept(client));
}
@Configuration
......@@ -304,6 +336,11 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
Collections.singletonList("application/json"));
}
@Bean
public PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertyPlaceholderConfigurer();
}
}
@Configuration
......
/*
* Copyright 2012-2017 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.endpoint.web;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link EndpointMapping}.
*
* @author Andy Wilkinson
*/
public class EndpointMappingTests {
@Test
public void normalizationTurnsASlashIntoAnEmptyString() {
assertThat(new EndpointMapping("/").getPath()).isEqualTo("");
}
@Test
public void normalizationLeavesAnEmptyStringAsIs() {
assertThat(new EndpointMapping("").getPath()).isEqualTo("");
}
@Test
public void normalizationRemovesATrailingSlash() {
assertThat(new EndpointMapping("/test/").getPath()).isEqualTo("/test");
}
@Test
public void normalizationAddsALeadingSlash() {
assertThat(new EndpointMapping("test").getPath()).isEqualTo("/test");
}
@Test
public void normalizationAddsALeadingSlashAndRemovesATrailingSlash() {
assertThat(new EndpointMapping("test/").getPath()).isEqualTo("/test");
}
@Test
public void normalizationLeavesAPathWithALeadingSlashAndNoTrailingSlashAsIs() {
assertThat(new EndpointMapping("/test").getPath()).isEqualTo("/test");
}
@Test
public void subPathForAnEmptyStringReturnsBasePath() {
assertThat(new EndpointMapping("/test").createSubPath("")).isEqualTo("/test");
}
@Test
public void subPathWithALeadingSlashIsSeparatedFromBasePathBySingleSlash() {
assertThat(new EndpointMapping("/test").createSubPath("/one"))
.isEqualTo("/test/one");
}
@Test
public void subPathWithoutALeadingSlashIsSeparaedFromBasePathBySingleSlash() {
assertThat(new EndpointMapping("/test").createSubPath("one")).isEqualTo("/test/one");
}
@Test
public void trailingSlashIsRemovedFromASubPath() {
assertThat(new EndpointMapping("/test").createSubPath("one/"))
.isEqualTo("/test/one");
}
}
......@@ -28,12 +28,14 @@ import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.servlet.ServletContainer;
import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* Integration tests for web endpoints exposed using Jersey.
......@@ -50,7 +52,9 @@ public class JerseyWebEndpointIntegrationTests extends
@Override
protected AnnotationConfigServletWebServerApplicationContext createApplicationContext(
Class<?>... config) {
return new AnnotationConfigServletWebServerApplicationContext(config);
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.register(config);
return context;
}
@Override
......@@ -74,11 +78,12 @@ public class JerseyWebEndpointIntegrationTests extends
}
@Bean
public ResourceConfig resourceConfig(
public ResourceConfig resourceConfig(Environment environment,
WebAnnotationEndpointDiscoverer endpointDiscoverer) {
ResourceConfig resourceConfig = new ResourceConfig();
Collection<Resource> resources = new JerseyEndpointResourceFactory()
.createEndpointResources("endpoints",
.createEndpointResources(
new EndpointMapping(environment.getProperty("endpointPath")),
endpointDiscoverer.discoverEndpoints());
resourceConfig.registerResources(new HashSet<>(resources));
resourceConfig.register(JacksonFeature.class);
......
......@@ -21,11 +21,13 @@ import java.util.Arrays;
import org.junit.Test;
import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
......@@ -59,7 +61,9 @@ public class MvcWebEndpointIntegrationTests extends
@Override
protected AnnotationConfigServletWebServerApplicationContext createApplicationContext(
Class<?>... config) {
return new AnnotationConfigServletWebServerApplicationContext(config);
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext();
context.register(config);
return context;
}
@Override
......@@ -83,11 +87,13 @@ public class MvcWebEndpointIntegrationTests extends
@Bean
public WebEndpointServletHandlerMapping webEndpointHandlerMapping(
Environment environment,
WebAnnotationEndpointDiscoverer webEndpointDiscoverer) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new WebEndpointServletHandlerMapping("/endpoints",
return new WebEndpointServletHandlerMapping(
new EndpointMapping(environment.getProperty("endpointPath")),
webEndpointDiscoverer.discoverEndpoints(), corsConfiguration);
}
......
......@@ -21,6 +21,7 @@ import java.util.Arrays;
import org.junit.Test;
import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
......@@ -29,6 +30,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.cors.CorsConfiguration;
......@@ -63,7 +65,9 @@ public class ReactiveWebEndpointIntegrationTests
@Override
protected ReactiveWebServerApplicationContext createApplicationContext(
Class<?>... config) {
return new ReactiveWebServerApplicationContext(config);
ReactiveWebServerApplicationContext context = new ReactiveWebServerApplicationContext();
context.register(config);
return context;
}
@Override
......@@ -89,11 +93,13 @@ public class ReactiveWebEndpointIntegrationTests
@Bean
public WebEndpointReactiveHandlerMapping webEndpointHandlerMapping(
Environment environment,
WebAnnotationEndpointDiscoverer endpointDiscoverer) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new WebEndpointReactiveHandlerMapping("endpoints",
return new WebEndpointReactiveHandlerMapping(
new EndpointMapping(environment.getProperty("endpointPath")),
endpointDiscoverer.discoverEndpoints(), corsConfiguration);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment