diff --git a/pom.xml b/pom.xml index 0e9f48599c..50d046ea78 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,7 @@ spring-boot-devtools spring-boot-docs spring-boot-starters + spring-boot-actuator-docs spring-boot-cli diff --git a/spring-boot-actuator-docs/pom.xml b/spring-boot-actuator-docs/pom.xml new file mode 100644 index 0000000000..76e893ccb3 --- /dev/null +++ b/spring-boot-actuator-docs/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + spring-boot-actuator-docs + jar + + spring-boot-actuator-docs + Docs project for Spring Boot + + + org.springframework.boot + spring-boot-parent + 1.3.0.BUILD-SNAPSHOT + + + + UTF-8 + 1.7 + + + + + org.springframework.boot + spring-boot-starter-web + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-starter-hateoas + true + + + org.springframework.restdocs + spring-restdocs + 1.0.0.M1 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-groovy-templates + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Documentation.java + + + ${project.build.directory}/generated-snippets + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.2 + + + generate-docs + prepare-package + + process-asciidoc + + + html + book + index.adoc + + ${project.build.directory}/generated-snippets + ${project.build.directory}/../src/main/asciidoc + + + + + + + maven-resources-plugin + + + copy-resources + prepare-package + + copy-resources + + + ${project.build.outputDirectory}/META-INF/resources/spring-boot-actuator/docs + + + ${project.build.directory}/generated-docs + + + + + + + + + + diff --git a/spring-boot-actuator-docs/src/main/asciidoc/autoconfig.adoc b/spring-boot-actuator-docs/src/main/asciidoc/autoconfig.adoc new file mode 100644 index 0000000000..5208f8fd70 --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/autoconfig.adoc @@ -0,0 +1,26 @@ +=== /autoconfig + +This endpoint is a report on the Spring Boot Autoconfiguration process +that happened when your application started up. It lists all the +`@Conditional` annotations that were evaluated as the context started +and in each case it gives an indication of if (and why) the condition +matched. A positive match results in a bean being included in the context, +and a negative result means the opposite (the beans's class may not even +be loaded). + +The report is split into 2 parts, positive matches first, and then negative. +If the context is a hierarchy, there is also a separate report on the parent +context with the same format (and recursively up to the top of the hierarchy). + +NOTE: the report is actually about `@Conditional` evaluation not autoconfiguration +per se, but most autoconfiguration features use `@Conditional` heavily, so there is +a lot of overlap. + +Example curl request: +include::{generated}/autoconfig/curl-request.adoc[] + +Example HTTP request: +include::{generated}/autoconfig/http-request.adoc[] + +Example HTTP response: +include::{generated}/autoconfig/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/beans.adoc b/spring-boot-actuator-docs/src/main/asciidoc/beans.adoc new file mode 100644 index 0000000000..e725f3f21d --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/beans.adoc @@ -0,0 +1,17 @@ +=== /beans + +This endpoint is a report on the Spring Boot `ApplicationContext`. It lists +the beans in the context and their dependencies, detailing the names and +concrete classes of each bean. + +NOTE: some beans are pure configuration (any class that is annotated +`@Configuration`). + +Example curl request: +include::{generated}/beans/curl-request.adoc[] + +Example HTTP request: +include::{generated}/beans/http-request.adoc[] + +Example HTTP response: +include::{generated}/beans/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/configprops.adoc b/spring-boot-actuator-docs/src/main/asciidoc/configprops.adoc new file mode 100644 index 0000000000..32b3a5b990 --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/configprops.adoc @@ -0,0 +1,19 @@ +=== /configprops + +This endpoint is a report on the Spring Boot `@ConfigurationProperties` +beans. Beans with this annotation are bound to the `Environment` on +startup, so they reflect the externalised configuration of the application. +Beans are listed by name. +A bean that is added using `@EnableConfigurationProperties` will have +a conventional name: `.CONFIGURATION_PROPERTIES`, where +`` is the environment key prefix specified in the +`@ConfigurationProperties` annotation. + +Example curl request: +include::{generated}/configprops/curl-request.adoc[] + +Example HTTP request: +include::{generated}/configprops/http-request.adoc[] + +Example HTTP response: +include::{generated}/configprops/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/dump.adoc b/spring-boot-actuator-docs/src/main/asciidoc/dump.adoc new file mode 100644 index 0000000000..4a5ac589e4 --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/dump.adoc @@ -0,0 +1,19 @@ +=== /dump + +This endpoint is a thread dump: the result is a list of threads each with +their name, monitor state and stack. It is the same information as you would +get from `kill -3` of a running Java process. Can be very useful for detecting +issues at runtime, especially sluggish behaviour caused by threads blocked +by slow or unavailable I/O (e.g. if a connection pool is exhausted). + +NOTE: some `SecurityManager` implementations might prevent this endpoint +from working. + +Example curl request: +include::{generated}/dump/curl-request.adoc[] + +Example HTTP request: +include::{generated}/dump/http-request.adoc[] + +Example HTTP response: +include::{generated}/dump/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/env.adoc b/spring-boot-actuator-docs/src/main/asciidoc/env.adoc new file mode 100644 index 0000000000..c0266a0c1b --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/env.adoc @@ -0,0 +1,17 @@ +=== /env + +This endpoint is a dump of the Spring `Environment`. It lists the active +profiles and all the `PropertySources` in the `Environment` (the ones that +are listed first take precedence when binding to `@ConfigurationProperties` +or `@Value`). Normally you will see the Java `System` properties and the +OS environment variables in their own `PropertySources` plus any `.properties` +or `.yml` files used to configure the application on start up. + +Example curl request: +include::{generated}/env/curl-request.adoc[] + +Example HTTP request: +include::{generated}/env/http-request.adoc[] + +Example HTTP response: +include::{generated}/env/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/health.adoc b/spring-boot-actuator-docs/src/main/asciidoc/health.adoc new file mode 100644 index 0000000000..54a292378d --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/health.adoc @@ -0,0 +1,22 @@ +=== /health + +This endpoint is an indication of the health of the application. +It has an overall status ("UP", "DOWN" etc.), which is the only thing +you see unless either you are authenticated or the endpoint is marked +as `sensitive=false` (`endpoints.health.sensitive=false`). + +The HTTP code in the response reflects the status (e.g. "UP" = 200, +"OUT_OF_SERVICE"=503, "DOWN"=503). The mappings can be changed by +configuring `endpoints.health.mapping.=XXX`. + +Example curl request: +include::{generated}/health/curl-request.adoc[] + +Example HTTP request: +include::{generated}/health/http-request.adoc[] + +Example HTTP response: +include::{generated}/health/http-response.adoc[] + +Example HTTP response with `endpoints.health.sensitive=false`: +include::{generated}/health/unsensitive/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/images/hal-browser.png b/spring-boot-actuator-docs/src/main/asciidoc/images/hal-browser.png new file mode 100644 index 0000000000..2cab51e51d Binary files /dev/null and b/spring-boot-actuator-docs/src/main/asciidoc/images/hal-browser.png differ diff --git a/spring-boot-actuator-docs/src/main/asciidoc/index.adoc b/spring-boot-actuator-docs/src/main/asciidoc/index.adoc new file mode 100644 index 0000000000..b56959055b --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/index.adoc @@ -0,0 +1,148 @@ += Spring Boot Actuator Endpoints +:toc: left +:idprefix: spring_boot_actuator_ + +Actuator endpoints allow you to monitor and interact with your application. Spring Boot +includes a number of built-in endpoints and you can also add your own. For example the +`health` endpoint provides basic application health information. + +The way that endpoints are exposed will depend on the type of technology that you choose. +Most applications choose HTTP monitoring, where the ID of the endpoint is mapped +to a URL. For example, by default, the `health` endpoint will be mapped to `/health`. + +== List of Endpoints + +include::{generated}/endpoints.adoc[] + +=== /logfile + +This endpoint (if available) contains the plain text logfile configured by the user +using `logging.file` or `logging.path` (by default logs are only emitted on stdout +so one of these properties has to be set for this endpoint to be active). + +Example curl request: +include::{generated}/logfile/curl-request.adoc[] + +Example HTTP request: +include::{generated}/logfile/http-request.adoc[] + +Example HTTP response: +include::{generated}/logfile/http-response.adoc[] + +=== /docs + +This endpoint (if available) contains HTML documemtation for the other endpoints. Its path +can be "/docs" (if there is an existing home page) or "/" (otherwise, including if the +HAL browser is not active). + +== Hypermedia Support + +If https://projects.spring.io/spring-hateoas[Spring HATEOAS] is enabled +(i.e. if it is on the classpath by default) then the Actuator +endpoint responses are enhanced with hypermedia in the form of "links". The default +media type for responses is http://stateless.co/hal_specification.html[HAL], resulting +in each resource having an extra property called "_links". You can change the +media type to another one supported by Spring HATEOAS by providing your own +`@EnableHypermedia` annotation and custom providers as necessary. + +Example enhanced "/metrics" endpoint with additional "_links": + +include::{generated}/metrics/hypermedia/http-response.adoc[] + +WARNING: Beware of Actuator endpoint paths clashing with application endpoints. +The easiest way to avoid that is to use a `management.contextPath`, e.g. "/admin". + +TIP: You can disable the hypermedia support in Actuator endpoints by setting +`endpoints.links.enabled=false`. + +=== Default home page +If the `management.contextPath` is empty, or if the home page provided +by the application happens to be a response body of type `ResourceSupport`, then it will +be enhanced with links to the actuator endpoints. The latter would happen for instance +if you use Spring Data REST to expose `Repository` endpoints. + +Example vanilla "/" endpoint if the `management.contextPath` is empty (the "/admin" +page would be the same with different links if `management.contextPath=/admin`): + +include::{generated}/admin/http-response.adoc[] + +=== Endpoints with format changes +Some endpoints in their "raw" form consist of an array (e.g. the "/beans" and the "/trace" endpoints). +These need to be converted to objects (maps) before they can be enhanced with +links, so their contents are inserted as a field named "content". +Example enhanced "/beans" endpoint with additional "_links": + +include::{generated}/beans/hypermedia/http-response.adoc[] + +== HAL Browser + +If Hypermedia is enabled and the HAL format is in use (which is the default), then +you can provide a browser for the resources by including a dependency +on the https://github.com/mikekelly/hal-browser[HAL browser] webjar. +For example in Maven: + +[source,xml] +---- + + org.webjars + hal-browser + +---- + + +or in Gradle + +[source,groovy] +---- +dependencies { + ... + compile('org.webjars:hal-browser') + ... +} +---- + +NOTE: if you are using Spring Data REST, then a dependency on the `spring-data-rest-hal-browser` +will have an equivalent effect. + +If you do that then a new endpoint will appear at "/" or "/hal" (relative to the `management.contextPath`) +serving up a static HTML page with some JavaScript that lets you browse the available +resources. The default endpoint path depends on whether or not there is already a static home page +("index.html") - if there is not and the `management.contextPath` is empty, then the HAL browser +shows up on the home page. Example: + +image::hal-browser.png[HAL Browser] + +TIP: The endpoint path can always, as with all MVC endpoints, be overridden using +`endpoints.hal.path=/yourpath` (note the leading slash). + +== Actuator Documentation Browser + +You can also provide a browser for the standard generated documentation +for the Actuator endpoints by including a dependency on the documentation jar. +For example in Maven: + +[source,xml] +---- + + org.springframework.boot + spring-boot-hypermedia-docs + +---- + + +or in Gradle + +[source,groovy] +---- +dependencies { + ... + compile('org.springframework.boot:spring-boot-hypermedia-docs') + ... +} +---- + +If you do that then a new endpoint at "/" or "/docs" (relative to the `management.contextPath`) +will serve up a static HTML page with this documentation in it. The default endpoint path depends +on whether or not there is already a static home page +("index.html" or a HAL browser) - if there is not and the `management.contextPath` is empty, +then the docs browser shows up on the home page. \ No newline at end of file diff --git a/spring-boot-actuator-docs/src/main/asciidoc/info.adoc b/spring-boot-actuator-docs/src/main/asciidoc/info.adoc new file mode 100644 index 0000000000..af1c191462 --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/info.adoc @@ -0,0 +1,16 @@ +=== /info + +This endpoint is empty and marked as `sensitive=false` +by default (so it is unauthenticated by default if Spring +Security is in use). It reflects the content of the `info.*` properties +in the `Environment`, as well as the properties in `git.properties` +if such a file exists in the root of the classpath. + +Example curl request: +include::{generated}/info/curl-request.adoc[] + +Example HTTP request: +include::{generated}/info/http-request.adoc[] + +Example HTTP response: +include::{generated}/info/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/mappings.adoc b/spring-boot-actuator-docs/src/main/asciidoc/mappings.adoc new file mode 100644 index 0000000000..685d227231 --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/mappings.adoc @@ -0,0 +1,14 @@ +=== /mappings + +This endpoint lists the Spring MVC request mappings, so users can +see the handlers registered for requests by path, method, media type, +etc. + +Example curl request: +include::{generated}/mappings/curl-request.adoc[] + +Example HTTP request: +include::{generated}/mappings/http-request.adoc[] + +Example HTTP response: +include::{generated}/mappings/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/metrics.adoc b/spring-boot-actuator-docs/src/main/asciidoc/metrics.adoc new file mode 100644 index 0000000000..42c61f6c34 --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/metrics.adoc @@ -0,0 +1,17 @@ +=== /metrics + +This endpoint lists the public metrics exposed by the application. +By default this includes all the counters in the `CounterService` +and all the gauges in the `GaugeService`, plus a few JVM metrics about +memory and uptime. Users can register additional sources by creating +beans of type `PublicMetrics` and/or by registering counters and +gauges. + +Example curl request: +include::{generated}/metrics/curl-request.adoc[] + +Example HTTP request: +include::{generated}/metrics/http-request.adoc[] + +Example HTTP response: +include::{generated}/metrics/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/main/asciidoc/trace.adoc b/spring-boot-actuator-docs/src/main/asciidoc/trace.adoc new file mode 100644 index 0000000000..c35b66a829 --- /dev/null +++ b/spring-boot-actuator-docs/src/main/asciidoc/trace.adoc @@ -0,0 +1,16 @@ +=== /trace + +This endpoint lists contents of the `TraceRepository` (which +users can override by providing a bean of that type, or by +injecting that bean and adding stuff to it). By default +it is the last 100 HTTP requests, including all headers in the +request and response, and the path and HTTP status. + +Example curl request: +include::{generated}/trace/curl-request.adoc[] + +Example HTTP request: +include::{generated}/trace/http-request.adoc[] + +Example HTTP response: +include::{generated}/trace/http-response.adoc[] diff --git a/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/EndpointDocumentation.java b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/EndpointDocumentation.java new file mode 100644 index 0000000000..1b02d66ea4 --- /dev/null +++ b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/EndpointDocumentation.java @@ -0,0 +1,170 @@ +package org.springframework.boot.actuate.hypermedia.test; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import groovy.text.Template; +import groovy.text.TemplateEngine; + +import java.io.File; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.Filter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.test.SpringApplicationConfiguration; +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; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultHandler; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.StringUtils; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", + "endpoints.health.sensitive=true", "endpoints.links.enabled=false" }) +@DirtiesContext +public class EndpointDocumentation { + + @Autowired + private WebApplicationContext context; + + @Autowired + private MvcEndpoints mvcEndpoints; + + @Autowired + @Qualifier("metricFilter") + private Filter metricFilter; + + @Autowired + @Qualifier("webRequestLoggingFilter") + private Filter traceFilter; + + @Autowired + private TemplateEngine templates; + + @Value("${org.springframework.restdocs.outputDir:${user.dir}/target/generated-snippets}") + private String restdocsOutputDir; + + private MockMvc mockMvc; + + @Before + public void setUp() { + System.setProperty("org.springframework.restdocs.outputDir", + this.restdocsOutputDir); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .addFilters(this.metricFilter, this.traceFilter) + .apply(documentationConfiguration()).build(); + } + + @Test + public void logfile() throws Exception { + this.mockMvc.perform(get("/logfile").accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andDo(document("logfile")); + } + + @Test + public void endpoints() throws Exception { + + final File docs = new File("src/main/asciidoc"); + + final Map model = new LinkedHashMap(); + final List endpoints = new ArrayList(); + model.put("endpoints", endpoints); + for (MvcEndpoint endpoint : getEndpoints()) { + final String endpointPath = StringUtils.hasText(endpoint.getPath()) ? endpoint + .getPath() : "/"; + + if (!endpointPath.equals("/docs") && !endpointPath.equals("/logfile")) { + String output = endpointPath.substring(1); + output = output.length() > 0 ? output : "./"; + this.mockMvc + .perform(get(endpointPath).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document(output)) + .andDo(new ResultHandler() { + @Override + public void handle(MvcResult mvcResult) throws Exception { + EndpointDoc endpoint = new EndpointDoc(docs, endpointPath); + endpoints.add(endpoint); + } + }); + } + } + File file = new File(this.restdocsOutputDir + "/endpoints.adoc"); + file.getParentFile().mkdirs(); + PrintWriter writer = new PrintWriter(file, "UTF-8"); + try { + Template template = this.templates.createTemplate(new File( + "src/test/resources/templates/endpoints.adoc.tpl")); + template.make(model).writeTo(writer); + } + finally { + writer.close(); + } + } + + private Collection getEndpoints() { + List endpoints = new ArrayList( + this.mvcEndpoints.getEndpoints()); + Collections.sort(endpoints, new Comparator() { + @Override + public int compare(MvcEndpoint o1, MvcEndpoint o2) { + return o1.getPath().compareTo(o2.getPath()); + } + }); + return endpoints; + } + + public static class EndpointDoc { + + private String path; + private String custom; + private String title; + + public EndpointDoc(File rootDir, String path) { + this.title = path; + this.path = path.equals("/") ? "" : path; + String custom = path.substring(1) + ".adoc"; + if (new File(rootDir, custom).exists()) { + this.custom = custom; + } + } + + public String getTitle() { + return this.title; + } + + public String getPath() { + return this.path; + } + + public String getCustom() { + return this.custom; + } + + } + +} diff --git a/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/HealthEndpointDocumentation.java b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/HealthEndpointDocumentation.java new file mode 100644 index 0000000000..d9f6ba26cf --- /dev/null +++ b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/HealthEndpointDocumentation.java @@ -0,0 +1,63 @@ +package org.springframework.boot.actuate.hypermedia.test; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import groovy.text.TemplateEngine; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.test.SpringApplicationConfiguration; +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; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", +"endpoints.health.sensitive=false" }) +@DirtiesContext +public class HealthEndpointDocumentation { + + @Autowired + private WebApplicationContext context; + + @Autowired + private MvcEndpoints mvcEndpoints; + + @Autowired + private TemplateEngine templates; + + @Value("${org.springframework.restdocs.outputDir:target/generated-snippets}") + private String restdocsOutputDir; + + private MockMvc mockMvc; + + @Before + public void setUp() { + System.setProperty("org.springframework.restdocs.outputDir", + this.restdocsOutputDir); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration()) + .build(); + } + + @Test + public void health() throws Exception { + this.mockMvc.perform(get("/health").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("health/unsensitive")); + } + +} diff --git a/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/HypermediaEndpointDocumentation.java b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/HypermediaEndpointDocumentation.java new file mode 100644 index 0000000000..1ca6a2e2ae --- /dev/null +++ b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/HypermediaEndpointDocumentation.java @@ -0,0 +1,76 @@ +package org.springframework.boot.actuate.hypermedia.test; + +import static org.springframework.restdocs.RestDocumentation.document; +import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import groovy.text.TemplateEngine; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.test.SpringApplicationConfiguration; +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; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@TestPropertySource(properties = "spring.jackson.serialization.indent_output=true") +@DirtiesContext +public class HypermediaEndpointDocumentation { + + @Autowired + private WebApplicationContext context; + + @Autowired + private MvcEndpoints mvcEndpoints; + + @Autowired + private TemplateEngine templates; + + @Value("${org.springframework.restdocs.outputDir:target/generated-snippets}") + private String restdocsOutputDir; + + private MockMvc mockMvc; + + @Before + public void setUp() { + System.setProperty("org.springframework.restdocs.outputDir", + this.restdocsOutputDir); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration()) + .build(); + } + + @Test + public void beans() throws Exception { + this.mockMvc.perform(get("/beans").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("beans/hypermedia")); + } + + @Test + public void metrics() throws Exception { + this.mockMvc.perform(get("/metrics").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("metrics/hypermedia")); + } + + @Test + public void home() throws Exception { + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("admin")); + } + +} diff --git a/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/SpringBootHypermediaApplication.java b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/SpringBootHypermediaApplication.java new file mode 100644 index 0000000000..5a9ca58fe6 --- /dev/null +++ b/spring-boot-actuator-docs/src/test/java/org/springframework/boot/actuate/hypermedia/test/SpringBootHypermediaApplication.java @@ -0,0 +1,21 @@ +package org.springframework.boot.actuate.hypermedia.test; + +import groovy.text.GStringTemplateEngine; +import groovy.text.TemplateEngine; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SpringBootHypermediaApplication { + + @Bean + public TemplateEngine groovyTemplateEngine() { + return new GStringTemplateEngine(); + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, args); + } +} diff --git a/spring-boot-actuator-docs/src/test/resources/application.properties b/spring-boot-actuator-docs/src/test/resources/application.properties new file mode 100644 index 0000000000..6cd849878a --- /dev/null +++ b/spring-boot-actuator-docs/src/test/resources/application.properties @@ -0,0 +1,2 @@ +# management.contextPath=/admin +logging.path: target/logs \ No newline at end of file diff --git a/spring-boot-actuator-docs/src/test/resources/templates/endpoints.adoc.tpl b/spring-boot-actuator-docs/src/test/resources/templates/endpoints.adoc.tpl new file mode 100644 index 0000000000..a7c0617c55 --- /dev/null +++ b/spring-boot-actuator-docs/src/test/resources/templates/endpoints.adoc.tpl @@ -0,0 +1,16 @@ +<% endpoints.each { endpoint -> + if (endpoint.custom) { %> +include::{docs}/${endpoint.custom}[] +<% } else { %> +=== ${endpoint.title} + +Example curl request: +include::{generated}${endpoint.path}/curl-request.adoc[] + +Example HTTP request: +include::{generated}${endpoint.path}/http-request.adoc[] + +Example HTTP response: +include::{generated}${endpoint.path}/http-response.adoc[] +<% } +} %> \ No newline at end of file diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index a61dbf5925..10b7554350 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -42,6 +42,26 @@ spring-context + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + true + + + org.springframework.hateoas + spring-hateoas + true + + + org.springframework.plugin + spring-plugin-core + true + + + org.webjars + hal-browser + true + com.google.guava guava @@ -219,6 +239,11 @@ true + + com.jayway.jsonpath + json-path + test + org.springframework.boot spring-boot diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java index 227427682d..bf7b3f9e3d 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfiguration.java @@ -44,8 +44,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.bind.RelaxedPropertyResolver; @@ -86,8 +88,9 @@ import org.springframework.web.servlet.DispatcherServlet; @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) @ConditionalOnWebApplication @AutoConfigureAfter({ PropertyPlaceholderAutoConfiguration.class, - EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class }) + EmbeddedServletContainerAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + HypermediaAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class }) public class EndpointWebMvcAutoConfiguration implements ApplicationContextAware, SmartInitializingSingleton { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaConfiguration.java new file mode 100644 index 0000000000..a387675fe9 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaConfiguration.java @@ -0,0 +1,380 @@ +/* + * Copyright 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.autoconfigure; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorDocsEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.HalBrowserEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.HypermediaDisabled; +import org.springframework.boot.actuate.endpoint.mvc.LinksEnhancer; +import org.springframework.boot.actuate.endpoint.mvc.LinksMvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnResource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.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.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.type.AnnotatedTypeMetadata; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.UriTemplate; +import org.springframework.hateoas.hal.CurieProvider; +import org.springframework.hateoas.hal.DefaultCurieProvider; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.StringUtils; +import org.springframework.util.TypeUtils; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +/** + * Autoconfiguration for hypermedia in HTTP endpoints. + * + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnClass(Link.class) +@ConditionalOnWebApplication +@ConditionalOnBean(HttpMessageConverters.class) +@ConditionalOnProperty(value = "endpoints.enabled", matchIfMissing = true) +@EnableConfigurationProperties(ResourceProperties.class) +public class EndpointWebMvcHypermediaConfiguration { + + @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( + ManagementServerProperties management, ResourceProperties resources) { + return new HalBrowserEndpoint(management, + resources.getWelcomePage() != null ? "/hal" : ""); + } + + @Bean + @ConditionalOnProperty(value = "endpoints.docs.enabled", matchIfMissing = true) + @ConditionalOnResource(resources = "classpath:/META-INF/resources/spring-boot-actuator/docs/index.html") + public ActuatorDocsEndpoint actuatorDocsEndpoint(ManagementServerProperties management) { + return new ActuatorDocsEndpoint(management); + } + + @Bean + @ConditionalOnBean(ActuatorDocsEndpoint.class) + @ConditionalOnMissingBean(CurieProvider.class) + @ConditionalOnProperty(value = "endpoints.docs.curies.enabled", matchIfMissing = false) + public DefaultCurieProvider curieProvider(ServerProperties server, + ManagementServerProperties management, ActuatorDocsEndpoint endpoint) { + String path = management.getContextPath() + endpoint.getPath() + + "/#spring_boot_actuator__{rel}"; + if (server.getPort() == management.getPort() && management.getPort() != null + && management.getPort() != 0) { + path = server.getPath(path); + } + return new DefaultCurieProvider("boot", new UriTemplate(path)); + } + + @Configuration("EndpointHypermediaAutoConfiguration.MissingResourceCondition") + @ConditionalOnResource(resources = "classpath:/META-INF/spring-data-rest/hal-browser/index.html") + protected static class MissingSpringDataRestResourceCondition 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"); + } + } + + @ConditionalOnProperty(value = "endpoints.links.enabled", matchIfMissing = true) + public static class LinksConfiguration { + + @Bean + public LinksMvcEndpoint linksMvcEndpoint(ResourceProperties resources) { + return new LinksMvcEndpoint(resources.getWelcomePage() != null ? "/links" + : ""); + } + + /** + * Controller advice that adds links to the home page and/or the management + * context path. The home page is enhanced if it is composed already of a + * {@link ResourceSupport} (e.g. when using Spring Data REST). + * + * @author Dave Syer + * + */ + @ControllerAdvice + public static class HomePageLinksAdvice implements ResponseBodyAdvice { + + @Autowired + MvcEndpoints endpoints; + + @Autowired + LinksMvcEndpoint linksEndpoint; + + @Autowired + ManagementServerProperties management; + + private LinksEnhancer linksEnhancer; + + @PostConstruct + public void init() { + this.linksEnhancer = new LinksEnhancer(this.endpoints, + this.management.getContextPath()); + } + + @Override + public boolean supports(MethodParameter returnType, + Class> converterType) { + Class controllerType = returnType.getDeclaringClass(); + if (!LinksMvcEndpoint.class.isAssignableFrom(controllerType) + && MvcEndpoint.class.isAssignableFrom(controllerType)) { + return false; + } + returnType.increaseNestingLevel(); + Type nestedType = returnType.getNestedGenericParameterType(); + returnType.decreaseNestingLevel(); + return ResourceSupport.class.isAssignableFrom(returnType + .getParameterType()) + || TypeUtils.isAssignable(ResourceSupport.class, nestedType); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + HttpServletRequest servletRequest = null; + if (request instanceof ServletServerHttpRequest) { + servletRequest = ((ServletServerHttpRequest) request) + .getServletRequest(); + Object pattern = servletRequest + .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( + EndpointWebMvcHypermediaConfiguration.class) + .slash(this.management.getContextPath()).withRel( + rel)); + } + else { + this.linksEnhancer.addEndpointLinks(resource, ""); + } + } + } + } + return body; + } + + private boolean hasManagementPath() { + return StringUtils.hasText(this.management.getContextPath()); + } + + private boolean isManagementPath(String path) { + return this.management.getContextPath().equals(path); + } + + private boolean isLinksPath(String path) { + return (this.management.getContextPath() + this.linksEndpoint.getPath()) + .equals(path); + } + + private boolean isHomePage(String path) { + return "".equals(path) || "/".equals(path); + } + + } + + /** + * 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. + * + * @author Dave Syer + * + */ + @ControllerAdvice(assignableTypes = MvcEndpoint.class) + public static class MvcEndpointAdvice implements ResponseBodyAdvice { + + @Autowired + ManagementServerProperties management; + + @Autowired + HttpMessageConverters converters; + + private Map> converterCache = new ConcurrentHashMap>(); + + @Autowired + ObjectMapper mapper; + + @Override + public boolean supports(MethodParameter returnType, + Class> converterType) { + Class controllerType = returnType.getDeclaringClass(); + return !LinksMvcEndpoint.class.isAssignableFrom(controllerType) + && !HalBrowserEndpoint.class.isAssignableFrom(controllerType); + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, + MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + + if (body == null) { + // Assume it already was handled + return body; + } + + if (body instanceof Resource) { + // Assume it already has its links + return body; + } + + @SuppressWarnings("unchecked") + HttpMessageConverter converter = (HttpMessageConverter) findConverter( + selectedConverterType, selectedContentType); + if (converter == null) { + // Not a resource that can be enhanced with a link + return body; + } + if (AnnotationUtils.findAnnotation(returnType.getMethod(), + HypermediaDisabled.class) != null + || AnnotationUtils.findAnnotation(returnType.getMethod() + .getDeclaringClass(), HypermediaDisabled.class) != null) { + return body; + } + + HttpServletRequest servletRequest = null; + if (request instanceof ServletServerHttpRequest) { + servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); + String path = (String) servletRequest + .getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); + if (path == null) { + path = ""; + } + try { + converter.write(new EndpointResource(body, path), + selectedContentType, response); + } + catch (IOException e) { + throw new HttpMessageNotWritableException("Cannot write response", e); + } + return null; + } + else { + return body; + } + + } + + private HttpMessageConverter findConverter( + Class> selectedConverterType, + MediaType mediaType) { + if (this.converterCache.containsKey(mediaType)) { + return this.converterCache.get(mediaType); + } + for (HttpMessageConverter converter : this.converters) { + if (selectedConverterType.isAssignableFrom(converter.getClass()) + && converter.canWrite(EndpointResource.class, mediaType)) { + this.converterCache.put(mediaType, converter); + return converter; + } + } + return null; + } + + } + + } + + @JsonInclude(content = Include.NON_NULL) + @JacksonXmlRootElement(localName = "resource") + private static class EndpointResource extends ResourceSupport { + + private Object content; + + private Map embedded; + + @SuppressWarnings("unchecked") + public EndpointResource(Object content, String path) { + this.content = content instanceof Map ? null : content; + this.embedded = (Map) (this.content == null ? content : null); + add(linkTo(Object.class).slash(path).withSelfRel()); + } + + @JsonUnwrapped + public Object getContent() { + return this.content; + } + + @JsonAnyGetter + public Map getEmbedded() { + return this.embedded; + } + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java index 534e834054..85b5698b8b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java @@ -19,6 +19,7 @@ package org.springframework.boot.actuate.autoconfigure; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -102,7 +103,7 @@ public class ManagementSecurityAutoConfiguration { @Configuration protected static class ManagementSecurityPropertiesConfiguration implements - SecurityPrerequisite { + SecurityPrerequisite { @Autowired(required = false) private SecurityProperties security; @@ -114,7 +115,7 @@ public class ManagementSecurityAutoConfiguration { public void init() { if (this.management != null && this.security != null) { this.security.getUser().getRole() - .add(this.management.getSecurity().getRole()); + .add(this.management.getSecurity().getRole()); } } @@ -123,7 +124,7 @@ public class ManagementSecurityAutoConfiguration { // Get the ignored paths in early @Order(SecurityProperties.IGNORED_ORDER + 1) private static class IgnoredPathsWebSecurityConfigurerAdapter implements - WebSecurityConfigurer { + WebSecurityConfigurer { @Autowired(required = false) private ErrorController errorController; @@ -208,7 +209,7 @@ public class ManagementSecurityAutoConfiguration { @ConditionalOnProperty(prefix = "management.security", name = "enabled", matchIfMissing = true) @Order(ManagementServerProperties.BASIC_AUTH_ORDER) protected static class ManagementWebSecurityConfigurerAdapter extends - WebSecurityConfigurerAdapter { + WebSecurityConfigurerAdapter { @Autowired private SecurityProperties security; @@ -317,13 +318,13 @@ public class ManagementSecurityAutoConfiguration { endpointMapping, false) : getEndpointPaths(endpointMapping); - for (String path : paths) { - pathMatchers.add(new AntPathRequestMatcher( - ManagementWebSecurityConfigurerAdapter.this.server + for (String path : paths) { + pathMatchers.add(new AntPathRequestMatcher( + ManagementWebSecurityConfigurerAdapter.this.server .getPath(path))); - } - this.delegate = pathMatchers.isEmpty() ? AnyRequestMatcher.INSTANCE - : new OrRequestMatcher(pathMatchers); + } + this.delegate = pathMatchers.isEmpty() ? AnyRequestMatcher.INSTANCE + : new OrRequestMatcher(pathMatchers); } return this.delegate.matches(request); } @@ -343,15 +344,19 @@ public class ManagementSecurityAutoConfiguration { return NO_PATHS; } Set endpoints = endpointHandlerMapping.getEndpoints(); - List paths = new ArrayList(endpoints.size()); + Set paths = new LinkedHashSet(endpoints.size()); for (MvcEndpoint endpoint : endpoints) { if (endpoint.isSensitive() == secure) { String path = endpointHandlerMapping.getPath(endpoint.getPath()); paths.add(path); - // Ensure that nested paths are secured - paths.add(path + "/**"); - // Add Spring MVC-generated additional paths - paths.add(path + ".*"); + if (!path.equals("")) { + // Ensure that nested paths are secured + paths.add(path + "/**"); + // Add Spring MVC-generated additional paths + paths.add(path + ".*"); + } else { + paths.add("/"); + } } } return paths.toArray(new String[paths.size()]); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorDocsEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorDocsEndpoint.java new file mode 100644 index 0000000000..568c6d1692 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorDocsEndpoint.java @@ -0,0 +1,112 @@ +/* + * Copyright 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.actuate.endpoint.mvc.MvcEndpoint; +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; + +/** + * @author Dave Syer + * + */ +@ConfigurationProperties("endpoints.docs") +public class ActuatorDocsEndpoint extends WebMvcConfigurerAdapter implements MvcEndpoint { + + private String path = "/docs"; + + private boolean sensitive; + + private ManagementServerProperties management; + + + private Curies curies = new Curies(); + + public Curies getCuries() { + return this.curies; + } + + /** + * Properties of the default CurieProvider (used for adding docs links). If enabled, all + * unqualified rels will pick up a prefix and a curie template pointing to the docs endpoint. + * + */ + public static class Curies { + /** + * Enable the curie generation (off by default). + */ + private boolean enabled = false; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + } + + public ActuatorDocsEndpoint(ManagementServerProperties management) { + this.management = management; + } + + @RequestMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE) + public String browse() { + return "forward:" + this.management.getContextPath() + this.path + "/index.html"; + } + + @RequestMapping(value = "", produces = MediaType.TEXT_HTML_VALUE) + public String redirect() { + return "redirect:" + this.management.getContextPath() + this.path + "/"; + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler(this.management.getContextPath() + this.path + "/**") + .addResourceLocations( + "classpath:/META-INF/resources/spring-boot-actuator/docs/"); + } + + 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> getEndpointType() { + return null; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java index 1aec58d6cd..655059cf5b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java @@ -38,7 +38,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; * @author Andy Wilkinson */ public class EnvironmentMvcEndpoint extends EndpointMvcAdapter implements - EnvironmentAware { +EnvironmentAware { private Environment environment; @@ -48,6 +48,7 @@ public class EnvironmentMvcEndpoint extends EndpointMvcAdapter implements @RequestMapping(value = "/{name:.*}", method = RequestMethod.GET) @ResponseBody + @HypermediaDisabled public Object value(@PathVariable String name) { if (!getDelegate().isEnabled()) { // Shouldn't happen - MVC endpoint shouldn't be registered when delegate's diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserEndpoint.java new file mode 100644 index 0000000000..96aafbb823 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalBrowserEndpoint.java @@ -0,0 +1,96 @@ +/* + * Copyright 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; + +/** + * @author Dave Syer + * + */ +@ConfigurationProperties("endpoints.hal") +public class HalBrowserEndpoint extends WebMvcConfigurerAdapter implements MvcEndpoint { + + private static final String HAL_BROWSER_VERSION = "b7669f1-1"; + + 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 otherwise the browser won't come back for + // the JSON + registry.addResourceHandler(this.management.getContextPath() + this.path + "/") + .addResourceLocations( + "classpath:/META-INF/resources/webjars/hal-browser/" + + HAL_BROWSER_VERSION + "/").setCachePeriod(0); + registry.addResourceHandler(this.management.getContextPath() + this.path + "/**") + .addResourceLocations( + "classpath:/META-INF/resources/webjars/hal-browser/" + + HAL_BROWSER_VERSION + "/"); + } + + 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> getEndpointType() { + return null; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java new file mode 100644 index 0000000000..100481d86a --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HypermediaDisabled.java @@ -0,0 +1,34 @@ +/* + * Copyright 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.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Dave Syer + * + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface HypermediaDisabled { + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java index 71152270bf..fa16def8a5 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpoint.java @@ -45,8 +45,9 @@ import org.springframework.web.util.UrlPathHelper; * @author Andy Wilkinson */ @ConfigurationProperties(prefix = "endpoints.jolokia", ignoreUnknownFields = false) +@HypermediaDisabled public class JolokiaMvcEndpoint implements MvcEndpoint, InitializingBean, - ApplicationContextAware, ServletContextAware { +ApplicationContextAware, ServletContextAware { /** * Endpoint URL path. diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksEnhancer.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksEnhancer.java new file mode 100644 index 0000000000..6e96af9756 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksEnhancer.java @@ -0,0 +1,61 @@ +/* + * Copyright 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 static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + +import org.springframework.hateoas.ResourceSupport; +import org.springframework.util.StringUtils; + +/** + * @author Dave Syer + * + */ +public class LinksEnhancer { + + private MvcEndpoints endpoints; + + private String rootPath; + + public LinksEnhancer(MvcEndpoints endpoints, String rootPath) { + this.endpoints = endpoints; + this.rootPath = rootPath; + } + + public void addEndpointLinks(ResourceSupport resource, String self) { + if (!resource.hasLink("self")) { + resource.add(linkTo(LinksEnhancer.class).slash( + this.rootPath + self).withSelfRel()); + } + for (MvcEndpoint endpoint : this.endpoints.getEndpoints()) { + if (endpoint.getPath().equals(self)) { + continue; + } + Class type = endpoint.getEndpointType(); + if (type == null) { + type = Object.class; + } + String path = endpoint.getPath(); + String rel = path.startsWith("/") ? path.substring(1) : path; + if (StringUtils.hasText(rel)) { + resource.add(linkTo(type).slash(this.rootPath + endpoint.getPath()) + .withRel(rel)); + } + } + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java new file mode 100644 index 0000000000..7466a9ee2f --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/LinksMvcEndpoint.java @@ -0,0 +1,70 @@ +/* + * Copyright 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.endpoint.Endpoint; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Dave Syer + * + */ +@ConfigurationProperties("endpoints.links") +public class LinksMvcEndpoint implements MvcEndpoint { + + private String path = ""; + private boolean sensitive = false; + + public LinksMvcEndpoint(String defaultPath) { + this.path = defaultPath; + } + + @RequestMapping(value = { "/", "" }, produces=MediaType.APPLICATION_JSON_VALUE) + @ResponseBody + public ResourceSupport links() { + ResourceSupport resource = new ResourceSupport(); + return resource; + } + + public void setPath(String path) { + this.path = path; + } + + @Override + public String getPath() { + return this.path; + } + + @Override + public boolean isSensitive() { + return this.sensitive; + } + + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; + } + + @Override + public Class> getEndpointType() { + return null; + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java index ae54c50ca9..0fbf96fbbd 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpoint.java @@ -44,6 +44,7 @@ public class MetricsMvcEndpoint extends EndpointMvcAdapter { @RequestMapping(value = "/{name:.*}", method = RequestMethod.GET) @ResponseBody + @HypermediaDisabled public Object value(@PathVariable String name) { if (!this.delegate.isEnabled()) { // Shouldn't happen - MVC endpoint shouldn't be registered when delegate's diff --git a/spring-boot-actuator/src/main/resources/META-INF/spring.factories b/spring-boot-actuator/src/main/resources/META-INF/spring.factories index 70773f6d40..3d76679570 100644 --- a/spring-boot-actuator/src/main/resources/META-INF/spring.factories +++ b/spring-boot-actuator/src/main/resources/META-INF/spring.factories @@ -19,4 +19,5 @@ org.springframework.boot.actuate.autoconfigure.TraceRepositoryAutoConfiguration, org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration=\ -org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration \ No newline at end of file +org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration,\ +org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaConfiguration \ No newline at end of file diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java new file mode 100644 index 0000000000..668886bf6b --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/BrowserPathHypermediaIntegrationTests.java @@ -0,0 +1,71 @@ +package org.springframework.boot.actuate.autoconfigure; + +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.status; + +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.BrowserPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +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; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@TestPropertySource(properties = "endpoints.hal.path=/hal") +@DirtiesContext +public class BrowserPathHypermediaIntegrationTests { + + @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 browser() throws Exception { + MvcResult response = this.mockMvc + .perform(get("/hal/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()).andReturn(); + assertEquals("/hal/browser.html", response.getResponse().getForwardedUrl()); + } + + @Test + public void redirect() throws Exception { + this.mockMvc.perform(get("/hal").accept(MediaType.TEXT_HTML)) + .andExpect(status().isFound()) + .andExpect(header().string("location", "/hal/#")); + } + + @MinimalActuatorHypermediaApplication + @Configuration + public static class SpringBootHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, args); + } + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java new file mode 100644 index 0000000000..78eae21400 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ContextPathHypermediaIntegrationTests.java @@ -0,0 +1,105 @@ +package org.springframework.boot.actuate.autoconfigure; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.ContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.hateoas.ResourceSupport; +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; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@TestPropertySource(properties = "management.contextPath:/admin") +@DirtiesContext +public class ContextPathHypermediaIntegrationTests { + + @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 home() throws Exception { + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + } + + @Test + public void links() throws Exception { + this.mockMvc.perform(get("/admin").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + } + + @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()); + } + + @Test + public void endpointsAllListed() throws Exception { + for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { + String path = endpoint.getPath(); + path = path.startsWith("/") ? path.substring(1) : path; + path = path.length() > 0 ? path : "self"; + this.mockMvc + .perform(get("/admin").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$._links.%s.href", path).value( + "http://localhost/admin" + endpoint.getPath())); + } + } + + @MinimalActuatorHypermediaApplication + @RestController + public static class SpringBootHypermediaApplication { + + @RequestMapping("") + public ResourceSupport home() { + ResourceSupport resource = new ResourceSupport(); + resource.add(linkTo(SpringBootHypermediaApplication.class).slash("/") + .withSelfRel()); + return resource; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(SpringBootHypermediaApplication.class) + .properties("management.contextPath:/admin").run(args); + } + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java new file mode 100644 index 0000000000..674c03d0b9 --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CustomHomepageHypermediaIntegrationTests.java @@ -0,0 +1,80 @@ +package org.springframework.boot.actuate.autoconfigure; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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.CustomHomepageHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.hateoas.ResourceSupport; +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.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@DirtiesContext +public class CustomHomepageHypermediaIntegrationTests { + + @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 links() throws Exception { + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + } + + @Test + public void endpointsAllListed() throws Exception { + for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) { + String path = endpoint.getPath(); + path = path.startsWith("/") ? path.substring(1) : path; + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.%s.href", path).exists()); + } + } + + @MinimalActuatorHypermediaApplication + @RestController + public static class SpringBootHypermediaApplication { + + @RequestMapping("") + public ResourceSupport home() { + ResourceSupport resource = new ResourceSupport(); + resource.add(linkTo(SpringBootHypermediaApplication.class).slash("/").withSelfRel()); + return resource; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, args); + } + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java index ec42c628ae..4ebb3e1e73 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointMvcIntegrationTests.java @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -38,6 +41,7 @@ import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; @@ -59,9 +63,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - /** * Integration tests for MVC {@link Endpoint}s. * @@ -93,18 +94,19 @@ public class EndpointMvcIntegrationTests { @Retention(RetentionPolicy.RUNTIME) @Documented @Import({ EmbeddedServletContainerAutoConfiguration.class, - ServerPropertiesAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) + ServerPropertiesAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, + JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class }) protected static @interface MinimalWebConfiguration { } @Configuration @MinimalWebConfiguration - @Import({ ManagementServerPropertiesAutoConfiguration.class, - EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class }) + @Import({ ManagementServerPropertiesAutoConfiguration.class, JacksonAutoConfiguration.class, + EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class }) @RestController protected static class Application { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java index 0d22296a90..cb20ac6e8b 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcAutoConfigurationTests.java @@ -16,6 +16,18 @@ package org.springframework.boot.actuate.autoconfigure; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + import java.io.FileNotFoundException; import java.net.SocketException; import java.net.URI; @@ -39,6 +51,7 @@ import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.ShutdownMvcEndpoint; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; @@ -71,18 +84,6 @@ import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - /** * Tests for {@link EndpointWebMvcAutoConfiguration}. * @@ -228,6 +229,7 @@ public class EndpointWebMvcAutoConfigurationTests { ServerPortConfig.class, PropertyPlaceholderAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, ServerPropertiesAutoConfiguration.class, + JacksonAutoConfiguration.class, EmbeddedServletContainerAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, @@ -244,7 +246,7 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.register(RootConfig.class, BaseConfiguration.class, ServerPortConfig.class, EndpointWebMvcAutoConfiguration.class); new ServerPortInfoApplicationContextInitializer() - .initialize(this.applicationContext); + .initialize(this.applicationContext); this.applicationContext.refresh(); Integer localServerPort = this.applicationContext.getEnvironment().getProperty( "local.server.port", Integer.class); @@ -260,7 +262,7 @@ public class EndpointWebMvcAutoConfigurationTests { @Test public void portPropertiesOnDifferentPort() throws Exception { new ServerPortInfoApplicationContextInitializer() - .initialize(this.applicationContext); + .initialize(this.applicationContext); this.applicationContext.register(RootConfig.class, DifferentPortConfig.class, BaseConfiguration.class, EndpointWebMvcAutoConfiguration.class, ErrorMvcAutoConfiguration.class); @@ -295,7 +297,7 @@ public class EndpointWebMvcAutoConfigurationTests { this.applicationContext.refresh(); // /health, /metrics, /env (/shutdown is disabled by default) assertThat(this.applicationContext.getBeansOfType(MvcEndpoint.class).size(), - is(equalTo(4))); + is(equalTo(6))); } @Test @@ -421,12 +423,13 @@ public class EndpointWebMvcAutoConfigurationTests { @Configuration @Import({ PropertyPlaceholderAutoConfiguration.class, - EmbeddedServletContainerAutoConfiguration.class, - EndpointAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class, - ServerPropertiesAutoConfiguration.class, WebMvcAutoConfiguration.class }) + EmbeddedServletContainerAutoConfiguration.class, + JacksonAutoConfiguration.class, + EndpointAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + ServerPropertiesAutoConfiguration.class, WebMvcAutoConfiguration.class }) protected static class BaseConfiguration { } @@ -584,7 +587,7 @@ public class EndpointWebMvcAutoConfigurationTests { } private static class GrabManagementPort implements - ApplicationListener { + ApplicationListener { private ApplicationContext rootContext; diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java index 787ad9d3ed..2aaeaa6d98 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/HealthMvcEndpointAutoConfigurationTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure; +import static org.junit.Assert.assertEquals; + import org.junit.After; import org.junit.Test; import org.springframework.boot.actuate.endpoint.mvc.HealthMvcEndpoint; @@ -23,14 +25,14 @@ import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Health.Builder; import org.springframework.boot.actuate.health.Status; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.mock.web.MockServletContext; import org.springframework.stereotype.Component; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import static org.junit.Assert.assertEquals; - /** * Tests for {@link EndpointWebMvcAutoConfiguration} of the {@link HealthMvcEndpoint}. * @@ -53,6 +55,8 @@ public class HealthMvcEndpointAutoConfigurationTests { this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, + JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, TestHealthIndicator.class); this.context.refresh(); @@ -67,6 +71,8 @@ public class HealthMvcEndpointAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, + JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, TestHealthIndicator.class); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java index 4097b46669..f42231ef27 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java @@ -16,6 +16,13 @@ package org.springframework.boot.actuate.autoconfigure; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + import javax.servlet.Filter; import org.hamcrest.Matchers; @@ -23,9 +30,11 @@ import org.junit.After; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.FallbackWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -51,13 +60,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.StringUtils; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - /** * Tests for {@link ManagementSecurityAutoConfiguration}. * @@ -81,6 +83,7 @@ public class ManagementSecurityAutoConfigurationTests { this.context.setServletContext(new MockServletContext()); this.context.register(SecurityAutoConfiguration.class, ManagementSecurityAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, @@ -109,6 +112,7 @@ public class ManagementSecurityAutoConfigurationTests { this.context.setServletContext(new MockServletContext()); this.context.register(EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, SecurityAutoConfiguration.class, @@ -118,7 +122,7 @@ public class ManagementSecurityAutoConfigurationTests { UserDetails user = getUser(); assertTrue(user.getAuthorities().containsAll( AuthorityUtils - .commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ADMIN"))); + .commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ADMIN"))); } private UserDetails getUser() { @@ -153,6 +157,7 @@ public class ManagementSecurityAutoConfigurationTests { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); this.context.register(HttpMessageConvertersAutoConfiguration.class, + JacksonAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, SecurityAutoConfiguration.class, @@ -203,9 +208,11 @@ public class ManagementSecurityAutoConfigurationTests { this.context.register(AuthenticationConfig.class, SecurityAutoConfiguration.class, ManagementSecurityAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, + WebMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); this.context.refresh(); @@ -214,20 +221,20 @@ public class ManagementSecurityAutoConfigurationTests { .addFilters(filter).build(); // no user (Main) - mockMvc.perform(MockMvcRequestBuilders.get("/")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(springAuthenticateRealmHeader()); + mockMvc.perform(MockMvcRequestBuilders.get("/home")) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()) + .andExpect(springAuthenticateRealmHeader()); // invalid user (Main) mockMvc.perform( - MockMvcRequestBuilders.get("/").header("authorization", "Basic xxx")) + MockMvcRequestBuilders.get("/home").header("authorization", "Basic xxx")) .andExpect(MockMvcResultMatchers.status().isUnauthorized()) .andExpect(springAuthenticateRealmHeader()); // no user (Management) mockMvc.perform(MockMvcRequestBuilders.get("/beans")) - .andExpect(MockMvcResultMatchers.status().isUnauthorized()) - .andExpect(springAuthenticateRealmHeader()); + .andExpect(MockMvcResultMatchers.status().isUnauthorized()) + .andExpect(springAuthenticateRealmHeader()); // invalid user (Management) mockMvc.perform( @@ -247,7 +254,7 @@ public class ManagementSecurityAutoConfigurationTests { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("user").password("password") - .roles("USER"); + .roles("USER"); } } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java new file mode 100644 index 0000000000..00b92c8a4c --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/MinimalActuatorHypermediaApplication.java @@ -0,0 +1,55 @@ +/* + * Copyright 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.autoconfigure; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.EmbeddedServletContainerAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author Dave Syer + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Configuration +@Import({ ServerPropertiesAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class, + EmbeddedServletContainerAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class, + HypermediaAutoConfiguration.class, EndpointAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, ErrorMvcAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) +public @interface MinimalActuatorHypermediaApplication { + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java new file mode 100644 index 0000000000..876f97ad0e --- /dev/null +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ServerContextPathHypermediaIntegrationTests.java @@ -0,0 +1,82 @@ +package org.springframework.boot.actuate.autoconfigure; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.ServerContextPathHypermediaIntegrationTests.SpringBootHypermediaApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@IntegrationTest({ "server.port=0", "server.contextPath=/spring" }) +@DirtiesContext +public class ServerContextPathHypermediaIntegrationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void links() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + ResponseEntity entity = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/spring/", HttpMethod.GET, + new HttpEntity(null, headers), String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains("\"_links\":")); + } + + @Test + public void browser() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); + ResponseEntity entity = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/spring/", HttpMethod.GET, + new HttpEntity(null, headers), String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains(" entity = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/", HttpMethod.GET, + new HttpEntity(null, headers), String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertTrue("Wrong body: " + entity.getBody(), + entity.getBody().contains("\"_links\":")); + } + + @Test + public void browser() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); + ResponseEntity entity = new TestRestTemplate().exchange( + "http://localhost:" + this.port + "/", HttpMethod.GET, + new HttpEntity(null, headers), String.class); + assertEquals(HttpStatus.OK, entity.getStatusCode()); + assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains(" 0 ? path : "/"; + this.mockMvc + .perform(get(path).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$._links.self.href").value( + "http://localhost" + endpoint.getPath())); + } + } + + @MinimalActuatorHypermediaApplication + @Configuration + public static class SpringBootHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, args); + } + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java index 0f866a1cda..e3db7deb29 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpointTests.java @@ -16,6 +16,12 @@ package org.springframework.boot.actuate.endpoint.mvc; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -24,6 +30,9 @@ import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfigur import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.actuate.endpoint.EnvironmentEndpoint; import org.springframework.boot.actuate.endpoint.mvc.EnvironmentMvcEndpointTests.TestConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.ConfigurableApplicationContext; @@ -35,13 +44,6 @@ import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalToIgnoringCase; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests for {@link EnvironmentMvcEndpoint} @@ -70,13 +72,13 @@ public class EnvironmentMvcEndpointTests { @Test public void home() throws Exception { this.mvc.perform(get("/env")).andExpect(status().isOk()) - .andExpect(content().string(containsString("systemProperties"))); + .andExpect(content().string(containsString("systemProperties"))); } @Test public void sub() throws Exception { this.mvc.perform(get("/env/foo")).andExpect(status().isOk()) - .andExpect(content().string(equalToIgnoringCase("bar"))); + .andExpect(content().string(equalToIgnoringCase("bar"))); } @Test @@ -88,13 +90,15 @@ public class EnvironmentMvcEndpointTests { @Test public void regex() throws Exception { this.mvc.perform(get("/env/foo.*")).andExpect(status().isOk()) - .andExpect(content().string(containsString("\"foo\":\"bar\""))) - .andExpect(content().string(containsString("\"fool\":\"baz\""))); + .andExpect(content().string(containsString("\"foo\":\"bar\""))) + .andExpect(content().string(containsString("\"fool\":\"baz\""))); } - @Import({ EndpointWebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class }) - @EnableWebMvc + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + WebMvcAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) @Configuration public static class TestConfiguration { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java index f50fb4f05c..9b338faabf 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointContextPathTests.java @@ -16,6 +16,11 @@ package org.springframework.boot.actuate.endpoint.mvc; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -25,6 +30,8 @@ import org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.actuate.endpoint.mvc.JolokiaMvcEndpointContextPathTests.Config; import org.springframework.boot.actuate.endpoint.mvc.JolokiaMvcEndpointContextPathTests.ContextPathListener; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.SpringApplicationConfiguration; @@ -39,11 +46,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * @author Christian Dupuis * @author Dave Syer @@ -71,20 +73,22 @@ public class JolokiaMvcEndpointContextPathTests { @Test public void read() throws Exception { this.mvc.perform(get("/admin/jolokia/read/java.lang:type=Memory")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("NonHeapMemoryUsage"))); + .andExpect(status().isOk()) + .andExpect(content().string(containsString("NonHeapMemoryUsage"))); } @Configuration @EnableConfigurationProperties @EnableWebMvc - @Import({ EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class }) + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) public static class Config { } public static class ContextPathListener implements - ApplicationContextInitializer { + ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext context) { EnvironmentTestUtils.addEnvironment(context, "management.contextPath:/admin"); diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java index b41647183f..3df45ee534 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/JolokiaMvcEndpointTests.java @@ -16,6 +16,14 @@ package org.springframework.boot.actuate.endpoint.mvc; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import java.util.Set; import org.hamcrest.Matcher; @@ -27,6 +35,8 @@ import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfigur import org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.actuate.endpoint.mvc.JolokiaMvcEndpointTests.Config; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.SpringApplicationConfiguration; @@ -40,14 +50,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.instanceOf; -import static org.junit.Assert.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * Tests for {@link JolokiaMvcEndpoint} * @@ -84,28 +86,30 @@ public class JolokiaMvcEndpointTests { @Test public void search() throws Exception { this.mvc.perform(get("/jolokia/search/java.lang:*")).andExpect(status().isOk()) - .andExpect(content().string(containsString("GarbageCollector"))); + .andExpect(content().string(containsString("GarbageCollector"))); } @Test public void read() throws Exception { this.mvc.perform(get("/jolokia/read/java.lang:type=Memory")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("NonHeapMemoryUsage"))); + .andExpect(status().isOk()) + .andExpect(content().string(containsString("NonHeapMemoryUsage"))); } @Test public void list() throws Exception { this.mvc.perform(get("/jolokia/list/java.lang/type=Memory/attr")) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("NonHeapMemoryUsage"))); + .andExpect(status().isOk()) + .andExpect(content().string(containsString("NonHeapMemoryUsage"))); } @Configuration @EnableConfigurationProperties @EnableWebMvc - @Import({ EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class }) + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, JolokiaAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) public static class Config { } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java index 1d46913733..55a51ceb1f 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MetricsMvcEndpointTests.java @@ -16,6 +16,12 @@ package org.springframework.boot.actuate.endpoint.mvc; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -30,6 +36,9 @@ import org.springframework.boot.actuate.endpoint.MetricsEndpoint; import org.springframework.boot.actuate.endpoint.PublicMetrics; import org.springframework.boot.actuate.endpoint.mvc.MetricsMvcEndpointTests.TestConfiguration; import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,13 +48,6 @@ import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests for {@link MetricsMvcEndpoint} @@ -72,7 +74,7 @@ public class MetricsMvcEndpointTests { @Test public void home() throws Exception { this.mvc.perform(get("/metrics")).andExpect(status().isOk()) - .andExpect(content().string(containsString("\"foo\":1"))); + .andExpect(content().string(containsString("\"foo\":1"))); } @Test @@ -84,7 +86,7 @@ public class MetricsMvcEndpointTests { @Test public void specificMetric() throws Exception { this.mvc.perform(get("/metrics/foo")).andExpect(status().isOk()) - .andExpect(content().string(equalTo("1"))); + .andExpect(content().string(equalTo("1"))); } @Test @@ -100,35 +102,37 @@ public class MetricsMvcEndpointTests { @Test public void regexAll() throws Exception { - String expected = "{\"foo\":1,\"group1.a\":1,\"group1.b\":1,\"group2.a\":1,\"group2_a\":1}"; + String expected = "\"foo\":1,\"group1.a\":1,\"group1.b\":1,\"group2.a\":1,\"group2_a\":1"; this.mvc.perform(get("/metrics/.*")).andExpect(status().isOk()) - .andExpect(content().string(expected)); + .andExpect(content().string(containsString(expected))); } @Test public void regexGroupDot() throws Exception { - String expected = "{\"group1.a\":1,\"group1.b\":1,\"group2.a\":1}"; + String expected = "\"group1.a\":1,\"group1.b\":1,\"group2.a\":1"; this.mvc.perform(get("/metrics/group[0-9]+\\..*")).andExpect(status().isOk()) - .andExpect(content().string(expected)); + .andExpect(content().string(containsString(expected))); } @Test public void regexGroup1() throws Exception { - String expected = "{\"group1.a\":1,\"group1.b\":1}"; + String expected = "\"group1.a\":1,\"group1.b\":1"; this.mvc.perform(get("/metrics/group1\\..*")).andExpect(status().isOk()) - .andExpect(content().string(expected)); + .andExpect(content().string(containsString(expected))); } @Test public void specificMetricWithDot() throws Exception { this.mvc.perform(get("/metrics/group2.a")).andExpect(status().isOk()) - .andExpect(content().string("1")); + .andExpect(content().string(containsString("1"))); } - @Import({ EndpointWebMvcAutoConfiguration.class, - ManagementServerPropertiesAutoConfiguration.class }) - @EnableWebMvc + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, + WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) @Configuration public static class TestConfiguration { diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java index 46d32b4e1b..29dd74b717 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/mvc/MvcEndpointCorsIntegrationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.actuate.endpoint.mvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.Before; import org.junit.Test; import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; @@ -23,6 +27,7 @@ import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfigur import org.springframework.boot.actuate.autoconfigure.JolokiaAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.test.EnvironmentTestUtils; @@ -33,10 +38,6 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - /** * Integration tests for the actuator endpoints' CORS support * @@ -50,7 +51,7 @@ public class MvcEndpointCorsIntegrationTests { public void createContext() { this.context = new AnnotationConfigWebApplicationContext(); this.context.setServletContext(new MockServletContext()); - this.context.register(HttpMessageConvertersAutoConfiguration.class, + this.context.register(JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, @@ -62,7 +63,7 @@ public class MvcEndpointCorsIntegrationTests { createMockMvc().perform( options("/beans").header("Origin", "foo.example.com").header( HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect( - header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); } @Test @@ -72,7 +73,7 @@ public class MvcEndpointCorsIntegrationTests { createMockMvc().perform( options("/beans").header("Origin", "bar.example.com").header( HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect( - status().isForbidden()); + status().isForbidden()); performAcceptedCorsRequest(); } @@ -99,8 +100,8 @@ public class MvcEndpointCorsIntegrationTests { "endpoints.cors.allowed-origins:foo.example.com"); createMockMvc().perform( options("/beans").header("Origin", "foo.example.com") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Alpha")) + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Alpha")) .andExpect(status().isForbidden()); } @@ -110,15 +111,15 @@ public class MvcEndpointCorsIntegrationTests { "endpoints.cors.allowed-origins:foo.example.com", "endpoints.cors.allowed-headers:Alpha,Bravo"); createMockMvc() - .perform( - options("/beans") - .header("Origin", "foo.example.com") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, - "Alpha")) - .andExpect(status().isOk()) - .andExpect( - header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Alpha")); + .perform( + options("/beans") + .header("Origin", "foo.example.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, + "Alpha")) + .andExpect(status().isOk()) + .andExpect( + header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Alpha")); } @Test @@ -128,7 +129,7 @@ public class MvcEndpointCorsIntegrationTests { createMockMvc().perform( options("/health").header(HttpHeaders.ORIGIN, "foo.example.com").header( HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD")).andExpect( - status().isForbidden()); + status().isForbidden()); } @Test @@ -137,10 +138,10 @@ public class MvcEndpointCorsIntegrationTests { "endpoints.cors.allowed-origins:foo.example.com", "endpoints.cors.allowed-methods:GET,HEAD"); createMockMvc() - .perform( - options("/health") - .header(HttpHeaders.ORIGIN, "foo.example.com") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD")) + .perform( + options("/health") + .header(HttpHeaders.ORIGIN, "foo.example.com") + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "HEAD")) .andExpect(status().isOk()) .andExpect( header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, @@ -172,7 +173,7 @@ public class MvcEndpointCorsIntegrationTests { createMockMvc().perform( options("/jolokia").header("Origin", "bar.example.com").header( HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")).andExpect( - status().isForbidden()); + status().isForbidden()); performAcceptedCorsRequest("/jolokia"); } @@ -189,10 +190,10 @@ public class MvcEndpointCorsIntegrationTests { return createMockMvc() .perform( options(url).header(HttpHeaders.ORIGIN, "foo.example.com") - .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) - .andExpect( - header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, - "foo.example.com")).andExpect(status().isOk()); + .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET")) + .andExpect( + header().string(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, + "foo.example.com")).andExpect(status().isOk()); } } diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 689de2877a..2b20e5c7ab 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -178,6 +178,11 @@ spring-boot-actuator 1.3.0.BUILD-SNAPSHOT + + org.springframework.boot + spring-boot-actuator-docs + 1.3.0.BUILD-SNAPSHOT + org.springframework.boot spring-boot-autoconfigure @@ -1842,6 +1847,11 @@ thymeleaf-extras-springsecurity4 ${thymeleaf-extras-springsecurity4.version} + + org.webjars + hal-browser + b7669f1-1 + org.yaml snakeyaml diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 43e8754c3a..138bbf292b 100644 --- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -150,6 +150,28 @@ For example, the following will disable _all_ endpoints except for `info`: endpoints.info.enabled=true ---- +[[production-ready-endpoint-hypermedia]] +=== Hypermedia for MVC Endpoints +If http://projects.spring.io/spring-hateoas[Spring HATEOAS] is on the classpath (e.g. +through the `spring-boot-starter-hateoas` or if you are using +http://projects.spring.io/spring-data-rest[Spring Data REST]) then the HTTP endpoints +from the Actuator are enhanced with hypermedia links, and a "discovery page" is added +with links to all the endpoints. The "discovery page" is actually an endpoint itself, +so it can be disabled along with the rest of the hypermedia by setting +`endpoints.links.enabled=false`. If it is not explicitly disabled the links +endpoint renders a JSON object with a link for each other endpoint, and the default +path is the same as the `management.contentPath` (so "/" by default). + +NOTE: if there is a static home page ("index.html") in your application and the links +endpoint is registered with its default path ("/") then content negotiation will kick in +to determine which content is shown to a client that requests the home page (the +links will show only if the client accepts `application/json`). + +If the https://github.com/mikekelly/hal-browser[HAL Browser] is on the classpath +via its webjar (`org.webjars:hal-browser`), or via the `spring-data-hal-browser` then +the default home page for HTML clients will be the HAL Browser. This is also exposed via an +endpoint ("hal") so it can be disabled and have its path explicitly configured like +the other endpoints. [[production-ready-customizing-endpoints-programmatically]] diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml index 0b4be403c9..00ed00afbd 100644 --- a/spring-boot-samples/pom.xml +++ b/spring-boot-samples/pom.xml @@ -43,6 +43,10 @@ spring-boot-sample-flyway spring-boot-sample-hateoas spring-boot-sample-hornetq + spring-boot-sample-hypermedia + spring-boot-sample-hypermedia-gson + spring-boot-sample-hypermedia-jpa + spring-boot-sample-hypermedia-ui spring-boot-sample-integration spring-boot-sample-jersey spring-boot-sample-jersey1 diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-gson/pom.xml b/spring-boot-samples/spring-boot-sample-hypermedia-gson/pom.xml new file mode 100644 index 0000000000..e8f6fc2c20 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-gson/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + spring-boot-sample-hypermedia-gson + jar + + spring-boot-sample-hypermedia-gson + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + + + UTF-8 + 1.7 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-hateoas + + + com.google.code.gson + gson + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.jayway.jsonpath + json-path + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/main/java/demo/SpringBootHypermediaApplication.java b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/main/java/demo/SpringBootHypermediaApplication.java new file mode 100644 index 0000000000..954faed6f9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/main/java/demo/SpringBootHypermediaApplication.java @@ -0,0 +1,12 @@ +package demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringBootHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, args); + } +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/main/resources/application.properties new file mode 100644 index 0000000000..ef0f024d90 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# management.contextPath=/admin +spring.http.converters.preferred-json-mapper: gson \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/demo/SpringBootHypermediaApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/demo/SpringBootHypermediaApplicationTests.java new file mode 100644 index 0000000000..54e46efb31 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-gson/src/test/java/demo/SpringBootHypermediaApplicationTests.java @@ -0,0 +1,61 @@ +package demo; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.http.MediaType; +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; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@TestPropertySource(properties="endpoints.health.sensitive: false") +public class SpringBootHypermediaApplicationTests { + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build(); + } + + @Test + public void health() throws Exception { + this.mockMvc + .perform(get("/health").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.links[0].href").value("http://localhost/health")) + .andExpect(jsonPath("$.content.status").exists()); + } + + @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()); + } + + @Test + public void envValue() throws Exception { + this.mockMvc.perform(get("/env/user.home").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links").doesNotExist()); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/pom.xml b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/pom.xml new file mode 100644 index 0000000000..b8ead35e0c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + spring-boot-sample-hypermedia-jpa + jar + + spring-boot-sample-hypermedia-jpa + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + + + UTF-8 + 1.7 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-hateoas + + + org.springframework.boot + spring-boot-actuator-docs + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.data + spring-data-rest-hal-browser + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.jayway.jsonpath + json-path + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/Book.java b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/Book.java new file mode 100644 index 0000000000..838c1901bd --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/Book.java @@ -0,0 +1,35 @@ +/* + * Copyright 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 demo; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Book { + @Id + @GeneratedValue + private Long id; + private String title; + public String getTitle() { + return this.title; + } + public void setTitle(String title) { + this.title = title; + } +} \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/BookRepository.java b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/BookRepository.java new file mode 100644 index 0000000000..6ac159bc81 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/BookRepository.java @@ -0,0 +1,22 @@ +/* + * Copyright 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 demo; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookRepository extends JpaRepository { +} \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/JpaHypermediaApplication.java b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/JpaHypermediaApplication.java new file mode 100644 index 0000000000..baae2f2a8c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/java/demo/JpaHypermediaApplication.java @@ -0,0 +1,13 @@ +package demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JpaHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(JpaHypermediaApplication.class, args); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/resources/application.properties new file mode 100644 index 0000000000..31e1ded85d --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/main/resources/application.properties @@ -0,0 +1,2 @@ +management.contextPath=/admin +endpoints.docs.curies.enabled=true \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/demo/JpaHypermediaApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/demo/JpaHypermediaApplicationTests.java new file mode 100644 index 0000000000..46ac33e2d9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/demo/JpaHypermediaApplicationTests.java @@ -0,0 +1,18 @@ +package demo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = JpaHypermediaApplication.class) +@WebAppConfiguration +public class JpaHypermediaApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/demo/VanillaHypermediaIntegrationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/demo/VanillaHypermediaIntegrationTests.java new file mode 100644 index 0000000000..e20d8be0f0 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-jpa/src/test/java/demo/VanillaHypermediaIntegrationTests.java @@ -0,0 +1,74 @@ +package demo; + +import static org.hamcrest.Matchers.containsString; +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.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; +import org.springframework.boot.test.SpringApplicationConfiguration; +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; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = JpaHypermediaApplication.class) +@WebAppConfiguration +@DirtiesContext +public class VanillaHypermediaIntegrationTests { + + @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 links() throws Exception { + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + } + + @Test + public void health() throws Exception { + this.mockMvc.perform(get("/admin/health").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + } + + @Test + public void adminLinks() throws Exception { + this.mockMvc.perform(get("/admin").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andExpect(jsonPath("$._links").exists()); + } + + @Test + public void docs() throws Exception { + MvcResult response = this.mockMvc.perform(get("/admin/docs/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isOk()).andReturn(); + System.err.println(response.getResponse().getContentAsString()); + } + + @Test + public void browser() throws Exception { + MvcResult response = this.mockMvc.perform(get("/").accept(MediaType.TEXT_HTML)) + .andExpect(status().isFound()).andReturn(); + assertEquals("/browser/index.html#", response.getResponse().getHeaders("location").get(0)); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-ui/pom.xml b/spring-boot-samples/spring-boot-sample-hypermedia-ui/pom.xml new file mode 100644 index 0000000000..2ed2b746cf --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-ui/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + spring-boot-sample-hypermedia-ui + jar + + spring-boot-sample-hypermedia-ui + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + + + UTF-8 + 1.7 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-hateoas + + + org.springframework.boot + spring-boot-actuator-docs + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/java/demo/SpringBootHypermediaApplication.java b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/java/demo/SpringBootHypermediaApplication.java new file mode 100644 index 0000000000..954faed6f9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/java/demo/SpringBootHypermediaApplication.java @@ -0,0 +1,12 @@ +package demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringBootHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, args); + } +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/resources/application.properties new file mode 100644 index 0000000000..560a46ccde --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/resources/application.properties @@ -0,0 +1 @@ +management.contextPath=/admin \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/resources/static/index.html b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/resources/static/index.html new file mode 100644 index 0000000000..66a41f708c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/main/resources/static/index.html @@ -0,0 +1,5 @@ + + +

Hello World!

+ + \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/test/java/demo/HomePageHypermediaApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/test/java/demo/HomePageHypermediaApplicationTests.java new file mode 100644 index 0000000000..e68f6aafe7 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/test/java/demo/HomePageHypermediaApplicationTests.java @@ -0,0 +1,65 @@ +package demo; + +import static org.junit.Assert.assertTrue; + +import java.net.URI; +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@IntegrationTest({ "management.contextPath=", "server.port=0" }) +public class HomePageHypermediaApplicationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void home() { + String response = new TestRestTemplate().getForObject("http://localhost:" + port, + String.class); + assertTrue("Wrong body: " + response, response.contains("Hello World")); + } + + @Test + public void links() { + String response = new TestRestTemplate().getForObject("http://localhost:" + port + "/links", + String.class); + assertTrue("Wrong body: " + response, response.contains("\"_links\":")); + } + + @Test + public void linksWithJson() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + ResponseEntity response = new TestRestTemplate().exchange( + new RequestEntity(headers , HttpMethod.GET, new URI("http://localhost:" + + port + "/links")), String.class); + assertTrue("Wrong body: " + response, response.getBody().contains("\"_links\":")); + } + + @Test + public void homeWithHtml() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); + ResponseEntity response = new TestRestTemplate().exchange( + new RequestEntity(headers , HttpMethod.GET, new URI("http://localhost:" + + port)), String.class); + assertTrue("Wrong body: " + response, response.getBody().contains("Hello World")); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/test/java/demo/SpringBootHypermediaApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/test/java/demo/SpringBootHypermediaApplicationTests.java new file mode 100644 index 0000000000..1cf70aeee2 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia-ui/src/test/java/demo/SpringBootHypermediaApplicationTests.java @@ -0,0 +1,18 @@ +package demo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +public class SpringBootHypermediaApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia/pom.xml b/spring-boot-samples/spring-boot-sample-hypermedia/pom.xml new file mode 100644 index 0000000000..a937dc548c --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + spring-boot-sample-hypermedia + jar + + spring-boot-sample-hypermedia + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-samples + 1.3.0.BUILD-SNAPSHOT + + + + UTF-8 + 1.7 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-hateoas + + + org.springframework.boot + spring-boot-actuator-docs + + + org.webjars + hal-browser + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-boot-samples/spring-boot-sample-hypermedia/src/main/java/demo/SpringBootHypermediaApplication.java b/spring-boot-samples/spring-boot-sample-hypermedia/src/main/java/demo/SpringBootHypermediaApplication.java new file mode 100644 index 0000000000..954faed6f9 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia/src/main/java/demo/SpringBootHypermediaApplication.java @@ -0,0 +1,12 @@ +package demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringBootHypermediaApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringBootHypermediaApplication.class, args); + } +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-hypermedia/src/main/resources/application.properties new file mode 100644 index 0000000000..39f6a14b37 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# management.contextPath=/admin +endpoints.docs.curies.enabled=true \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-hypermedia/src/test/java/demo/HomePageHypermediaApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia/src/test/java/demo/HomePageHypermediaApplicationTests.java new file mode 100644 index 0000000000..676a937971 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia/src/test/java/demo/HomePageHypermediaApplicationTests.java @@ -0,0 +1,59 @@ +package demo; + +import static org.junit.Assert.assertTrue; + +import java.net.URI; +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port=0") +public class HomePageHypermediaApplicationTests { + + @Value("${local.server.port}") + private int port; + + @Test + public void home() { + String response = new TestRestTemplate().getForObject("http://localhost:" + port, + String.class); + assertTrue("Wrong body: " + response, response.contains("\"_links\":")); + assertTrue("Wrong body: " + response, response.contains("\"curies\":")); + } + + @Test + public void homeWithJson() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + ResponseEntity response = new TestRestTemplate().exchange( + new RequestEntity(headers , HttpMethod.GET, new URI("http://localhost:" + + port + "/")), String.class); + assertTrue("Wrong body: " + response, response.getBody().contains("\"_links\":")); + } + + @Test + public void homeWithHtml() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.TEXT_HTML)); + ResponseEntity response = new TestRestTemplate().exchange( + new RequestEntity(headers , HttpMethod.GET, new URI("http://localhost:" + + port)), String.class); + assertTrue("Wrong body: " + response, response.getBody().contains("HAL Browser")); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-hypermedia/src/test/java/demo/SpringBootHypermediaApplicationTests.java b/spring-boot-samples/spring-boot-sample-hypermedia/src/test/java/demo/SpringBootHypermediaApplicationTests.java new file mode 100644 index 0000000000..fc06e71649 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-hypermedia/src/test/java/demo/SpringBootHypermediaApplicationTests.java @@ -0,0 +1,18 @@ +package demo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.class) +@WebAppConfiguration +public class SpringBootHypermediaApplicationTests { + + @Test + public void contextLoads() { + } + +}