diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java index 7d319b894d5..79b6c925cb8 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java @@ -61,7 +61,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", - "endpoints.health.sensitive=true", "endpoints.links.enabled=false" }) + "endpoints.health.sensitive=true", "endpoints.actuator.enabled=false" }) @DirtiesContext public class EndpointDocumentation { diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java index 6188868134b..b343e421d1e 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java @@ -82,7 +82,7 @@ public class HypermediaEndpointDocumentation { @Test public void home() throws Exception { - this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON)) + this.mockMvc.perform(get("/actuator").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("admin")); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java index c95db6d57b5..e5e75ab0045 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java @@ -25,32 +25,23 @@ import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.endpoint.mvc.ActuatorDocsEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint.HalBrowserLocation; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HypermediaDisabled; -import org.springframework.boot.actuate.endpoint.mvc.LinksMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.context.annotation.Conditional; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; @@ -75,7 +66,6 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; -import static org.springframework.hateoas.mvc.BasicLinkBuilder.linkToCurrentMapping; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; /** @@ -83,6 +73,7 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; * * @author Dave Syer * @author Phillip Webb + * @author Andy Wilkinson * @since 1.3.0 */ @ManagementContextConfiguration @@ -93,16 +84,15 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; @EnableConfigurationProperties(ResourceProperties.class) public class EndpointWebMvcHypermediaManagementContextConfiguration { - @ConditionalOnProperty(value = "endpoints.hal.enabled", matchIfMissing = true) - @Conditional(HalBrowserCondition.class) + @ConditionalOnProperty(prefix = "endpoints.actuator", name = "enabled", matchIfMissing = true) @Bean - public HalBrowserMvcEndpoint halBrowserMvcEndpoint( - ManagementServerProperties management, ResourceProperties resources) { - return new HalBrowserMvcEndpoint(management); + public ActuatorMvcEndpoint actuatorMvcEndpoint(ManagementServerProperties management, + ResourceProperties resources) { + return new ActuatorMvcEndpoint(management); } @Bean - @ConditionalOnProperty(value = "endpoints.docs.enabled", matchIfMissing = true) + @ConditionalOnProperty(prefix = "endpoints.docs", name = "enabled", matchIfMissing = true) @ConditionalOnResource(resources = "classpath:/META-INF/resources/spring-boot-actuator/docs/index.html") public ActuatorDocsEndpoint actuatorDocsEndpoint(ManagementServerProperties management) { return new ActuatorDocsEndpoint(management); @@ -111,7 +101,7 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { @Bean @ConditionalOnBean(ActuatorDocsEndpoint.class) @ConditionalOnMissingBean(CurieProvider.class) - @ConditionalOnProperty(value = "endpoints.docs.curies.enabled", matchIfMissing = false) + @ConditionalOnProperty(prefix = "endpoints.docs.curies", name = "enabled", matchIfMissing = false) public DefaultCurieProvider curieProvider(ServerProperties server, ManagementServerProperties management, ActuatorDocsEndpoint endpoint) { String path = management.getContextPath() + endpoint.getPath() @@ -124,217 +114,167 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { } /** - * {@link SpringBootCondition} to detect the HAL browser. + * Controller advice that adds links to the actuator endpoint's path. */ - protected static class HalBrowserCondition extends SpringBootCondition { + @ControllerAdvice + public static class ActuatorEndpointLinksAdvice implements ResponseBodyAdvice { + + @Autowired + private MvcEndpoints endpoints; + + @Autowired(required = false) + private ActuatorMvcEndpoint actuatorEndpoint; + + @Autowired + private ManagementServerProperties management; + + private LinksEnhancer linksEnhancer; + + @PostConstruct + public void init() { + this.linksEnhancer = new LinksEnhancer(this.management.getContextPath(), + this.endpoints); + } @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, - AnnotatedTypeMetadata metadata) { - ResourceLoader loader = context.getResourceLoader(); - loader = (loader == null ? new DefaultResourceLoader() : loader); - HalBrowserLocation found = HalBrowserMvcEndpoint - .getHalBrowserLocation(loader); - return new ConditionOutcome(found != null, "HAL Browser " - + (found == null ? "not found" : "at " + found)); + public boolean supports(MethodParameter returnType, + Class> converterType) { + returnType.increaseNestingLevel(); + Type nestedType = returnType.getNestedGenericParameterType(); + returnType.decreaseNestingLevel(); + return ResourceSupport.class.isAssignableFrom(returnType.getParameterType()) + || TypeUtils.isAssignable(ResourceSupport.class, nestedType); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (request instanceof ServletServerHttpRequest) { + beforeBodyWrite(body, (ServletServerHttpRequest) request); + } + return body; + } + + private void beforeBodyWrite(Object body, ServletServerHttpRequest request) { + Object pattern = request.getServletRequest().getAttribute( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + if (pattern != null && body instanceof ResourceSupport) { + beforeBodyWrite(pattern.toString(), (ResourceSupport) body); + } + } + + private void beforeBodyWrite(String path, ResourceSupport body) { + if (isActuatorEndpointPath(path)) { + this.linksEnhancer + .addEndpointLinks(body, this.actuatorEndpoint.getPath()); + } + } + + private boolean isActuatorEndpointPath(String path) { + return this.actuatorEndpoint != null + && (this.management.getContextPath() + this.actuatorEndpoint + .getPath()).equals(path); } } /** - * Configuration for Endpoint links. + * Controller advice that adds links to the existing Actuator endpoints. By default + * all the top-level resources are enhanced with a "self" link. Those resources that + * could not be enhanced (e.g. "/env/{name}") because their values are "primitive" are + * ignored. Those that have values of type Collection (e.g. /trace) are transformed in + * to maps, and the original collection value is added with a key equal to the + * endpoint name. */ - @ConditionalOnProperty(value = "endpoints.links.enabled", matchIfMissing = true) - public static class LinksConfiguration { + @ControllerAdvice(assignableTypes = MvcEndpoint.class) + public static class MvcEndpointAdvice implements ResponseBodyAdvice { - @Bean - public LinksMvcEndpoint linksMvcEndpoint(ResourceProperties resources) { - return new LinksMvcEndpoint(); + @Autowired + private ManagementServerProperties management; + + @Autowired + private HttpMessageConverters converters; + + private Map> converterCache = new ConcurrentHashMap>(); + + @Autowired + private ObjectMapper mapper; + + @Override + public boolean supports(MethodParameter returnType, + Class> converterType) { + Class controllerType = returnType.getDeclaringClass(); + return !ActuatorMvcEndpoint.class.isAssignableFrom(controllerType); } - /** - * Controller advice that adds links to the home page and/or the management - * context path. The home page is enhanced if it is composed already of a - * {@link ResourceSupport} (e.g. when using Spring Data REST). - */ - @ControllerAdvice - public static class HomePageLinksAdvice implements ResponseBodyAdvice { - - @Autowired - private MvcEndpoints endpoints; - - @Autowired(required = false) - private LinksMvcEndpoint linksEndpoint; - - @Autowired - private ManagementServerProperties management; - - private LinksEnhancer linksEnhancer; - - @PostConstruct - public void init() { - this.linksEnhancer = new LinksEnhancer(this.management.getContextPath(), - this.endpoints); + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (request instanceof ServletServerHttpRequest) { + return beforeBodyWrite(body, returnType, selectedContentType, + selectedConverterType, (ServletServerHttpRequest) request, + response); } - - @Override - public boolean supports(MethodParameter returnType, - Class> converterType) { - Class controllerType = returnType.getDeclaringClass(); - if (!LinksMvcEndpoint.class.isAssignableFrom(controllerType) - && MvcEndpoint.class.isAssignableFrom(controllerType)) { - return false; - } - returnType.increaseNestingLevel(); - Type nestedType = returnType.getNestedGenericParameterType(); - returnType.decreaseNestingLevel(); - return ResourceSupport.class.isAssignableFrom(returnType - .getParameterType()) - || TypeUtils.isAssignable(ResourceSupport.class, nestedType); - } - - @Override - public Object beforeBodyWrite(Object body, MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - if (request instanceof ServletServerHttpRequest) { - beforeBodyWrite(body, (ServletServerHttpRequest) request); - } - return body; - } - - private void beforeBodyWrite(Object body, ServletServerHttpRequest request) { - Object pattern = request.getServletRequest().getAttribute( - HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - if (pattern != null && body instanceof ResourceSupport) { - beforeBodyWrite(pattern.toString(), (ResourceSupport) body); - } - } - - private void beforeBodyWrite(String path, ResourceSupport body) { - if (isLinksPath(path)) { - this.linksEnhancer.addEndpointLinks(body, - this.linksEndpoint.getPath()); - } - else if (isHomePage(path)) { - body.add(linkToCurrentMapping() - .slash(this.management.getContextPath()) - .slash(this.linksEndpoint.getPath()).withRel("actuator")); - } - } - - private boolean isLinksPath(String path) { - return this.linksEndpoint != null - && (this.management.getContextPath() + this.linksEndpoint - .getPath()).equals(path); - } - - private boolean isHomePage(String path) { - return "".equals(path) || "/".equals(path); - } - + return body; } - /** - * Controller advice that adds links to the existing Actuator endpoints. By - * default all the top-level resources are enhanced with a "self" link. Those - * resources that could not be enhanced (e.g. "/env/{name}") because their values - * are "primitive" are ignored. Those that have values of type Collection (e.g. - * /trace) are transformed in to maps, and the original collection value is added - * with a key equal to the endpoint name. - */ - @ControllerAdvice(assignableTypes = MvcEndpoint.class) - public static class MvcEndpointAdvice implements ResponseBodyAdvice { - - @Autowired - private ManagementServerProperties management; - - @Autowired - private HttpMessageConverters converters; - - private Map> converterCache = new ConcurrentHashMap>(); - - @Autowired - private ObjectMapper mapper; - - @Override - public boolean supports(MethodParameter returnType, - Class> converterType) { - Class controllerType = returnType.getDeclaringClass(); - return !LinksMvcEndpoint.class.isAssignableFrom(controllerType) - && !HalBrowserMvcEndpoint.class.isAssignableFrom(controllerType); - } - - @Override - public Object beforeBodyWrite(Object body, MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - if (request instanceof ServletServerHttpRequest) { - return beforeBodyWrite(body, returnType, selectedContentType, - selectedConverterType, (ServletServerHttpRequest) request, - response); - } + private Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServletServerHttpRequest request, ServerHttpResponse response) { + if (body == null || body instanceof Resource) { + // Assume it already was handled or it already has its links return body; } - - private Object beforeBodyWrite(Object body, MethodParameter returnType, - MediaType selectedContentType, - Class> selectedConverterType, - ServletServerHttpRequest request, ServerHttpResponse response) { - if (body == null || body instanceof Resource) { - // Assume it already was handled or it already has its links - return body; - } - HttpMessageConverter converter = findConverter( - selectedConverterType, selectedContentType); - if (converter == null || isHypermediaDisabled(returnType)) { - // Not a resource that can be enhanced with a link - return body; - } - String path = getPath(request); - try { - converter.write(new EndpointResource(body, path), - selectedContentType, response); - } - catch (IOException ex) { - throw new HttpMessageNotWritableException("Cannot write response", ex); - } - return null; + HttpMessageConverter converter = findConverter(selectedConverterType, + selectedContentType); + if (converter == null || isHypermediaDisabled(returnType)) { + // Not a resource that can be enhanced with a link + return body; } + String path = getPath(request); + try { + converter.write(new EndpointResource(body, path), selectedContentType, + response); + } + catch (IOException ex) { + throw new HttpMessageNotWritableException("Cannot write response", ex); + } + return null; + } - @SuppressWarnings("unchecked") - private HttpMessageConverter findConverter( - Class> selectedConverterType, - MediaType mediaType) { - if (this.converterCache.containsKey(mediaType)) { - return (HttpMessageConverter) this.converterCache - .get(mediaType); + @SuppressWarnings("unchecked") + private HttpMessageConverter findConverter( + Class> selectedConverterType, + MediaType mediaType) { + if (this.converterCache.containsKey(mediaType)) { + return (HttpMessageConverter) this.converterCache.get(mediaType); + } + for (HttpMessageConverter converter : this.converters) { + if (selectedConverterType.isAssignableFrom(converter.getClass()) + && converter.canWrite(EndpointResource.class, mediaType)) { + this.converterCache.put(mediaType, converter); + return (HttpMessageConverter) converter; } - for (HttpMessageConverter converter : this.converters) { - if (selectedConverterType.isAssignableFrom(converter.getClass()) - && converter.canWrite(EndpointResource.class, mediaType)) { - this.converterCache.put(mediaType, converter); - return (HttpMessageConverter) converter; - } - } - return null; } + return null; + } - private boolean isHypermediaDisabled(MethodParameter returnType) { - return AnnotationUtils.findAnnotation(returnType.getMethod(), - HypermediaDisabled.class) != null - || AnnotationUtils.findAnnotation(returnType.getMethod() - .getDeclaringClass(), HypermediaDisabled.class) != null; - } - - private String getPath(ServletServerHttpRequest request) { - String path = (String) request.getServletRequest().getAttribute( - HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - return (path == null ? "" : path); - } + private boolean isHypermediaDisabled(MethodParameter returnType) { + return AnnotationUtils.findAnnotation(returnType.getMethod(), + HypermediaDisabled.class) != null + || AnnotationUtils.findAnnotation(returnType.getMethod() + .getDeclaringClass(), HypermediaDisabled.class) != null; + } + private String getPath(ServletServerHttpRequest request) { + String path = (String) request.getServletRequest().getAttribute( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + return (path == null ? "" : path); } } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java index c37c701bb64..6207f9ece9f 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/LinksEnhancer.java @@ -27,7 +27,7 @@ import org.springframework.util.StringUtils; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; /** - * Adds endpoints links to {@link ResourceSupport}. + * Adds endpoint links to {@link ResourceSupport}. * * @author Dave Syer */ diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMvcEndpoint.java similarity index 74% rename from spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpoint.java rename to spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMvcEndpoint.java index 97fd03ee8af..1013b8aac12 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMvcEndpoint.java @@ -23,16 +23,20 @@ import javax.servlet.http.HttpServletRequest; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.resource.ResourceTransformer; @@ -40,15 +44,16 @@ import org.springframework.web.servlet.resource.ResourceTransformerChain; import org.springframework.web.servlet.resource.TransformedResource; /** - * {@link MvcEndpoint} to support the HAL browser. + * {@link MvcEndpoint} for the actuator. Uses content negotiation to provide access to the + * HAL browser (when on the classpath), and to HAL-formatted JSON. * * @author Dave Syer - * @author Phillip Webb - * @since 1.3.0 + * @author Phil Webb + * @author Andy Wilkinson */ -@ConfigurationProperties("endpoints.hal") -public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements - MvcEndpoint, ResourceLoaderAware { +@ConfigurationProperties("endpoints.actuator") +public class ActuatorMvcEndpoint extends WebMvcConfigurerAdapter implements MvcEndpoint, + ResourceLoaderAware { private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -63,8 +68,8 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements * Endpoint URL path. */ @NotNull - @Pattern(regexp = "/[^/]*", message = "Path must start with /") - private String path = "/hal"; + @Pattern(regexp = "^$|/[^/]*", message = "Path must be empty or start with /") + private String path; /** * Enable security on the endpoint. @@ -78,13 +83,16 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements private final ManagementServerProperties management; - @Autowired(required = false) - private LinksMvcEndpoint linksMvcEndpoint; - private HalBrowserLocation location; - public HalBrowserMvcEndpoint(ManagementServerProperties management) { + public ActuatorMvcEndpoint(ManagementServerProperties management) { this.management = management; + if (StringUtils.hasText(management.getContextPath())) { + this.path = ""; + } + else { + this.path = "/actuator"; + } } @Override @@ -94,22 +102,34 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public String browse(HttpServletRequest request) { - String contextPath = this.management.getContextPath() + this.path + "/"; + if (this.location == null) { + throw new HalBrowserUnavailableException(); + } + String contextPath = this.management.getContextPath() + + (this.path.endsWith("/") ? this.path : this.path + "/"); if (request.getRequestURI().endsWith("/")) { return "forward:" + contextPath + this.location.getHtmlFile(); } return "redirect:" + contextPath; } + @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public ResourceSupport links() { + return new ResourceSupport(); + } + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // Make sure the root path is not cached so the browser comes back for the JSON // and add a transformer to set the initial link - String start = this.management.getContextPath() + this.path; - registry.addResourceHandler(start + "/", start + "/**") - .addResourceLocations(this.location.getResourceLocation()) - .setCachePeriod(0).resourceChain(true) - .addTransformer(new InitialUrlTransformer()); + if (this.location != null) { + String start = this.management.getContextPath() + this.path; + registry.addResourceHandler(start + "/", start + "/**") + .addResourceLocations(this.location.getResourceLocation()) + .setCachePeriod(0).resourceChain(true) + .addTransformer(new InitialUrlTransformer()); + } } public void setPath(String path) { @@ -121,23 +141,23 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements return this.path; } - public void setSensitive(boolean sensitive) { - this.sensitive = sensitive; - } - @Override public boolean isSensitive() { return this.sensitive; } - public void setEnabled(boolean enabled) { - this.enabled = enabled; + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; } public boolean isEnabled() { return this.enabled; } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + @Override public Class> getEndpointType() { return null; @@ -167,21 +187,17 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements ResourceTransformerChain transformerChain) throws IOException { resource = transformerChain.transform(request, resource); if (resource.getFilename().equalsIgnoreCase( - HalBrowserMvcEndpoint.this.location.getHtmlFile())) { + ActuatorMvcEndpoint.this.location.getHtmlFile())) { return replaceInitialLink(resource); } return resource; } private Resource replaceInitialLink(Resource resource) throws IOException { - LinksMvcEndpoint linksEndpoint = HalBrowserMvcEndpoint.this.linksMvcEndpoint; - if (linksEndpoint == null) { - return resource; - } byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); String content = new String(bytes, DEFAULT_CHARSET); - String initialLink = HalBrowserMvcEndpoint.this.management.getContextPath() - + linksEndpoint.getPath(); + String initialLink = ActuatorMvcEndpoint.this.management.getContextPath() + + getPath(); content = content.replace("entryPoint: '/'", "entryPoint: '" + initialLink + "'"); return new TransformedResource(resource, content.getBytes(DEFAULT_CHARSET)); @@ -215,4 +231,9 @@ public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements } + @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) + private static class HalBrowserUnavailableException extends RuntimeException { + + } + } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java index 032a531b23f..9be27aad91b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java @@ -25,8 +25,8 @@ import java.lang.annotation.Target; import org.springframework.web.bind.annotation.RequestMapping; /** - * Annotation to that {@link MvcEndpoint} class or {@link RequestMapping} method should't - * generate a hypermedia response. + * Annotation to indicate that an {@link MvcEndpoint} class or {@link RequestMapping} + * method should't generate a hypermedia response. * * @author Dave Syer * @since 1.3.0 diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java deleted file mode 100644 index 8e2f4aa20d0..00000000000 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2013-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 javax.validation.constraints.NotNull; -import javax.validation.constraints.Pattern; - -import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.hateoas.ResourceSupport; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * {@link MvcEndpoint} to add hypermedia links. - * - * @author Dave Syer - * @since 1.3.0 - */ -@ConfigurationProperties("endpoints.links") -public class LinksMvcEndpoint implements MvcEndpoint { - - /** - * Endpoint URL path. - */ - @NotNull - @Pattern(regexp = "/[^/]*", message = "Path must start with /") - private String path = "/links"; - - /** - * Enable security on the endpoint. - */ - private boolean sensitive = false; - - /** - * Enable the endpoint. - */ - private boolean enabled = true; - - public LinksMvcEndpoint() { - } - - @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) - @ResponseBody - public ResourceSupport links() { - return new ResourceSupport(); - } - - public void setPath(String path) { - this.path = path; - } - - @Override - public String getPath() { - return this.path; - } - - @Override - public boolean isSensitive() { - return this.sensitive; - } - - public void setSensitive(boolean sensitive) { - this.sensitive = sensitive; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public Class> getEndpointType() { - return null; - } - -} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java index 818ab120719..2f7ef02ac2c 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java @@ -20,8 +20,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; import org.springframework.boot.actuate.autoconfigure.BrowserPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.annotation.Configuration; @@ -40,10 +40,16 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +/** + * Integration tests for {@link ActuatorMvcEndpoint}'s HAL Browser support + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration -@TestPropertySource(properties = "endpoints.hal.path=/hal") +@TestPropertySource(properties = "endpoints.actuator.path=/actuator") @DirtiesContext public class BrowserPathHypermediaIntegrationTests { @@ -63,26 +69,22 @@ public class BrowserPathHypermediaIntegrationTests { @Test public void browser() throws Exception { MvcResult response = this.mockMvc - .perform(get("/hal/").accept(MediaType.TEXT_HTML)) + .perform(get("/actuator/").accept(MediaType.TEXT_HTML)) .andExpect(status().isOk()).andReturn(); - assertEquals("/hal/browser.html", response.getResponse().getForwardedUrl()); + assertEquals("/actuator/browser.html", response.getResponse().getForwardedUrl()); } @Test public void redirect() throws Exception { - this.mockMvc.perform(get("/hal").accept(MediaType.TEXT_HTML)) + this.mockMvc.perform(get("/actuator").accept(MediaType.TEXT_HTML)) .andExpect(status().isFound()) - .andExpect(header().string("location", "/hal/")); + .andExpect(header().string("location", "/actuator/")); } @MinimalActuatorHypermediaApplication @Configuration public static class SpringBootHypermediaApplication { - public static void main(String[] args) { - SpringApplication.run(SpringBootHypermediaApplication.class, args); - } - } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java deleted file mode 100644 index fbe0da4615c..00000000000 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.autoconfigure; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.actuate.autoconfigure.CustomHomepageHypermediaIntegrationTests.SpringBootHypermediaApplication; -import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; -import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.hateoas.ResourceSupport; -import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; - -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) -@WebAppConfiguration -@DirtiesContext -public class CustomHomepageHypermediaIntegrationTests { - - @Autowired - private WebApplicationContext context; - - @Autowired - private MvcEndpoints mvcEndpoints; - - private MockMvc mockMvc; - - @Before - public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); - } - - @Test - public void links() throws Exception { - this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); - } - - @Test - public void endpointsAllListed() throws Exception { - for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { - String path = endpoint.getPath(); - if ("/links".equals(path)) { - continue; - } - path = path.startsWith("/") ? path.substring(1) : path; - this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._links.%s.href", path).exists()); - } - } - - @MinimalActuatorHypermediaApplication - @RestController - public static class SpringBootHypermediaApplication { - - @RequestMapping("") - public ResourceSupport home() { - ResourceSupport resource = new ResourceSupport(); - resource.add(linkTo(SpringBootHypermediaApplication.class).slash("/") - .withSelfRel()); - return resource; - } - - public static void main(String[] args) { - SpringApplication.run(SpringBootHypermediaApplication.class, args); - } - - } - -} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index 76b591643e4..c6318442830 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -292,9 +292,9 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.register(RootConfig.class, BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); this.applicationContext.refresh(); - // /health, /metrics, /env (/shutdown is disabled by default) + // /health, /metrics, /env, /actuator (/shutdown is disabled by default) assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class).size(), - is(equalTo(5))); + is(equalTo(4))); } @Test diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementContextPathHypermediaIntegrationTests.java similarity index 79% rename from spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java rename to spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementContextPathHypermediaIntegrationTests.java index e6cfaa0311d..f3a3e3a75fa 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementContextPathHypermediaIntegrationTests.java @@ -20,10 +20,10 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.ContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.autoconfigure.ManagementContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; -import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.ResourceSupport; import org.springframework.http.MediaType; @@ -39,15 +39,23 @@ import org.springframework.web.context.WebApplicationContext; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +/** + * Integration tests for {@link ActuatorMvcEndpoint} when a custom management context path + * has been configured. + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @TestPropertySource(properties = "management.contextPath:/admin") @DirtiesContext -public class ContextPathHypermediaIntegrationTests { +public class ManagementContextPathHypermediaIntegrationTests { @Autowired private WebApplicationContext context; @@ -63,15 +71,16 @@ public class ContextPathHypermediaIntegrationTests { } @Test - public void home() throws Exception { - this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + public void actuatorHomeJson() throws Exception { + this.mockMvc.perform(get("/admin").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); } @Test - public void links() throws Exception { - this.mockMvc.perform(get("/admin/links").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + public void actuatorHomeHtml() throws Exception { + this.mockMvc.perform(get("/admin/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("/admin/browser.html")); } @Test @@ -89,13 +98,13 @@ public class ContextPathHypermediaIntegrationTests { public void endpointsAllListed() throws Exception { for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { String path = endpoint.getPath(); - if ("/links".equals(path)) { + if ("/actuator".equals(path)) { continue; } path = path.startsWith("/") ? path.substring(1) : path; path = path.length() > 0 ? path : "self"; this.mockMvc - .perform(get("/admin/links").accept(MediaType.APPLICATION_JSON)) + .perform(get("/admin").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect( jsonPath("$._links.%s.href", path).value( @@ -115,11 +124,6 @@ public class ContextPathHypermediaIntegrationTests { return resource; } - public static void main(String[] args) { - new SpringApplicationBuilder(SpringBootHypermediaApplication.class) - .properties("management.contextPath:/admin").run(args); - } - } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java index 7a8383d6b75..1c17e25b05d 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java @@ -22,7 +22,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.ServerContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.test.IntegrationTest; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.TestRestTemplate; @@ -43,6 +43,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +/** + * Integration tests for {@link ActuatorMvcEndpoint} when a custom server context path has + * been configured. + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @@ -54,7 +61,7 @@ public class ServerContextPathHypermediaIntegrationTests { private int port; @Test - public void links() throws Exception { + public void linksAddedToHomePage() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); ResponseEntity entity = new TestRestTemplate().exchange( @@ -66,16 +73,28 @@ public class ServerContextPathHypermediaIntegrationTests { } @Test - public void browser() throws Exception { + public void actuatorBrowser() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); ResponseEntity entity = new TestRestTemplate().exchange( - "http://localhost:" + this.port + "/spring/hal/", HttpMethod.GET, + "http://localhost:" + this.port + "/spring/actuator/", HttpMethod.GET, new HttpEntity(null, headers), String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains(" entity = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/spring/actuator", HttpMethod.GET, + new HttpEntity(null, headers), String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains("\"_links\":")); + } + @MinimalActuatorHypermediaApplication @RestController public static class SpringBootHypermediaApplication { @@ -88,11 +107,6 @@ public class ServerContextPathHypermediaIntegrationTests { return resource; } - public static void main(String[] args) { - new SpringApplicationBuilder(SpringBootHypermediaApplication.class) - .properties("server.contextPath=/spring").run(args); - } - } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java index 0743f84d044..8a04cd1c0fc 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerPortHypermediaIntegrationTests.java @@ -22,7 +22,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.actuate.autoconfigure.ServerPortHypermediaIntegrationTests.SpringBootHypermediaApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMvcEndpoint; import org.springframework.boot.test.IntegrationTest; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.TestRestTemplate; @@ -43,6 +43,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +/** + * Integration tests for {@link ActuatorMvcEndpoint} when a custom server port has been + * configured. + * + * @author Dave Syer + * @author Andy Wilkinson + */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) @WebAppConfiguration @@ -58,11 +65,13 @@ public class ServerPortHypermediaIntegrationTests { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); ResponseEntity entity = new TestRestTemplate().exchange( - "http://localhost:" + this.port + "/links", HttpMethod.GET, + "http://localhost:" + this.port + "/actuator", HttpMethod.GET, new HttpEntity(null, headers), String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("\"_links\":")); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains(":" + this.port)); } @Test @@ -70,7 +79,7 @@ public class ServerPortHypermediaIntegrationTests { HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); ResponseEntity entity = new TestRestTemplate().exchange( - "http://localhost:" + this.port + "/hal/", HttpMethod.GET, + "http://localhost:" + this.port + "/actuator/", HttpMethod.GET, new HttpEntity(null, headers), String.class); assertEquals(HttpStatus.OK, entity.getStatusCode()); assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains(" 0 ? path : "/"; this.mockMvc .perform(get(path).accept(MediaType.APPLICATION_JSON)) @@ -126,10 +129,6 @@ public class VanillaHypermediaIntegrationTests { @Configuration public static class SpringBootHypermediaApplication { - public static void main(String[] args) { - SpringApplication.run(SpringBootHypermediaApplication.class, args); - } - } } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 1783d80da1f..4bdc4abcd70 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -699,6 +699,9 @@ content into your application; rather pick only the properties that you need. endpoints.trace.enabled=true # HYPERMEDIA ENDPOINTS + endpoints.actuator.enabled=true + endpoints.actuator.path=/actuator + endpoints.actuator.sensitive=false endpoints.docs.curies.enabled=false endpoints.docs.enabled=true endpoints.docs.path=/docs @@ -709,9 +712,6 @@ content into your application; rather pick only the properties that you need. endpoints.hal.enabled=true endpoints.hal.path= # Redirects root HTML traffic to the HAL browser endpoints.hal.sensitive=false - endpoints.links.enabled=true - endpoints.links.path=/links - endpoints.links.sensitive=false endpoints.liquibase.enabled=true endpoints.liquibase.id=liquibase endpoints.liquibase.sensitive=false diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 22964fd5f7d..9ee714e32f1 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -67,6 +67,10 @@ The following endpoints are available: |=== | ID | Description | Sensitive +|`actuator` +|Provides a hypermedia-based "`discovery page`" for the other endpoints. Requires Spring +HATEOAS to be on the classpath. + |`autoconfig` |Displays an auto-configuration report showing all auto-configuration candidates and the reason why they '`were`' or '`were not`' applied. @@ -169,18 +173,19 @@ If http://projects.spring.io/spring-hateoas[Spring HATEOAS] is on the classpath through the `spring-boot-starter-hateoas` or if you are using http://projects.spring.io/spring-data-rest[Spring Data REST]) then the HTTP endpoints from the Actuator are enhanced with hypermedia links, and a "`discovery page`" is added -with links to all the endpoints. The "`discovery page`" is actually an endpoint itself, -so it can be disabled along with the rest of the hypermedia by setting -`endpoints.links.enabled=false`. If it is not explicitly disabled the links -endpoint renders a JSON object with a link for each other endpoint on `/links`. If Spring -Data REST is used, the root endpoint is enhanced with an extra `actuator` links that -points to the "`discovery page`". +with links to all the endpoints. The "`discovery page`" is available on `/actuator` by +default. It is implemented as an endpoint, allowing properties to be used to configure +its path (`endpoints.actuator.path`) and whether or not it is enabled +(`endpoints.actuator.enabled`). + +When a custom management context path is configured, the "`discovery page`" will +automatically move from `/actuator` 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`. If the https://github.com/mikekelly/hal-browser[HAL Browser] is on the classpath via its webjar (`org.webjars:hal-browser`), or via the `spring-data-rest-hal-browser` then -the default home page for HTML clients will be the HAL Browser. This is also exposed via -an endpoint ("`hal`") so it can be disabled and have its path explicitly configured like -the other endpoints. +an HTML "`discovery page`", in the form of the HAL Browser, is also provided.