Commit 56afc253 authored by Stephane Nicoll's avatar Stephane Nicoll

Allow to customize the path of a web endpoint

This commit introduces a endpoints.<id>.web.path generic property that
allows to customize the path of an endpoint. By default the path is the
same as the id of the endpoint.

Such customization does not apply for the CloudFoundry specific
endpoints.

Closes gh-10181
parent 622e65a2
......@@ -82,7 +82,7 @@ public class CloudFoundryActuatorAutoConfiguration {
RestTemplateBuilder builder) {
WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext, parameterMapper, cachingConfigurationFactory,
endpointMediaTypes);
endpointMediaTypes, (id) -> id);
return new CloudFoundryWebEndpointServletHandlerMapping(
new EndpointMapping("/cloudfoundryapplication"),
endpointDiscoverer.discoverEndpoints(), endpointMediaTypes,
......
/*
* 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.actuate.autoconfigure.endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.core.env.Environment;
/**
* Default {@link EndpointPathResolver} implementation that use the
* {@link Environment} to determine if an endpoint has a custom path.
*
* @author Stephane Nicoll
*/
class DefaultEndpointPathResolver implements EndpointPathResolver {
private final Environment environment;
DefaultEndpointPathResolver(Environment environment) {
this.environment = environment;
}
@Override
public String resolvePath(String endpointId) {
String key = String.format("endpoints.%s.web.path", endpointId);
return this.environment.getProperty(key, String.class, endpointId);
}
}
......@@ -26,6 +26,7 @@ import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFacto
import org.springframework.boot.actuate.endpoint.convert.ConversionServiceOperationParameterMapper;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
......@@ -77,14 +78,22 @@ public class EndpointAutoConfiguration {
return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES);
}
@Bean
@ConditionalOnMissingBean
public EndpointPathResolver endpointPathResolver(
Environment environment) {
return new DefaultEndpointPathResolver(environment);
}
@Bean
public EndpointProvider<WebEndpointOperation> webEndpointProvider(
OperationParameterMapper parameterMapper,
DefaultCachingConfigurationFactory cachingConfigurationFactory) {
DefaultCachingConfigurationFactory cachingConfigurationFactory,
EndpointPathResolver endpointPathResolver) {
Environment environment = this.applicationContext.getEnvironment();
WebAnnotationEndpointDiscoverer endpointDiscoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext, parameterMapper, cachingConfigurationFactory,
endpointMediaTypes());
endpointMediaTypes(), endpointPathResolver);
return new EndpointProvider<>(environment, endpointDiscoverer,
EndpointExposure.WEB);
}
......
......@@ -219,6 +219,22 @@ public class CloudFoundryActuatorAutoConfigurationTests {
assertThat(endpoints.get(0).getId()).isEqualTo("test");
}
@Test
public void endpointPathCustomizationIsNotApplied()
throws Exception {
TestPropertyValues.of("endpoints.test.web.path=another/custom")
.applyTo(this.context);
this.context.register(TestConfiguration.class);
this.context.refresh();
CloudFoundryWebEndpointServletHandlerMapping handlerMapping = getHandlerMapping();
List<EndpointInfo<WebEndpointOperation>> endpoints = (List<EndpointInfo<WebEndpointOperation>>) handlerMapping
.getEndpoints();
assertThat(endpoints.size()).isEqualTo(1);
assertThat(endpoints.get(0).getOperations()).hasSize(1);
assertThat(endpoints.get(0).getOperations().iterator().next()
.getRequestPredicate().getPath()).isEqualTo("test");
}
private CloudFoundryWebEndpointServletHandlerMapping getHandlerMapping() {
TestPropertyValues
.of("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
......
......@@ -219,7 +219,7 @@ public class CloudFoundryMvcWebEndpointIntegrationTests {
DefaultConversionService.getSharedInstance());
return new WebAnnotationEndpointDiscoverer(applicationContext,
parameterMapper, (id) -> new CachingConfiguration(0),
endpointMediaTypes);
endpointMediaTypes, (id) -> id);
}
@Bean
......
/*
* 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.actuate.autoconfigure.endpoint;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultEndpointPathResolver}.
*
* @author Stephane Nicoll
*/
public class DefaultEndpointPathResolverTests {
private final MockEnvironment environment = new MockEnvironment();
private final EndpointPathResolver resolver = new DefaultEndpointPathResolver(
this.environment);
@Test
public void defaultConfiguration() {
assertThat(this.resolver.resolvePath("test")).isEqualTo("test");
}
@Test
public void userConfiguration() {
this.environment.setProperty("endpoints.test.web.path", "custom");
assertThat(this.resolver.resolvePath("test")).isEqualTo("custom");
}
}
/*
* 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.actuate.endpoint.web;
/**
* Resolve the path of an endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface EndpointPathResolver {
/**
* Resolve the path for the endpoint with the specified {@code endpointId}.
* @param endpointId the id of an endpoint
* @return the path of the endpoint
*/
String resolvePath(String endpointId);
}
......@@ -40,6 +40,7 @@ import org.springframework.boot.actuate.endpoint.cache.CachingConfiguration;
import org.springframework.boot.actuate.endpoint.cache.CachingConfigurationFactory;
import org.springframework.boot.actuate.endpoint.cache.CachingOperationInvoker;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.EndpointPathResolver;
import org.springframework.boot.actuate.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebEndpointHttpMethod;
import org.springframework.boot.actuate.endpoint.web.WebEndpointOperation;
......@@ -71,14 +72,17 @@ public class WebAnnotationEndpointDiscoverer extends
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
* @param endpointMediaTypes the media types produced and consumed by web endpoint
* operations
* @param endpointPathResolver the {@link EndpointPathResolver} used to resolve
* endpoint paths
*/
public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
OperationParameterMapper operationParameterMapper,
CachingConfigurationFactory cachingConfigurationFactory,
EndpointMediaTypes endpointMediaTypes) {
EndpointMediaTypes endpointMediaTypes,
EndpointPathResolver endpointPathResolver) {
super(applicationContext,
new WebEndpointOperationFactory(operationParameterMapper,
endpointMediaTypes),
endpointMediaTypes, endpointPathResolver),
WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory);
}
......@@ -121,10 +125,14 @@ public class WebAnnotationEndpointDiscoverer extends
private final EndpointMediaTypes endpointMediaTypes;
private final EndpointPathResolver endpointPathResolver;
private WebEndpointOperationFactory(OperationParameterMapper parameterMapper,
EndpointMediaTypes endpointMediaTypes) {
EndpointMediaTypes endpointMediaTypes,
EndpointPathResolver endpointPathResolver) {
this.parameterMapper = parameterMapper;
this.endpointMediaTypes = endpointMediaTypes;
this.endpointPathResolver = endpointPathResolver;
}
@Override
......@@ -147,7 +155,8 @@ public class WebAnnotationEndpointDiscoverer extends
}
private String determinePath(String endpointId, Method operationMethod) {
StringBuilder path = new StringBuilder(endpointId);
StringBuilder path = new StringBuilder(
this.endpointPathResolver.resolvePath(endpointId));
Stream.of(operationMethod.getParameters())
.filter((
parameter) -> parameter.getAnnotation(Selector.class) != null)
......
......@@ -385,7 +385,7 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
DefaultConversionService.getSharedInstance());
return new WebAnnotationEndpointDiscoverer(applicationContext,
parameterMapper, (id) -> new CachingConfiguration(0),
endpointMediaTypes());
endpointMediaTypes(), (id) -> id);
}
@Bean
......
......@@ -189,8 +189,8 @@ public class WebAnnotationEndpointDiscovererTests {
@Test
public void endpointMainReadOperationIsCachedWithMatchingId() {
load((id) -> new CachingConfiguration(500), TestEndpointConfiguration.class,
(discoverer) -> {
load((id) -> new CachingConfiguration(500), (id) -> id,
TestEndpointConfiguration.class, (discoverer) -> {
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
discoverer.discoverEndpoints());
assertThat(endpoints).containsOnlyKeys("test");
......@@ -237,12 +237,29 @@ public class WebAnnotationEndpointDiscovererTests {
});
}
@Test
public void endpointPathCanBeCustomized() {
load((id) -> null, (id) -> "custom/" + id,
AdditionalOperationWebEndpointConfiguration.class, (discoverer) -> {
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
discoverer.discoverEndpoints());
assertThat(endpoints).containsOnlyKeys("test");
EndpointInfo<WebEndpointOperation> endpoint = endpoints.get("test");
assertThat(requestPredicates(endpoint)).has(requestPredicates(
path("custom/test").httpMethod(WebEndpointHttpMethod.GET).consumes()
.produces("application/json"),
path("custom/test/{id}").httpMethod(WebEndpointHttpMethod.GET).consumes()
.produces("application/json")));
});
}
private void load(Class<?> configuration,
Consumer<WebAnnotationEndpointDiscoverer> consumer) {
this.load((id) -> null, configuration, consumer);
this.load((id) -> null, (id) -> id, configuration, consumer);
}
private void load(CachingConfigurationFactory cachingConfigurationFactory,
EndpointPathResolver endpointPathResolver,
Class<?> configuration, Consumer<WebAnnotationEndpointDiscoverer> consumer) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
configuration);
......@@ -254,7 +271,8 @@ public class WebAnnotationEndpointDiscovererTests {
cachingConfigurationFactory,
new EndpointMediaTypes(
Collections.singletonList("application/json"),
Collections.singletonList("application/json"))));
Collections.singletonList("application/json")),
endpointPathResolver));
}
finally {
context.close();
......
......@@ -99,7 +99,7 @@ class JerseyEndpointsRunner extends AbstractWebEndpointRunner {
WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext,
new ConversionServiceOperationParameterMapper(), (id) -> null,
endpointMediaTypes);
endpointMediaTypes, (id) -> id);
Collection<Resource> resources = new JerseyEndpointResourceFactory()
.createEndpointResources(new EndpointMapping("/application"),
discoverer.discoverEndpoints(), endpointMediaTypes);
......
......@@ -105,7 +105,7 @@ class WebFluxEndpointsRunner extends AbstractWebEndpointRunner {
WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext,
new ConversionServiceOperationParameterMapper(), (id) -> null,
endpointMediaTypes);
endpointMediaTypes, (id) -> id);
return new WebFluxEndpointHandlerMapping(new EndpointMapping("/application"),
discoverer.discoverEndpoints(), endpointMediaTypes,
new CorsConfiguration());
......
......@@ -88,7 +88,7 @@ class WebMvcEndpointRunner extends AbstractWebEndpointRunner {
WebAnnotationEndpointDiscoverer discoverer = new WebAnnotationEndpointDiscoverer(
this.applicationContext,
new ConversionServiceOperationParameterMapper(), (id) -> null,
endpointMediaTypes);
endpointMediaTypes, (id) -> id);
return new WebMvcEndpointHandlerMapping(new EndpointMapping("/application"),
discoverer.discoverEndpoints(), endpointMediaTypes,
new CorsConfiguration());
......
......@@ -394,6 +394,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
type, null, String.format("Expose the %s endpoint as a Web endpoint.",
endpointId),
enabledByDefault, null));
this.metadataCollector.add(ItemMetadata.newProperty(
endpointKey(endpointId), "web.path", String.class.getName(), type,
null, String.format("Path of the %s endpoint.", endpointId),
endpointId, null));
}
}
......
......@@ -539,8 +539,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("simple", null));
assertThat(metadata).has(jmxEnabledFlag("simple", null));
assertThat(metadata).has(webEnabledFlag("simple", null));
assertThat(metadata).has(webPath("simple"));
assertThat(metadata).has(cacheTtl("simple"));
assertThat(metadata.getItems()).hasSize(5);
assertThat(metadata.getItems()).hasSize(6);
}
@Test
......@@ -551,8 +552,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("disabled", false));
assertThat(metadata).has(jmxEnabledFlag("disabled", false));
assertThat(metadata).has(webEnabledFlag("disabled", false));
assertThat(metadata).has(webPath("disabled"));
assertThat(metadata).has(cacheTtl("disabled"));
assertThat(metadata.getItems()).hasSize(5);
assertThat(metadata.getItems()).hasSize(6);
}
@Test
......@@ -563,8 +565,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("enabled", true));
assertThat(metadata).has(jmxEnabledFlag("enabled", true));
assertThat(metadata).has(webEnabledFlag("enabled", true));
assertThat(metadata).has(webPath("enabled"));
assertThat(metadata).has(cacheTtl("enabled"));
assertThat(metadata.getItems()).hasSize(5);
assertThat(metadata.getItems()).hasSize(6);
}
@Test
......@@ -577,8 +580,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("customprops", null));
assertThat(metadata).has(jmxEnabledFlag("customprops", null));
assertThat(metadata).has(webEnabledFlag("customprops", null));
assertThat(metadata).has(webPath("customprops"));
assertThat(metadata).has(cacheTtl("customprops"));
assertThat(metadata.getItems()).hasSize(6);
assertThat(metadata.getItems()).hasSize(7);
}
@Test
......@@ -599,8 +603,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
Metadata.withGroup("endpoints.web").fromSource(OnlyWebEndpoint.class));
assertThat(metadata).has(enabledFlag("web", null));
assertThat(metadata).has(webEnabledFlag("web", null));
assertThat(metadata).has(webPath("web"));
assertThat(metadata).has(cacheTtl("web"));
assertThat(metadata.getItems()).hasSize(4);
assertThat(metadata.getItems()).hasSize(5);
}
@Test
......@@ -613,8 +618,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(jmxEnabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
assertThat(metadata.getItems()).hasSize(6);
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
"id = \"incremental\", defaultEnablement = org.springframework.boot."
+ "configurationsample.DefaultEnablement.DISABLED");
......@@ -624,8 +630,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("incremental", false));
assertThat(metadata).has(jmxEnabledFlag("incremental", false));
assertThat(metadata).has(webEnabledFlag("incremental", false));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
assertThat(metadata.getItems()).hasSize(6);
}
@Test
......@@ -638,8 +645,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(jmxEnabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
assertThat(metadata.getItems()).hasSize(6);
project.replaceText(IncrementalEndpoint.class, "id = \"incremental\"",
"id = \"incremental\", exposure = org.springframework.boot."
+ "configurationsample.EndpointExposure.WEB");
......@@ -648,8 +656,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
.fromSource(IncrementalEndpoint.class));
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(4);
assertThat(metadata.getItems()).hasSize(5);
}
@Test
......@@ -671,8 +680,9 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata).has(enabledFlag("incremental", null));
assertThat(metadata).has(jmxEnabledFlag("incremental", null));
assertThat(metadata).has(webEnabledFlag("incremental", null));
assertThat(metadata).has(webPath("incremental"));
assertThat(metadata).has(cacheTtl("incremental"));
assertThat(metadata.getItems()).hasSize(5);
assertThat(metadata.getItems()).hasSize(6);
}
private Metadata.MetadataItemCondition enabledFlag(String endpointId,
......@@ -696,6 +706,12 @@ public class ConfigurationMetadataAnnotationProcessorTests {
.format("Expose the %s endpoint as a Web endpoint.", endpointId));
}
private Metadata.MetadataItemCondition webPath(String endpointId) {
return Metadata.withProperty("endpoints." + endpointId + ".web.path")
.ofType(String.class).withDefaultValue(endpointId).withDescription(String
.format("Path of the %s endpoint.", endpointId));
}
private Metadata.MetadataItemCondition cacheTtl(String endpointId) {
return Metadata.withProperty("endpoints." + endpointId + ".cache.time-to-live")
.ofType(Long.class).withDefaultValue(0).withDescription(
......
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