Add support for Spring HATEOAS hypermedia in Actuator endpoints

If spring-hateoas is on the classpath and an MvcEndpoint returns a
@ResponseBody it will be extended and wrapped into a Resource with links.
All the existing endpoints that return sensible JSON data can be extended
this way (i.e. not /logfile). The HAL browser will also be added as an
endpoint if available on the classpath. Finally, asciidocs for the
Actuator endpoints are available as a separate jar file, which if
included in an app will also generate a new (HTTP) endpoint.

Fixes gh-1390
pull/3403/merge
Dave Syer 10 years ago
parent 82fdb87a8c
commit 74e9e0749b

@ -87,6 +87,7 @@
<module>spring-boot-devtools</module>
<module>spring-boot-docs</module>
<module>spring-boot-starters</module>
<module>spring-boot-actuator-docs</module>
<module>spring-boot-cli</module>
</modules>
</profile>

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-actuator-docs</artifactId>
<packaging>jar</packaging>
<name>spring-boot-actuator-docs</name>
<description>Docs project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs</artifactId>
<version>1.0.0.M1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-groovy-templates</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Documentation.java</include>
</includes>
<systemPropertyVariables>
<org.springframework.restdocs.outputDir>${project.build.directory}/generated-snippets</org.springframework.restdocs.outputDir>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.2</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
<sourceDocumentName>index.adoc</sourceDocumentName>
<attributes>
<generated>${project.build.directory}/generated-snippets</generated>
<docs>${project.build.directory}/../src/main/asciidoc</docs>
</attributes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}/META-INF/resources/spring-boot-actuator/docs</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}/generated-docs</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

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

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

@ -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: `<prefix>.CONFIGURATION_PROPERTIES`, where
`<prefix>` 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[]

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

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

@ -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.<STATUS>=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[]

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

@ -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]
----
<dependency>
<groupId>org.webjars</groupId>
<artifactId>hal-browser</artifactId>
</dependency>
----
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]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-hypermedia-docs</artifactId>
</dependency>
----
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.

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

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

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

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

@ -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<String, Object> model = new LinkedHashMap<String, Object>();
final List<EndpointDoc> endpoints = new ArrayList<EndpointDoc>();
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<? extends MvcEndpoint> getEndpoints() {
List<? extends MvcEndpoint> endpoints = new ArrayList<MvcEndpoint>(
this.mvcEndpoints.getEndpoints());
Collections.sort(endpoints, new Comparator<MvcEndpoint>() {
@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;
}
}
}

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

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

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

@ -0,0 +1,2 @@
# management.contextPath=/admin
logging.path: target/logs

@ -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[]
<% }
} %>

@ -42,6 +42,26 @@
<artifactId>spring-context</artifactId>
</dependency>
<!-- Optional -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>hal-browser</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
@ -219,6 +239,11 @@
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>

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

@ -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<Object> {
@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<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<Object> {
@Autowired
ManagementServerProperties management;
@Autowired
HttpMessageConverters converters;
private Map<MediaType, HttpMessageConverter<?>> converterCache = new ConcurrentHashMap<MediaType, HttpMessageConverter<?>>();
@Autowired
ObjectMapper mapper;
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> 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<? extends HttpMessageConverter<?>> 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<Object> converter = (HttpMessageConverter<Object>) 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<? extends HttpMessageConverter<?>> 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<String, Object> embedded;
@SuppressWarnings("unchecked")
public EndpointResource(Object content, String path) {
this.content = content instanceof Map ? null : content;
this.embedded = (Map<String, Object>) (this.content == null ? content : null);
add(linkTo(Object.class).slash(path).withSelfRel());
}
@JsonUnwrapped
public Object getContent() {
return this.content;
}
@JsonAnyGetter
public Map<String, Object> getEmbedded() {
return this.embedded;
}
}
}

@ -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<WebSecurity> {
WebSecurityConfigurer<WebSecurity> {
@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<? extends MvcEndpoint> endpoints = endpointHandlerMapping.getEndpoints();
List<String> paths = new ArrayList<String>(endpoints.size());
Set<String> paths = new LinkedHashSet<String>(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()]);

@ -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<? extends Endpoint<?>> getEndpointType() {
return null;
}
}

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

@ -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<? extends Endpoint<?>> getEndpointType() {
return null;
}
}

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

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

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

@ -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<? extends Endpoint<?>> getEndpointType() {
return null;
}
}

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

@ -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
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcConfiguration,\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaConfiguration

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

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

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

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

@ -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<EmbeddedServletContainerInitializedEvent> {
ApplicationListener<EmbeddedServletContainerInitializedEvent> {
private ApplicationContext rootContext;

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

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

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

@ -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<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/", HttpMethod.GET,
new HttpEntity<Void>(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<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/spring/", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
assertTrue("Wrong body: " + entity.getBody(),
entity.getBody().contains("<title"));
}
@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("server.contextPath=/spring").run(args);
}
}
}

@ -0,0 +1,81 @@
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.ServerPortHypermediaIntegrationTests.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", "management.port=0" })
@DirtiesContext
public class ServerPortHypermediaIntegrationTests {
@Value("${local.management.port}")
private int port;
@Test
public void links() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
ResponseEntity<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/", HttpMethod.GET,
new HttpEntity<Void>(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<String> entity = new TestRestTemplate().exchange(
"http://localhost:" + this.port + "/", HttpMethod.GET,
new HttpEntity<Void>(null, headers), String.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
assertTrue("Wrong body: " + entity.getBody(), entity.getBody().contains("<title"));
}
@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.port:9000").run(args);
}
}
}

@ -0,0 +1,115 @@
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.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.VanillaHypermediaIntegrationTests.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.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SpringBootHypermediaApplication.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())
.andExpect(header().doesNotExist("cache-control"));
}
@Test
public void browser() throws Exception {
MvcResult response = this.mockMvc.perform(get("/").accept(MediaType.TEXT_HTML))
.andExpect(status().isOk()).andReturn();
assertEquals("/browser.html", response.getResponse().getForwardedUrl());
}
@Test
public void trace() throws Exception {
this.mockMvc
.perform(get("/trace").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links.self.href").value("http://localhost/trace"))
.andExpect(jsonPath("$.content").isArray());
}
@Test
public void envValue() throws Exception {
this.mockMvc.perform(get("/env/user.home").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$._links").doesNotExist());
}
@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());
}
}
@Test
public void endpointsEachHaveSelf() throws Exception {
for (MvcEndpoint endpoint : this.mvcEndpoints.getEndpoints()) {
String path = endpoint.getPath();
if ("/hal".equals(path) || "/logfile".equals(path)) {
// TODO: /logfile shouldn't be active anyway
continue;
}
path = path.length() > 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);
}
}
}

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

@ -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<ConfigurableApplicationContext> {
ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
EnvironmentTestUtils.addEnvironment(context, "management.contextPath:/admin");

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

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

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

@ -178,6 +178,11 @@
<artifactId>spring-boot-actuator</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-docs</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
@ -1842,6 +1847,11 @@
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>${thymeleaf-extras-springsecurity4.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>hal-browser</artifactId>
<version>b7669f1-1</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>

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

@ -43,6 +43,10 @@
<module>spring-boot-sample-flyway</module>
<module>spring-boot-sample-hateoas</module>
<module>spring-boot-sample-hornetq</module>
<module>spring-boot-sample-hypermedia</module>
<module>spring-boot-sample-hypermedia-gson</module>
<module>spring-boot-sample-hypermedia-jpa</module>
<module>spring-boot-sample-hypermedia-ui</module>
<module>spring-boot-sample-integration</module>
<module>spring-boot-sample-jersey</module>
<module>spring-boot-sample-jersey1</module>

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-sample-hypermedia-gson</artifactId>
<packaging>jar</packaging>
<name>spring-boot-sample-hypermedia-gson</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-samples</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

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

@ -0,0 +1,2 @@
# management.contextPath=/admin
spring.http.converters.preferred-json-mapper: gson

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

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-sample-hypermedia-jpa</artifactId>
<packaging>jar</packaging>
<name>spring-boot-sample-hypermedia-jpa</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-samples</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-docs</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

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

@ -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<Book, Long> {
}

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

@ -0,0 +1,2 @@
management.contextPath=/admin
endpoints.docs.curies.enabled=true

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

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

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-sample-hypermedia-ui</artifactId>
<packaging>jar</packaging>
<name>spring-boot-sample-hypermedia-ui</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-samples</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-docs</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

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

@ -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<String> response = new TestRestTemplate().exchange(
new RequestEntity<Void>(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<String> response = new TestRestTemplate().exchange(
new RequestEntity<Void>(headers , HttpMethod.GET, new URI("http://localhost:"
+ port)), String.class);
assertTrue("Wrong body: " + response, response.getBody().contains("Hello World"));
}
}

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

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-sample-hypermedia</artifactId>
<packaging>jar</packaging>
<name>spring-boot-sample-hypermedia</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-samples</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-docs</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>hal-browser</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

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

@ -0,0 +1,2 @@
# management.contextPath=/admin
endpoints.docs.curies.enabled=true

@ -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<String> response = new TestRestTemplate().exchange(
new RequestEntity<Void>(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<String> response = new TestRestTemplate().exchange(
new RequestEntity<Void>(headers , HttpMethod.GET, new URI("http://localhost:"
+ port)), String.class);
assertTrue("Wrong body: " + response, response.getBody().contains("HAL Browser"));
}
}

@ -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() {
}
}
Loading…
Cancel
Save