diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java index f61fcbc4905..1482d0220a7 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure; import java.io.IOException; import java.lang.reflect.Type; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -205,10 +206,9 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { * Controller advice that adds links to the existing Actuator endpoints. By default * all the top-level resources are enhanced with a "self" link. Those resources that * could not be enhanced (e.g. "/env/{name}") because their values are "primitive" are - * ignored. Those that have values of type Collection (e.g. /trace) are transformed in - * to maps, and the original collection value is added with a key equal to the - * endpoint name. + * ignored. */ + @ConditionalOnProperty(prefix = "endpoints.hypermedia", name = "enabled", matchIfMissing = false) @ControllerAdvice(assignableTypes = MvcEndpoint.class) public static class MvcEndpointAdvice implements ResponseBodyAdvice { @@ -245,6 +245,10 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { // 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 converter = findConverter(selectedConverterType, selectedContentType); if (converter == null || isHypermediaDisabled(returnType)) { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java new file mode 100644 index 00000000000..ffb22defe24 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointDisabledIntegrationTests.java @@ -0,0 +1,109 @@ +/* + * 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.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.MinimalActuatorHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpointDisabledIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for {@link HalBrowserMvcEndpoint} + * + * @author Dave Syer + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(SpringBootHypermediaApplication.class) +@WebAppConfiguration +@DirtiesContext +public class HalBrowserMvcEndpointDisabledIntegrationTests { + + @Autowired + private WebApplicationContext context; + + @Autowired + private MvcEndpoints mvcEndpoints; + + private MockMvc mockMvc; + + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + @Test + public void linksOnActuator() throws Exception { + this.mockMvc.perform(get("/actuator").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("/actuator/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()).andReturn(); + assertEquals("/actuator/browser.html", response.getResponse().getForwardedUrl()); + } + + @Test + public void endpointsDoNotHaveLinks() throws Exception { + for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { + String path = endpoint.getPath(); + if ("/actuator".equals(path)) { + continue; + } + path = path.length() > 0 ? path : "/"; + this.mockMvc.perform(get(path).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links").doesNotExist()); + } + } + + @MinimalActuatorHypermediaApplication + @Configuration + public static class SpringBootHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, + new String[] { "--endpoints.hypermedia.enabled=false" }); + } + + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java index b6436ef5c9c..0afafcb4480 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointManagementContextPathIntegrationTests.java @@ -93,10 +93,8 @@ public class HalBrowserMvcEndpointManagementContextPathIntegrationTests { @Test public void trace() throws Exception { this.mockMvc.perform(get("/admin/trace").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._links.self.href") - .value("http://localhost/admin/trace")) - .andExpect(jsonPath("$.content").isArray()); + .andExpect(status().isOk()).andExpect(jsonPath("$._links").doesNotExist()) + .andExpect(jsonPath("$").isArray()); } @Test diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java index 5eb50411d2e..f9f0c9c4a5b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserMvcEndpointVanillaIntegrationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.actuate.endpoint.mvc; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -27,6 +31,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -50,6 +55,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @SpringApplicationConfiguration(SpringBootHypermediaApplication.class) @WebAppConfiguration @DirtiesContext +@TestPropertySource(properties = "endpoints.hypermedia.enabled=true") public class HalBrowserMvcEndpointVanillaIntegrationTests { @Autowired @@ -83,9 +89,8 @@ public class HalBrowserMvcEndpointVanillaIntegrationTests { @Test public void trace() throws Exception { this.mockMvc.perform(get("/trace").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$._links.self.href").value("http://localhost/trace")) - .andExpect(jsonPath("$.content").isArray()); + .andExpect(status().isOk()).andExpect(jsonPath("$._links").doesNotExist()) + .andExpect(jsonPath("$").isArray()); } @Test @@ -111,8 +116,13 @@ public class HalBrowserMvcEndpointVanillaIntegrationTests { @Test public void endpointsEachHaveSelf() throws Exception { + Set collections = new HashSet( + Arrays.asList("/trace", "/beans", "/dump")); for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { String path = endpoint.getPath(); + if (collections.contains(path)) { + continue; + } path = path.length() > 0 ? path : "/"; this.mockMvc.perform(get(path).accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andExpect(jsonPath("$._links.self.href") diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java index 2b2a54023d8..9158ce0f85f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointIntegrationTests.java @@ -70,7 +70,7 @@ public class MvcEndpointIntegrationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(DefaultConfiguration.class); MockMvc mockMvc = createMockMvc(); - mockMvc.perform(get("/beans")).andExpect(content().string(startsWith("{\""))); + mockMvc.perform(get("/mappings")).andExpect(content().string(startsWith("{\""))); } @Test @@ -199,7 +199,7 @@ public class MvcEndpointIntegrationTests { EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.serialization.indent-output:true"); MockMvc mockMvc = createMockMvc(); - mockMvc.perform(get("/beans")) + mockMvc.perform(get("/mappings")) .andExpect(content().string(startsWith("{" + LINE_SEPARATOR))); } diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java index df387d99028..536743fc094 100644 --- a/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/sample/hypermedia/gson/SampleHypermediaGsonApplicationTests.java @@ -37,7 +37,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(SampleHypermediaGsonApplication.class) @WebAppConfiguration -@TestPropertySource(properties = "endpoints.health.sensitive: false") +@TestPropertySource(properties = "endpoints.hypermedia.enabled: true") public class SampleHypermediaGsonApplicationTests { @Autowired @@ -61,9 +61,8 @@ public class SampleHypermediaGsonApplicationTests { @Test public void trace() throws Exception { this.mockMvc.perform(get("/trace").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.links[0].href").value("http://localhost/trace")) - .andExpect(jsonPath("$.content").isArray()); + .andExpect(status().isOk()).andExpect(jsonPath("$.links").doesNotExist()) + .andExpect(jsonPath("$").isArray()); } @Test diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/sample/hypermedia/jpa/SampleHypermediaJpaApplicationIntegrationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/sample/hypermedia/jpa/SampleHypermediaJpaApplicationIntegrationTests.java index 95a2e3fb96e..67896386b71 100644 --- a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/sample/hypermedia/jpa/SampleHypermediaJpaApplicationIntegrationTests.java +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/sample/hypermedia/jpa/SampleHypermediaJpaApplicationIntegrationTests.java @@ -61,7 +61,8 @@ public class SampleHypermediaJpaApplicationIntegrationTests { @Test public void health() throws Exception { this.mockMvc.perform(get("/admin/health").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links").doesNotExist()); } @Test