/* * 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; import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.PostConstruct; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration.EndpointHypermediaEnabledCondition; import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes; import org.springframework.boot.actuate.endpoint.mvc.DocsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HypermediaDisabled; import org.springframework.boot.actuate.endpoint.mvc.ManagementServletContext; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; 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.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.ResourceLoader; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.hal.CurieProvider; import org.springframework.hateoas.hal.DefaultCurieProvider; import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.TypeUtils; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; /** * Configuration for hypermedia in HTTP endpoints. * * @author Dave Syer * @author Phillip Webb * @author Andy Wilkinson * @since 1.3.0 */ @ManagementContextConfiguration @ConditionalOnClass(Link.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnBean(HttpMessageConverters.class) @Conditional(EndpointHypermediaEnabledCondition.class) @EnableConfigurationProperties({ ResourceProperties.class, ManagementServerProperties.class }) public class EndpointWebMvcHypermediaManagementContextConfiguration { @Bean public ManagementServletContext managementServletContext( final ManagementServerProperties properties) { return new ManagementServletContext() { @Override public String getContextPath() { return properties.getContextPath(); } }; } @ConditionalOnEnabledEndpoint("actuator") @Bean public HalJsonMvcEndpoint halJsonMvcEndpoint( ManagementServletContext managementServletContext, ResourceProperties resources, ResourceLoader resourceLoader) { if (HalBrowserMvcEndpoint.getHalBrowserLocation(resourceLoader) != null) { return new HalBrowserMvcEndpoint(managementServletContext); } return new HalJsonMvcEndpoint(managementServletContext); } @Bean @ConditionalOnBean(DocsMvcEndpoint.class) @ConditionalOnMissingBean(CurieProvider.class) @ConditionalOnProperty(prefix = "endpoints.docs.curies", name = "enabled", matchIfMissing = false) public DefaultCurieProvider curieProvider(ManagementServerProperties management, DocsMvcEndpoint endpoint) { String path = management.getContextPath() + endpoint.getPath() + "/#spring_boot_actuator__{rel}"; return new DefaultCurieProvider("boot", new UriTemplate(path)); } @Configuration static class DocsMvcEndpointConfiguration { @Bean @ConditionalOnEnabledEndpoint("docs") @ConditionalOnResource(resources = "classpath:/META-INF/resources/spring-boot-actuator/docs/index.html") public DocsMvcEndpoint docsMvcEndpoint( ManagementServletContext managementServletContext) { return new DocsMvcEndpoint(managementServletContext); } } /** * Controller advice that adds links to the actuator endpoint's path. */ @ControllerAdvice public static class ActuatorEndpointLinksAdvice implements ResponseBodyAdvice<Object> { @Autowired private MvcEndpoints endpoints; @Autowired(required = false) private HalJsonMvcEndpoint halJsonMvcEndpoint; @Autowired private ManagementServerProperties management; private LinksEnhancer linksEnhancer; @PostConstruct public void init() { this.linksEnhancer = new LinksEnhancer(this.management.getContextPath(), this.endpoints); } @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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.halJsonMvcEndpoint.getPath()); } } private boolean isActuatorEndpointPath(String path) { if (this.halJsonMvcEndpoint != null) { String toMatch = this.management.getContextPath() + this.halJsonMvcEndpoint.getPath(); return toMatch.equals(path) || (toMatch + "/").equals(path); } return false; } } /** * 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. */ @ConditionalOnProperty(prefix = "endpoints.hypermedia", name = "enabled", matchIfMissing = false) @ControllerAdvice(assignableTypes = MvcEndpoint.class) static class MvcEndpointAdvice implements ResponseBodyAdvice<Object> { private final List<RequestMappingHandlerAdapter> handlerAdapters; private final Map<MediaType, HttpMessageConverter<?>> converterCache = new ConcurrentHashMap<>(); MvcEndpointAdvice(List<RequestMappingHandlerAdapter> handlerAdapters) { this.handlerAdapters = handlerAdapters; } @PostConstruct public void configureHttpMessageConverters() { for (RequestMappingHandlerAdapter handlerAdapter : this.handlerAdapters) { for (HttpMessageConverter<?> messageConverter : handlerAdapter .getMessageConverters()) { configureHttpMessageConverter(messageConverter); } } } private void configureHttpMessageConverter( HttpMessageConverter<?> messageConverter) { if (messageConverter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { List<MediaType> supportedMediaTypes = new ArrayList<>( messageConverter.getSupportedMediaTypes()); supportedMediaTypes.add(ActuatorMediaTypes.APPLICATION_ACTUATOR_V2_JSON); ((AbstractHttpMessageConverter<?>) messageConverter) .setSupportedMediaTypes(supportedMediaTypes); } } @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { Class<?> controllerType = returnType.getDeclaringClass(); return !HalJsonMvcEndpoint.class.isAssignableFrom(controllerType); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (request instanceof ServletServerHttpRequest) { return beforeBodyWrite(body, returnType, selectedContentType, selectedConverterType, (ServletServerHttpRequest) request, response); } return body; } private Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServletServerHttpRequest request, ServerHttpResponse response) { if (body == null || body instanceof Resource) { // Assume it already was handled or it already has its links return body; } if (body instanceof Collection || body.getClass().isArray()) { // We can't add links to a collection without wrapping it return body; } HttpMessageConverter<Object> 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<Object> findConverter( Class<? extends HttpMessageConverter<?>> selectedConverterType, MediaType mediaType) { HttpMessageConverter<Object> cached = (HttpMessageConverter<Object>) this.converterCache .get(mediaType); if (cached != null) { return cached; } for (RequestMappingHandlerAdapter handlerAdapter : this.handlerAdapters) { for (HttpMessageConverter<?> converter : handlerAdapter .getMessageConverters()) { if (selectedConverterType.isAssignableFrom(converter.getClass()) && converter.canWrite(EndpointResource.class, mediaType)) { this.converterCache.put(mediaType, converter); return (HttpMessageConverter<Object>) converter; } } } 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); } } @JsonInclude(content = Include.NON_NULL) @JacksonXmlRootElement(localName = "resource") private static class EndpointResource extends ResourceSupport { private Object content; private Map<String, Object> embedded; @SuppressWarnings("unchecked") EndpointResource(Object content, String path) { this.content = content instanceof Map ? null : content; this.embedded = (Map<String, Object>) (this.content == null ? content : null); add(linkTo(Object.class).slash(path).withSelfRel()); } @JsonUnwrapped public Object getContent() { return this.content; } @JsonAnyGetter public Map<String, Object> getEmbedded() { return this.embedded; } } static class EndpointHypermediaEnabledCondition extends AnyNestedCondition { EndpointHypermediaEnabledCondition() { super(ConfigurationPhase.REGISTER_BEAN); } @ConditionalOnEnabledEndpoint("actuator") static class ActuatorEndpointEnabled { } @ConditionalOnEnabledEndpoint("docs") static class DocsEndpointEnabled { } } }