Commit 28f32da7 authored by Phillip Webb's avatar Phillip Webb

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
parent 44aacd95
......@@ -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"));
}
......
/*
* 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;
}
}
/*
* 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"));
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment