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:
Phillip Webb
2015-08-05 22:17:29 -07:00
parent 44aacd9559
commit 28f32da74f
15 changed files with 310 additions and 264 deletions

View File

@@ -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"));
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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(

View File

@@ -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());
}

View File

@@ -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"));

View File

@@ -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"));

View File

@@ -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 : "/";

View File

@@ -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

View File

@@ -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());
}

View File

@@ -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());
}

View File

@@ -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"));
}