Don't user root path for HAL endpoints
Update LinksMvcEndpoint and HalBrowserMvcEndpoint so that no longer try to use the `/` context path. Links are now available from `/links` and the HAL browser is available from `/hal`. The actuator HAL browser now works with either WebJars or the Spring Data version. It also now transforms the initial HTML so that the form is pre-populated with `/links`. When using Spring Data's HAL browser, the root includes a link to `/links` with a rel of `actuator`. See gh-3621
This commit is contained in:
@@ -82,7 +82,7 @@ public class HypermediaEndpointDocumentation {
|
||||
|
||||
@Test
|
||||
public void home() throws Exception {
|
||||
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andDo(document("admin"));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,10 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.boot.actuate.endpoint.mvc.ActuatorDocsEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.mvc.HalBrowserEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint.HalBrowserLocation;
|
||||
import org.springframework.boot.actuate.endpoint.mvc.HypermediaDisabled;
|
||||
import org.springframework.boot.actuate.endpoint.mvc.LinksMvcEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
|
||||
@@ -44,17 +42,15 @@ 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.bind.RelaxedPropertyResolver;
|
||||
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.context.annotation.Configuration;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
import org.springframework.core.type.MethodMetadata;
|
||||
import org.springframework.hateoas.Link;
|
||||
import org.springframework.hateoas.Resource;
|
||||
import org.springframework.hateoas.ResourceSupport;
|
||||
@@ -67,8 +63,6 @@ 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.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.TypeUtils;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
@@ -81,12 +75,14 @@ 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;
|
||||
|
||||
/**
|
||||
* Configuration for hypermedia in HTTP endpoints.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@ManagementContextConfiguration
|
||||
@@ -97,14 +93,12 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
|
||||
@EnableConfigurationProperties(ResourceProperties.class)
|
||||
public class EndpointWebMvcHypermediaManagementContextConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "endpoints.hal.enabled", matchIfMissing = true)
|
||||
@ConditionalOnResource(resources = "classpath:/META-INF/resources/webjars/hal-browser/b7669f1-1")
|
||||
@Conditional(MissingSpringDataRestResourceCondition.class)
|
||||
public HalBrowserEndpoint halBrowserMvcEndpoint(
|
||||
@Conditional(HalBrowserCondition.class)
|
||||
@Bean
|
||||
public HalBrowserMvcEndpoint halBrowserMvcEndpoint(
|
||||
ManagementServerProperties management, ResourceProperties resources) {
|
||||
return new HalBrowserEndpoint(management,
|
||||
resources.getWelcomePage() != null ? "/hal" : "");
|
||||
return new HalBrowserMvcEndpoint(management);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -130,22 +124,19 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SpringBootCondition} to detect the Spring Data REST HAL browser.
|
||||
* {@link SpringBootCondition} to detect the HAL browser.
|
||||
*/
|
||||
@Configuration("EndpointHypermediaAutoConfiguration.MissingResourceCondition")
|
||||
@ConditionalOnResource(resources = "classpath:/META-INF/spring-data-rest/hal-browser/index.html")
|
||||
protected static class MissingSpringDataRestResourceCondition extends
|
||||
SpringBootCondition {
|
||||
protected static class HalBrowserCondition extends SpringBootCondition {
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context,
|
||||
AnnotatedTypeMetadata metadata) {
|
||||
if (context.getRegistry().containsBeanDefinition(
|
||||
"EndpointHypermediaAutoConfiguration.MissingResourceCondition")) {
|
||||
return ConditionOutcome.noMatch("Spring Data REST HAL browser found");
|
||||
}
|
||||
return ConditionOutcome.match("Spring Data REST HAL browser not found");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,98 +148,8 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
|
||||
public static class LinksConfiguration {
|
||||
|
||||
@Bean
|
||||
@Conditional(NotSpringDataRestHomePageCondition.class)
|
||||
public LinksMvcEndpoint linksMvcEndpoint(ResourceProperties resources) {
|
||||
String defaultPath = getDefaultPath(resources);
|
||||
return new LinksMvcEndpoint(defaultPath);
|
||||
}
|
||||
|
||||
private String getDefaultPath(ResourceProperties resources) {
|
||||
return resources.getWelcomePage() != null ? "/links" : "";
|
||||
}
|
||||
|
||||
private static class NotSpringDataRestHomePageCondition extends
|
||||
SpringBootCondition {
|
||||
|
||||
private static final String REST_CONFIGURATION_CLASS = "org.springframework."
|
||||
+ "data.rest.core.config.RepositoryRestConfiguration";
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context,
|
||||
AnnotatedTypeMetadata metadata) {
|
||||
if (!ClassUtils.isPresent(REST_CONFIGURATION_CLASS, null)) {
|
||||
return ConditionOutcome.match("Spring Data REST is not present");
|
||||
}
|
||||
return getMatchOutcome(context,
|
||||
ClassUtils.resolveClassName(REST_CONFIGURATION_CLASS, null));
|
||||
}
|
||||
|
||||
private ConditionOutcome getMatchOutcome(ConditionContext context,
|
||||
Class<?> configurationClass) {
|
||||
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
|
||||
if (beanFactory.getBeanNamesForType(configurationClass, true, false).length == 0) {
|
||||
return ConditionOutcome.match("Spring Data REST is not configured");
|
||||
}
|
||||
Environment environment = context.getEnvironment();
|
||||
String path = getManagementContextPath(beanFactory, environment);
|
||||
if (isHome(path)) {
|
||||
path = getProperty(environment, "endpoints.links.", "path");
|
||||
if (isHome(path)) {
|
||||
return ConditionOutcome.noMatch("Management context path "
|
||||
+ "is home and so is links path");
|
||||
}
|
||||
return ConditionOutcome.match("Management context path "
|
||||
+ "is home but links path is not: '" + path + "'");
|
||||
}
|
||||
// N.B. we don't cover the case where the user has Spring Data REST
|
||||
// but changes *its* home page - you'd have to instantiate the
|
||||
// RepositoryRestConfiguration and look at it's basePath for that.
|
||||
return ConditionOutcome.match("Management context path "
|
||||
+ "is not home: '" + path + "'");
|
||||
}
|
||||
|
||||
private String getManagementContextPath(
|
||||
ConfigurableListableBeanFactory beanFactory, Environment environment) {
|
||||
String path = getProperty(environment, "management.", "contextPath");
|
||||
if (path == null
|
||||
&& hasCustomBeanDefinition(beanFactory,
|
||||
ManagementServerProperties.class,
|
||||
ManagementServerPropertiesAutoConfiguration.class)) {
|
||||
path = beanFactory.getBean(ManagementServerProperties.class)
|
||||
.getContextPath();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private boolean isHome(String path) {
|
||||
return path == null || "".equals(path) || "/".equals(path);
|
||||
}
|
||||
|
||||
private String getProperty(Environment environment, String prefix, String name) {
|
||||
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
|
||||
environment, prefix);
|
||||
return resolver.getProperty(name, String.class);
|
||||
}
|
||||
|
||||
private <T> boolean hasCustomBeanDefinition(
|
||||
ConfigurableListableBeanFactory beanFactory, Class<T> type,
|
||||
Class<?> configClass) {
|
||||
String[] names = beanFactory.getBeanNamesForType(type, true, false);
|
||||
if (names == null || names.length != 1) {
|
||||
return false;
|
||||
}
|
||||
BeanDefinition definition = beanFactory.getBeanDefinition(names[0]);
|
||||
if (definition instanceof AnnotatedBeanDefinition) {
|
||||
MethodMetadata factoryMethodMetadata = ((AnnotatedBeanDefinition) definition)
|
||||
.getFactoryMethodMetadata();
|
||||
if (factoryMethodMetadata != null) {
|
||||
String className = factoryMethodMetadata.getDeclaringClassName();
|
||||
return !configClass.getName().equals(className);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return new LinksMvcEndpoint();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,29 +207,21 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
|
||||
private void beforeBodyWrite(Object body, ServletServerHttpRequest request) {
|
||||
Object pattern = request.getServletRequest().getAttribute(
|
||||
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
|
||||
if (pattern != null) {
|
||||
String path = pattern.toString();
|
||||
if (isHomePage(path) || isManagementPath(path) || isLinksPath(path)) {
|
||||
ResourceSupport resource = (ResourceSupport) body;
|
||||
if (isHomePage(path) && hasManagementPath()) {
|
||||
String rel = this.management.getContextPath().substring(1);
|
||||
resource.add(linkTo(
|
||||
EndpointWebMvcHypermediaManagementContextConfiguration.class)
|
||||
.slash(this.management.getContextPath()).withRel(rel));
|
||||
}
|
||||
else {
|
||||
this.linksEnhancer.addEndpointLinks(resource, "");
|
||||
}
|
||||
}
|
||||
if (pattern != null && body instanceof ResourceSupport) {
|
||||
beforeBodyWrite(pattern.toString(), (ResourceSupport) body);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasManagementPath() {
|
||||
return StringUtils.hasText(this.management.getContextPath());
|
||||
}
|
||||
|
||||
private boolean isManagementPath(String path) {
|
||||
return this.management.getContextPath().equals(path);
|
||||
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) {
|
||||
@@ -370,7 +263,7 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration {
|
||||
Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
Class<?> controllerType = returnType.getDeclaringClass();
|
||||
return !LinksMvcEndpoint.class.isAssignableFrom(controllerType)
|
||||
&& !HalBrowserEndpoint.class.isAssignableFrom(controllerType);
|
||||
&& !HalBrowserMvcEndpoint.class.isAssignableFrom(controllerType);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,96 +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.endpoint.mvc;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties;
|
||||
import org.springframework.boot.actuate.endpoint.Endpoint;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
|
||||
/**
|
||||
* {@link MvcEndpoint} to support the Spring Data HAL browser.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@ConfigurationProperties("endpoints.hal")
|
||||
public class HalBrowserEndpoint extends WebMvcConfigurerAdapter implements MvcEndpoint {
|
||||
|
||||
private static final String HAL_BROWSER_VERSION = "b7669f1-1";
|
||||
|
||||
private static final String HAL_BROWSER_LOCATION = "classpath:/META-INF/resources/webjars/hal-browser/"
|
||||
+ HAL_BROWSER_VERSION + "/";
|
||||
|
||||
private String path;
|
||||
|
||||
private ManagementServerProperties management;
|
||||
|
||||
private boolean sensitive = false;
|
||||
|
||||
public HalBrowserEndpoint(ManagementServerProperties management, String defaultPath) {
|
||||
this.management = management;
|
||||
this.path = defaultPath;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE)
|
||||
public String browse() {
|
||||
return "forward:" + this.management.getContextPath() + this.path
|
||||
+ "/browser.html";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "", produces = MediaType.TEXT_HTML_VALUE)
|
||||
public String redirect() {
|
||||
return "redirect:" + this.management.getContextPath() + this.path + "/#"
|
||||
+ this.management.getContextPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// Make sure the root path is not cached so the browser comes back for the JSON
|
||||
registry.addResourceHandler(this.management.getContextPath() + this.path + "/")
|
||||
.addResourceLocations(HAL_BROWSER_LOCATION).setCachePeriod(0);
|
||||
registry.addResourceHandler(this.management.getContextPath() + this.path + "/**")
|
||||
.addResourceLocations(HAL_BROWSER_LOCATION);
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
public void setSensitive(boolean sensitive) {
|
||||
this.sensitive = sensitive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSensitive() {
|
||||
return this.sensitive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Endpoint<?>> getEndpointType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.mvc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
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.http.MediaType;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
import org.springframework.web.servlet.resource.ResourceTransformer;
|
||||
import org.springframework.web.servlet.resource.ResourceTransformerChain;
|
||||
import org.springframework.web.servlet.resource.TransformedResource;
|
||||
|
||||
/**
|
||||
* {@link MvcEndpoint} to support the HAL browser.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@ConfigurationProperties("endpoints.hal")
|
||||
public class HalBrowserMvcEndpoint extends WebMvcConfigurerAdapter implements
|
||||
MvcEndpoint, ResourceLoaderAware {
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
private static HalBrowserLocation[] HAL_BROWSER_RESOURCE_LOCATIONS = {
|
||||
new HalBrowserLocation("classpath:/META-INF/spring-data-rest/hal-browser/",
|
||||
"index.html"),
|
||||
new HalBrowserLocation(
|
||||
"classpath:/META-INF/resources/webjars/hal-browser/b7669f1-1/",
|
||||
"browser.html") };
|
||||
|
||||
/**
|
||||
* Endpoint URL path.
|
||||
*/
|
||||
@NotNull
|
||||
@Pattern(regexp = "/[^/]*", message = "Path must start with /")
|
||||
private String path = "/hal";
|
||||
|
||||
/**
|
||||
* Enable security on the endpoint.
|
||||
*/
|
||||
private boolean sensitive = false;
|
||||
|
||||
/**
|
||||
* Enable the endpoint.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
private final ManagementServerProperties management;
|
||||
|
||||
@Autowired(required = false)
|
||||
private LinksMvcEndpoint linksMvcEndpoint;
|
||||
|
||||
private HalBrowserLocation location;
|
||||
|
||||
public HalBrowserMvcEndpoint(ManagementServerProperties management) {
|
||||
this.management = management;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.location = getHalBrowserLocation(resourceLoader);
|
||||
}
|
||||
|
||||
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
|
||||
public String browse(HttpServletRequest request) {
|
||||
String contextPath = this.management.getContextPath() + this.path + "/";
|
||||
if (request.getRequestURI().endsWith("/")) {
|
||||
return "forward:" + contextPath + this.location.getHtmlFile();
|
||||
}
|
||||
return "redirect:" + contextPath;
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
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 boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Endpoint<?>> getEndpointType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static HalBrowserLocation getHalBrowserLocation(ResourceLoader resourceLoader) {
|
||||
for (HalBrowserLocation candidate : HAL_BROWSER_RESOURCE_LOCATIONS) {
|
||||
try {
|
||||
Resource resource = resourceLoader.getResource(candidate.toString());
|
||||
if (resource != null && resource.exists()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ResourceTransformer} to change the initial link location.
|
||||
*/
|
||||
private class InitialUrlTransformer implements ResourceTransformer {
|
||||
|
||||
@Override
|
||||
public Resource transform(HttpServletRequest request, Resource resource,
|
||||
ResourceTransformerChain transformerChain) throws IOException {
|
||||
resource = transformerChain.transform(request, resource);
|
||||
if (resource.getFilename().equalsIgnoreCase(
|
||||
HalBrowserMvcEndpoint.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();
|
||||
content = content.replace("entryPoint: '/'", "entryPoint: '" + initialLink
|
||||
+ "'");
|
||||
return new TransformedResource(resource, content.getBytes(DEFAULT_CHARSET));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class HalBrowserLocation {
|
||||
|
||||
private final String resourceLocation;
|
||||
|
||||
private final String htmlFile;
|
||||
|
||||
public HalBrowserLocation(String resourceLocation, String html) {
|
||||
this.resourceLocation = resourceLocation;
|
||||
this.htmlFile = html;
|
||||
}
|
||||
|
||||
public String getResourceLocation() {
|
||||
return this.resourceLocation;
|
||||
}
|
||||
|
||||
public String getHtmlFile() {
|
||||
return this.htmlFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.resourceLocation + this.htmlFile;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
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;
|
||||
@@ -32,14 +35,27 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
@ConfigurationProperties("endpoints.links")
|
||||
public class LinksMvcEndpoint implements MvcEndpoint {
|
||||
|
||||
private String path;
|
||||
/**
|
||||
* Endpoint URL path.
|
||||
*/
|
||||
@NotNull
|
||||
@Pattern(regexp = "/[^/]*", message = "Path must start with /")
|
||||
private String path = "/links";
|
||||
|
||||
/**
|
||||
* Enable security on the endpoint.
|
||||
*/
|
||||
private boolean sensitive = false;
|
||||
|
||||
public LinksMvcEndpoint(String defaultPath) {
|
||||
this.path = defaultPath;
|
||||
/**
|
||||
* Enable the endpoint.
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
public LinksMvcEndpoint() {
|
||||
}
|
||||
|
||||
@RequestMapping(value = { "/", "" }, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@ResponseBody
|
||||
public ResourceSupport links() {
|
||||
return new ResourceSupport();
|
||||
@@ -63,6 +79,14 @@ public class LinksMvcEndpoint implements MvcEndpoint {
|
||||
this.sensitive = sensitive;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Endpoint<?>> getEndpointType() {
|
||||
return null;
|
||||
|
||||
@@ -72,7 +72,7 @@ public class BrowserPathHypermediaIntegrationTests {
|
||||
public void redirect() throws Exception {
|
||||
this.mockMvc.perform(get("/hal").accept(MediaType.TEXT_HTML))
|
||||
.andExpect(status().isFound())
|
||||
.andExpect(header().string("location", "/hal/#"));
|
||||
.andExpect(header().string("location", "/hal/"));
|
||||
}
|
||||
|
||||
@MinimalActuatorHypermediaApplication
|
||||
|
||||
@@ -70,7 +70,7 @@ public class ContextPathHypermediaIntegrationTests {
|
||||
|
||||
@Test
|
||||
public void links() throws Exception {
|
||||
this.mockMvc.perform(get("/admin").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/admin/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$._links").exists());
|
||||
}
|
||||
|
||||
@@ -89,10 +89,13 @@ public class ContextPathHypermediaIntegrationTests {
|
||||
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;
|
||||
path = path.length() > 0 ? path : "self";
|
||||
this.mockMvc
|
||||
.perform(get("/admin").accept(MediaType.APPLICATION_JSON))
|
||||
.perform(get("/admin/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(
|
||||
jsonPath("$._links.%s.href", path).value(
|
||||
|
||||
@@ -70,8 +70,11 @@ public class CustomHomepageHypermediaIntegrationTests {
|
||||
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("/").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.%s.href", path).exists());
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public class ServerContextPathHypermediaIntegrationTests {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
|
||||
ResponseEntity<String> entity = new TestRestTemplate().exchange(
|
||||
"http://localhost:" + this.port + "/spring/", HttpMethod.GET,
|
||||
"http://localhost:" + this.port + "/spring/hal/", HttpMethod.GET,
|
||||
new HttpEntity<Void>(null, headers), String.class);
|
||||
assertEquals(HttpStatus.OK, entity.getStatusCode());
|
||||
assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("<title"));
|
||||
|
||||
@@ -58,7 +58,7 @@ public class ServerPortHypermediaIntegrationTests {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
|
||||
ResponseEntity<String> entity = new TestRestTemplate().exchange(
|
||||
"http://localhost:" + this.port + "/", HttpMethod.GET,
|
||||
"http://localhost:" + this.port + "/links", HttpMethod.GET,
|
||||
new HttpEntity<Void>(null, headers), String.class);
|
||||
assertEquals(HttpStatus.OK, entity.getStatusCode());
|
||||
assertTrue("Wrong body: " + entity.getBody(),
|
||||
@@ -70,7 +70,7 @@ public class ServerPortHypermediaIntegrationTests {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
|
||||
ResponseEntity<String> entity = new TestRestTemplate().exchange(
|
||||
"http://localhost:" + this.port + "/", HttpMethod.GET,
|
||||
"http://localhost:" + this.port + "/hal/", HttpMethod.GET,
|
||||
new HttpEntity<Void>(null, headers), String.class);
|
||||
assertEquals(HttpStatus.OK, entity.getStatusCode());
|
||||
assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("<title"));
|
||||
|
||||
@@ -62,16 +62,17 @@ public class VanillaHypermediaIntegrationTests {
|
||||
|
||||
@Test
|
||||
public void links() throws Exception {
|
||||
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$._links").exists())
|
||||
.andExpect(header().doesNotExist("cache-control"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void browser() throws Exception {
|
||||
MvcResult response = this.mockMvc.perform(get("/").accept(MediaType.TEXT_HTML))
|
||||
MvcResult response = this.mockMvc
|
||||
.perform(get("/hal/").accept(MediaType.TEXT_HTML))
|
||||
.andExpect(status().isOk()).andReturn();
|
||||
assertEquals("/browser.html", response.getResponse().getForwardedUrl());
|
||||
assertEquals("/hal/browser.html", response.getResponse().getForwardedUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -94,8 +95,11 @@ public class VanillaHypermediaIntegrationTests {
|
||||
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("/").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$._links.%s.href", path).exists());
|
||||
}
|
||||
@@ -105,8 +109,7 @@ public class VanillaHypermediaIntegrationTests {
|
||||
public void endpointsEachHaveSelf() throws Exception {
|
||||
for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) {
|
||||
String path = endpoint.getPath();
|
||||
if ("/hal".equals(path) || "/logfile".equals(path)) {
|
||||
// TODO: /logfile shouldn't be active anyway
|
||||
if ("/hal".equals(path)) {
|
||||
continue;
|
||||
}
|
||||
path = path.length() > 0 ? path : "/";
|
||||
|
||||
@@ -59,10 +59,10 @@ public class SampleHypermediaJpaApplicationCustomLinksPathIntegrationTests {
|
||||
|
||||
@Test
|
||||
public void links() throws Exception {
|
||||
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$._links").exists())
|
||||
.andExpect(jsonPath("$._links.health").exists())
|
||||
.andExpect(jsonPath("$._links.books").exists());
|
||||
.andExpect(jsonPath("$._links.books").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -61,8 +61,7 @@ public class SampleHypermediaJpaApplicationSharedRootIntegrationTests {
|
||||
public void home() throws Exception {
|
||||
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$._links").exists())
|
||||
.andExpect(jsonPath("$._links.health").exists())
|
||||
.andExpect(jsonPath("$._links.admin").exists())
|
||||
.andExpect(jsonPath("$._links.actuator").exists())
|
||||
.andExpect(jsonPath("$._links.books").exists());
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ public class SampleHypermediaJpaApplicationVanillaIntegrationTests {
|
||||
|
||||
@Test
|
||||
public void adminLinks() throws Exception {
|
||||
this.mockMvc.perform(get("/admin").accept(MediaType.APPLICATION_JSON))
|
||||
this.mockMvc.perform(get("/admin/links").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("$._links").exists());
|
||||
}
|
||||
|
||||
|
||||
@@ -48,27 +48,26 @@ public class SampleHypermediaApplicationHomePageTests {
|
||||
public void home() {
|
||||
String response = new TestRestTemplate().getForObject("http://localhost:"
|
||||
+ this.port, String.class);
|
||||
assertTrue("Wrong body: " + response, response.contains("\"_links\":"));
|
||||
assertTrue("Wrong body: " + response, response.contains("\"curies\":"));
|
||||
assertTrue("Wrong body: " + response, response.contains("404"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void homeWithJson() throws Exception {
|
||||
public void linksWithJson() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
|
||||
ResponseEntity<String> response = new TestRestTemplate().exchange(
|
||||
new RequestEntity<Void>(headers, HttpMethod.GET, new URI(
|
||||
"http://localhost:" + this.port + "/")), String.class);
|
||||
"http://localhost:" + this.port + "/links")), String.class);
|
||||
assertTrue("Wrong body: " + response, response.getBody().contains("\"_links\":"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void homeWithHtml() throws Exception {
|
||||
public void halWithHtml() throws Exception {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
|
||||
ResponseEntity<String> response = new TestRestTemplate().exchange(
|
||||
new RequestEntity<Void>(headers, HttpMethod.GET, new URI(
|
||||
"http://localhost:" + this.port)), String.class);
|
||||
"http://localhost:" + this.port + "/hal/")), String.class);
|
||||
assertTrue("Wrong body: " + response, response.getBody().contains("HAL Browser"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user