diff --git a/eclipse/org.eclipse.jdt.core.prefs b/eclipse/org.eclipse.jdt.core.prefs
index e44b6fc366..3ec70345df 100644
--- a/eclipse/org.eclipse.jdt.core.prefs
+++ b/eclipse/org.eclipse.jdt.core.prefs
@@ -10,6 +10,7 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
diff --git a/spring-boot-parent/src/checkstyle/import-control.xml b/spring-boot-parent/src/checkstyle/import-control.xml
index b73b7cbd40..cc52288841 100644
--- a/spring-boot-parent/src/checkstyle/import-control.xml
+++ b/spring-boot-parent/src/checkstyle/import-control.xml
@@ -29,6 +29,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -109,4 +125,4 @@
-
+
\ No newline at end of file
diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml
index 56f060e620..21d22b2c0e 100644
--- a/spring-boot/pom.xml
+++ b/spring-boot/pom.xml
@@ -159,6 +159,11 @@
jetty-webapptrue
+
+ org.glassfish.jersey.core
+ jersey-server
+ true
+ org.hamcresthamcrest-library
@@ -271,6 +276,11 @@
h2test
+
+ com.jayway.jsonpath
+ json-path
+ test
+ com.microsoft.sqlservermssql-jdbc
@@ -316,6 +326,16 @@
jaybird-jdk18test
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet-core
+ test
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-jackson
+ test
+ org.hsqldbhsqldb
@@ -352,4 +372,4 @@
test
-
+
\ No newline at end of file
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointLinksResolver.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointLinksResolver.java
new file mode 100644
index 0000000000..647a59c69f
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointLinksResolver.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.boot.endpoint.EndpointInfo;
+
+/**
+ * A resolver for {@link Link links} to web endpoints.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class EndpointLinksResolver {
+
+ /**
+ * Resolves links to the operations of the given {code webEndpoints} based on a
+ * request with the given {@code requestUrl}.
+ *
+ * @param webEndpoints the web endpoints
+ * @param requestUrl the url of the request for the endpoint links
+ * @return the links
+ */
+ public Map resolveLinks(
+ Collection> webEndpoints,
+ String requestUrl) {
+ String normalizedUrl = normalizeRequestUrl(requestUrl);
+ Map links = new LinkedHashMap();
+ links.put("self", new Link(normalizedUrl));
+ for (EndpointInfo endpoint : webEndpoints) {
+ for (WebEndpointOperation operation : endpoint.getOperations()) {
+ webEndpoints.stream().map(EndpointInfo::getId).forEach((id) -> links
+ .put(operation.getId(), createLink(normalizedUrl, operation)));
+ }
+ }
+ return links;
+ }
+
+ private String normalizeRequestUrl(String requestUrl) {
+ if (requestUrl.endsWith("/")) {
+ return requestUrl.substring(0, requestUrl.length() - 1);
+ }
+ return requestUrl;
+ }
+
+ private Link createLink(String requestUrl, WebEndpointOperation operation) {
+ String path = operation.getRequestPredicate().getPath();
+ return new Link(requestUrl + (path.startsWith("/") ? path : "/" + path));
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/Link.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/Link.java
new file mode 100644
index 0000000000..9b2ecec8b7
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/Link.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+import org.springframework.core.style.ToStringCreator;
+
+/**
+ * Details for a link in a
+ * HAL-formatted
+ * response.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class Link {
+
+ private final String href;
+
+ private final boolean templated;
+
+ /**
+ * Creates a new {@link Link} with the given {@code href}.
+ * @param href the href
+ */
+ public Link(String href) {
+ this.href = href;
+ this.templated = href.contains("{");
+
+ }
+
+ /**
+ * Returns the href of the link.
+ * @return the href
+ */
+ public String getHref() {
+ return this.href;
+ }
+
+ /**
+ * Returns whether or not the {@link #getHref() href} is templated.
+ * @return {@code true} if the href is templated, otherwise {@code false}
+ */
+ public boolean isTemplated() {
+ return this.templated;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("href", this.href).toString();
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/OperationRequestPredicate.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/OperationRequestPredicate.java
new file mode 100644
index 0000000000..269877976b
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/OperationRequestPredicate.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.springframework.core.style.ToStringCreator;
+
+/**
+ * A predicate for a request to an operation on a web endpoint.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class OperationRequestPredicate {
+
+ private final String path;
+
+ private final String canonicalPath;
+
+ private final WebEndpointHttpMethod httpMethod;
+
+ private final Collection consumes;
+
+ private final Collection produces;
+
+ /**
+ * Creates a new {@code WebEndpointRequestPredict}.
+ *
+ * @param path the path for the operation
+ * @param httpMethod the HTTP method that the operation supports
+ * @param produces the media types that the operation produces
+ * @param consumes the media types that the operation consumes
+ */
+ public OperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod,
+ Collection consumes, Collection produces) {
+ this.path = path;
+ this.canonicalPath = path.replaceAll("\\{.*?}", "{*}");
+ this.httpMethod = httpMethod;
+ this.consumes = consumes;
+ this.produces = produces;
+ }
+
+ /**
+ * Returns the path for the operation.
+ * @return the path
+ */
+ public String getPath() {
+ return this.path;
+ }
+
+ /**
+ * Returns the HTTP method for the operation.
+ * @return the HTTP method
+ */
+ public WebEndpointHttpMethod getHttpMethod() {
+ return this.httpMethod;
+ }
+
+ /**
+ * Returns the media types that the operation consumes.
+ * @return the consumed media types
+ */
+ public Collection getConsumes() {
+ return Collections.unmodifiableCollection(this.consumes);
+ }
+
+ /**
+ * Returns the media types that the operation produces.
+ * @return the produced media types
+ */
+ public Collection getProduces() {
+ return Collections.unmodifiableCollection(this.produces);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("httpMethod", this.httpMethod)
+ .append("path", this.path).append("consumes", this.consumes)
+ .append("produces", this.produces).toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + this.consumes.hashCode();
+ result = prime * result + this.httpMethod.hashCode();
+ result = prime * result + this.canonicalPath.hashCode();
+ result = prime * result + this.produces.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ OperationRequestPredicate other = (OperationRequestPredicate) obj;
+ if (!this.consumes.equals(other.consumes)) {
+ return false;
+ }
+ if (this.httpMethod != other.httpMethod) {
+ return false;
+ }
+ if (!this.canonicalPath.equals(other.canonicalPath)) {
+ return false;
+ }
+ if (!this.produces.equals(other.produces)) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java
new file mode 100644
index 0000000000..35390beb12
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.reactivestreams.Publisher;
+
+import org.springframework.boot.endpoint.AnnotationEndpointDiscoverer;
+import org.springframework.boot.endpoint.CachingConfiguration;
+import org.springframework.boot.endpoint.CachingOperationInvoker;
+import org.springframework.boot.endpoint.Endpoint;
+import org.springframework.boot.endpoint.EndpointInfo;
+import org.springframework.boot.endpoint.EndpointOperationType;
+import org.springframework.boot.endpoint.EndpointType;
+import org.springframework.boot.endpoint.OperationInvoker;
+import org.springframework.boot.endpoint.OperationParameterMapper;
+import org.springframework.boot.endpoint.ReflectiveOperationInvoker;
+import org.springframework.boot.endpoint.Selector;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.io.Resource;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
+ * {@link WebEndpointExtension web extensions} applied to them.
+ *
+ * @author Andy Wilkinson
+ * @author Stephane Nicoll
+ * @since 2.0.0
+ */
+public class WebAnnotationEndpointDiscoverer extends
+ AnnotationEndpointDiscoverer {
+
+ /**
+ * Creates a new {@link WebAnnotationEndpointDiscoverer} that will discover
+ * {@link Endpoint endpoints} and {@link WebEndpointExtension web extensions} using
+ * the given {@link ApplicationContext}.
+ * @param applicationContext the application context
+ * @param operationParameterMapper the {@link OperationParameterMapper} used to
+ * convert arguments when an operation is invoked
+ * @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
+ * @param consumedMediaTypes the media types consumed by web endpoint operations
+ * @param producedMediaTypes the media types produced by web endpoint operations
+ */
+ public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
+ OperationParameterMapper operationParameterMapper,
+ Function cachingConfigurationFactory,
+ Collection consumedMediaTypes,
+ Collection producedMediaTypes) {
+ super(applicationContext,
+ new WebEndpointOperationFactory(operationParameterMapper,
+ consumedMediaTypes, producedMediaTypes),
+ WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory);
+ }
+
+ @Override
+ public Collection> discoverEndpoints() {
+ Collection> endpoints = discoverEndpointsWithExtension(
+ WebEndpointExtension.class, EndpointType.WEB);
+ verifyThatOperationsHaveDistinctPredicates(endpoints);
+ return endpoints.stream().map(EndpointInfoDescriptor::getEndpointInfo)
+ .collect(Collectors.toList());
+ }
+
+ private void verifyThatOperationsHaveDistinctPredicates(
+ Collection> endpointDescriptors) {
+ List> clashes = new ArrayList<>();
+ endpointDescriptors.forEach((descriptor) -> clashes
+ .addAll(descriptor.findDuplicateOperations().values()));
+ if (!clashes.isEmpty()) {
+ StringBuilder message = new StringBuilder();
+ message.append(String.format(
+ "Found multiple web operations with matching request predicates:%n"));
+ clashes.forEach((clash) -> {
+ message.append(" ").append(clash.get(0).getRequestPredicate())
+ .append(String.format(":%n"));
+ clash.forEach((operation) -> message.append(" ")
+ .append(String.format("%s%n", operation)));
+ });
+ throw new IllegalStateException(message.toString());
+ }
+ }
+
+ private static final class WebEndpointOperationFactory
+ implements EndpointOperationFactory {
+
+ private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent(
+ "org.reactivestreams.Publisher",
+ WebEndpointOperationFactory.class.getClassLoader());
+
+ private final OperationParameterMapper parameterMapper;
+
+ private final Collection consumedMediaTypes;
+
+ private final Collection producedMediaTypes;
+
+ private WebEndpointOperationFactory(OperationParameterMapper parameterMapper,
+ Collection consumedMediaTypes,
+ Collection producedMediaTypes) {
+ this.parameterMapper = parameterMapper;
+ this.consumedMediaTypes = consumedMediaTypes;
+ this.producedMediaTypes = producedMediaTypes;
+ }
+
+ @Override
+ public WebEndpointOperation createOperation(String endpointId,
+ AnnotationAttributes operationAttributes, Object target, Method method,
+ EndpointOperationType type, long timeToLive) {
+ WebEndpointHttpMethod httpMethod = determineHttpMethod(type);
+ OperationRequestPredicate requestPredicate = new OperationRequestPredicate(
+ determinePath(endpointId, method), httpMethod,
+ determineConsumedMediaTypes(httpMethod, method),
+ determineProducedMediaTypes(method));
+ OperationInvoker invoker = new ReflectiveOperationInvoker(
+ this.parameterMapper, target, method);
+ if (timeToLive > 0) {
+ invoker = new CachingOperationInvoker(invoker, timeToLive);
+ }
+ return new WebEndpointOperation(type, invoker, determineBlocking(method),
+ requestPredicate, determineId(endpointId, method));
+ }
+
+ private String determinePath(String endpointId, Method operationMethod) {
+ StringBuilder path = new StringBuilder(endpointId);
+ Stream.of(operationMethod.getParameters())
+ .filter((
+ parameter) -> parameter.getAnnotation(Selector.class) != null)
+ .map((parameter) -> "/{" + parameter.getName() + "}")
+ .forEach(path::append);
+ return path.toString();
+ }
+
+ private String determineId(String endpointId, Method operationMethod) {
+ StringBuilder path = new StringBuilder(endpointId);
+ Stream.of(operationMethod.getParameters())
+ .filter((
+ parameter) -> parameter.getAnnotation(Selector.class) != null)
+ .map((parameter) -> "-" + parameter.getName()).forEach(path::append);
+ return path.toString();
+ }
+
+ private Collection determineConsumedMediaTypes(
+ WebEndpointHttpMethod httpMethod, Method method) {
+ if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) {
+ return this.consumedMediaTypes;
+ }
+ return Collections.emptyList();
+ }
+
+ private Collection determineProducedMediaTypes(Method method) {
+ if (Void.class.equals(method.getReturnType())
+ || void.class.equals(method.getReturnType())) {
+ return Collections.emptyList();
+ }
+ if (producesResourceResponseBody(method)) {
+ return Collections.singletonList("application/octet-stream");
+ }
+ return this.producedMediaTypes;
+ }
+
+ private boolean producesResourceResponseBody(Method method) {
+ if (Resource.class.equals(method.getReturnType())) {
+ return true;
+ }
+ if (WebEndpointResponse.class.isAssignableFrom(method.getReturnType())) {
+ ResolvableType returnType = ResolvableType.forMethodReturnType(method);
+ if (ResolvableType.forClass(Resource.class)
+ .isAssignableFrom(returnType.getGeneric(0))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean consumesRequestBody(Method method) {
+ return Stream.of(method.getParameters()).anyMatch(
+ (parameter) -> parameter.getAnnotation(Selector.class) == null);
+ }
+
+ private WebEndpointHttpMethod determineHttpMethod(
+ EndpointOperationType operationType) {
+ if (operationType == EndpointOperationType.WRITE) {
+ return WebEndpointHttpMethod.POST;
+ }
+ return WebEndpointHttpMethod.GET;
+ }
+
+ private boolean determineBlocking(Method method) {
+ return !REACTIVE_STREAMS_PRESENT
+ || !Publisher.class.isAssignableFrom(method.getReturnType());
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointExtension.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointExtension.java
new file mode 100644
index 0000000000..f89416fb23
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointExtension.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+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.endpoint.Endpoint;
+
+/**
+ * Identifies a type as being a Web-specific extension of an {@link Endpoint}.
+ *
+ * @author Andy Wilkinson
+ * @author Stephane Nicoll
+ * @since 2.0.0
+ * @see Endpoint
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface WebEndpointExtension {
+
+ /**
+ * The {@link Endpoint endpoint} class to which this Web extension relates.
+ * @return the endpoint class
+ */
+ Class> endpoint();
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java
new file mode 100644
index 0000000000..d43d41b872
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+/**
+ * An enumeration of HTTP methods supported by web endpoint operations.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public enum WebEndpointHttpMethod {
+
+ /**
+ * An HTTP GET request.
+ */
+ GET,
+
+ /**
+ * An HTTP POST request.
+ */
+ POST
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointOperation.java
new file mode 100644
index 0000000000..df87213729
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointOperation.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+import org.springframework.boot.endpoint.EndpointOperation;
+import org.springframework.boot.endpoint.EndpointOperationType;
+import org.springframework.boot.endpoint.OperationInvoker;
+
+/**
+ * An operation on a web endpoint.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class WebEndpointOperation extends EndpointOperation {
+
+ private final OperationRequestPredicate requestPredicate;
+
+ private final String id;
+
+ /**
+ * Creates a new {@code WebEndpointOperation} with the given {@code type}. The
+ * operation can be performed using the given {@code operationInvoker}. The operation
+ * can handle requests that match the given {@code requestPredicate}.
+ * @param type the type of the operation
+ * @param operationInvoker used to perform the operation
+ * @param blocking whether or not this is a blocking operation
+ * @param requestPredicate the predicate for requests that can be handled by the
+ * @param id the id of the operation, unique within its endpoint operation
+ */
+ public WebEndpointOperation(EndpointOperationType type,
+ OperationInvoker operationInvoker, boolean blocking,
+ OperationRequestPredicate requestPredicate, String id) {
+ super(type, operationInvoker, blocking);
+ this.requestPredicate = requestPredicate;
+ this.id = id;
+ }
+
+ /**
+ * Returns the predicate for requests that can be handled by this operation.
+ * @return the predicate
+ */
+ public OperationRequestPredicate getRequestPredicate() {
+ return this.requestPredicate;
+ }
+
+ /**
+ * Returns the ID of the operation that uniquely identifies it within its endpoint.
+ * @return the ID
+ */
+ public String getId() {
+ return this.id;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointResponse.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointResponse.java
new file mode 100644
index 0000000000..ae50b1c14e
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointResponse.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web;
+
+/**
+ * A {@code WebEndpointResponse} can be returned by an operation on a
+ * {@link WebEndpointExtension} to provide additional, web-specific information such as
+ * the HTTP status code.
+ *
+ * @param the type of the response body
+ * @author Stephane Nicoll
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public final class WebEndpointResponse {
+
+ private final T body;
+
+ private final int status;
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with no body and a 200 (OK) status.
+ */
+ public WebEndpointResponse() {
+ this(null);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with no body and the given
+ * {@code status}.
+ * @param status the HTTP status
+ */
+ public WebEndpointResponse(int status) {
+ this(null, status);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with then given body and a 200 (OK)
+ * status.
+ * @param body the body
+ */
+ public WebEndpointResponse(T body) {
+ this(body, 200);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with then given body and status.
+ * @param body the body
+ * @param status the HTTP status
+ */
+ public WebEndpointResponse(T body, int status) {
+ this.body = body;
+ this.status = status;
+ }
+
+ /**
+ * Returns the body for the response.
+ * @return the body
+ */
+ public T getBody() {
+ return this.body;
+ }
+
+ /**
+ * Returns the status for the response.
+ * @return the status
+ */
+ public int getStatus() {
+ return this.status;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java
new file mode 100644
index 0000000000..eb49a5a58c
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web.jersey;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.glassfish.jersey.process.Inflector;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.server.model.Resource.Builder;
+
+import org.springframework.boot.endpoint.EndpointInfo;
+import org.springframework.boot.endpoint.OperationInvoker;
+import org.springframework.boot.endpoint.ParameterMappingException;
+import org.springframework.boot.endpoint.web.EndpointLinksResolver;
+import org.springframework.boot.endpoint.web.Link;
+import org.springframework.boot.endpoint.web.OperationRequestPredicate;
+import org.springframework.boot.endpoint.web.WebEndpointOperation;
+import org.springframework.boot.endpoint.web.WebEndpointResponse;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * A factory for creating Jersey {@link Resource Resources} for web endpoint operations.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class JerseyEndpointResourceFactory {
+
+ private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
+
+ /**
+ * Creates {@link Resource Resources} for the operations of the given
+ * {@code webEndpoints}.
+ * @param endpointPath the path beneath which all endpoints should be mapped
+ * @param webEndpoints the web endpoints
+ * @return the resources for the operations
+ */
+ public Collection createEndpointResources(String endpointPath,
+ Collection> webEndpoints) {
+ List resources = new ArrayList<>();
+ webEndpoints.stream()
+ .flatMap((endpointInfo) -> endpointInfo.getOperations().stream())
+ .map((operation) -> createResource(endpointPath, operation))
+ .forEach(resources::add);
+ resources.add(createEndpointLinksResource(endpointPath, webEndpoints));
+ return resources;
+ }
+
+ private Resource createResource(String endpointPath, WebEndpointOperation operation) {
+ OperationRequestPredicate requestPredicate = operation.getRequestPredicate();
+ Builder resourceBuilder = Resource.builder()
+ .path(endpointPath + "/" + requestPredicate.getPath());
+ resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
+ .consumes(toStringArray(requestPredicate.getConsumes()))
+ .produces(toStringArray(requestPredicate.getProduces()))
+ .handledBy(new EndpointInvokingInflector(operation.getOperationInvoker(),
+ !requestPredicate.getConsumes().isEmpty()));
+ return resourceBuilder.build();
+ }
+
+ private String[] toStringArray(Collection collection) {
+ return collection.toArray(new String[collection.size()]);
+ }
+
+ private Resource createEndpointLinksResource(String endpointPath,
+ Collection> webEndpoints) {
+ Builder resourceBuilder = Resource.builder().path(endpointPath);
+ resourceBuilder.addMethod("GET").handledBy(
+ new EndpointLinksInflector(webEndpoints, this.endpointLinksResolver));
+ return resourceBuilder.build();
+ }
+
+ private static final class EndpointInvokingInflector
+ implements Inflector {
+
+ private final OperationInvoker operationInvoker;
+
+ private final boolean readBody;
+
+ private EndpointInvokingInflector(OperationInvoker operationInvoker,
+ boolean readBody) {
+ this.operationInvoker = operationInvoker;
+ this.readBody = readBody;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Response apply(ContainerRequestContext data) {
+ Map arguments = new HashMap<>();
+ if (this.readBody) {
+ Map body = ((ContainerRequest) data)
+ .readEntity(Map.class);
+ if (body != null) {
+ arguments.putAll(body);
+ }
+ }
+ arguments.putAll(extractPathParmeters(data));
+ arguments.putAll(extractQueryParmeters(data));
+ try {
+ return convertToJaxRsResponse(this.operationInvoker.invoke(arguments),
+ data.getRequest().getMethod());
+ }
+ catch (ParameterMappingException ex) {
+ return Response.status(Status.BAD_REQUEST).build();
+ }
+ }
+
+ private Map extractPathParmeters(
+ ContainerRequestContext requestContext) {
+ return extract(requestContext.getUriInfo().getPathParameters());
+ }
+
+ private Map extractQueryParmeters(
+ ContainerRequestContext requestContext) {
+ return extract(requestContext.getUriInfo().getQueryParameters());
+ }
+
+ private Map extract(
+ MultivaluedMap multivaluedMap) {
+ Map result = new HashMap<>();
+ multivaluedMap.forEach((name, values) -> {
+ if (!CollectionUtils.isEmpty(values)) {
+ result.put(name, values.size() == 1 ? values.get(0) : values);
+ }
+ });
+ return result;
+ }
+
+ private Response convertToJaxRsResponse(Object response, String httpMethod) {
+ if (response == null) {
+ return Response.status(HttpMethod.GET.equals(httpMethod)
+ ? Status.NOT_FOUND : Status.NO_CONTENT).build();
+ }
+ try {
+ if (!(response instanceof WebEndpointResponse)) {
+ return Response.status(Status.OK).entity(convertIfNecessary(response))
+ .build();
+ }
+ WebEndpointResponse> webEndpointResponse = (WebEndpointResponse>) response;
+ return Response.status(webEndpointResponse.getStatus())
+ .entity(convertIfNecessary(webEndpointResponse.getBody()))
+ .build();
+ }
+ catch (IOException ex) {
+ return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+ private Object convertIfNecessary(Object body) throws IOException {
+ if (body instanceof org.springframework.core.io.Resource) {
+ return ((org.springframework.core.io.Resource) body).getInputStream();
+ }
+ return body;
+ }
+
+ }
+
+ private static final class EndpointLinksInflector
+ implements Inflector {
+
+ private final Collection> endpoints;
+
+ private final EndpointLinksResolver linksResolver;
+
+ private EndpointLinksInflector(
+ Collection> endpoints,
+ EndpointLinksResolver linksResolver) {
+ this.endpoints = endpoints;
+ this.linksResolver = linksResolver;
+ }
+
+ @Override
+ public Response apply(ContainerRequestContext request) {
+ Map links = this.linksResolver.resolveLinks(this.endpoints,
+ request.getUriInfo().getAbsolutePath().toString());
+ return Response.ok(Collections.singletonMap("_links", links)).build();
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/package-info.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/package-info.java
new file mode 100644
index 0000000000..65469b9973
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2017 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.
+ */
+
+/**
+ * Jersey web endpoint support.
+ */
+package org.springframework.boot.endpoint.web.jersey;
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java
new file mode 100644
index 0000000000..77065dd8c1
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2012-2017 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.endpoint.web.mvc;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.boot.endpoint.EndpointInfo;
+import org.springframework.boot.endpoint.OperationInvoker;
+import org.springframework.boot.endpoint.ParameterMappingException;
+import org.springframework.boot.endpoint.web.EndpointLinksResolver;
+import org.springframework.boot.endpoint.web.Link;
+import org.springframework.boot.endpoint.web.OperationRequestPredicate;
+import org.springframework.boot.endpoint.web.WebEndpointOperation;
+import org.springframework.boot.endpoint.web.WebEndpointResponse;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
+import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
+import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
+import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
+
+/**
+ * A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available
+ * over HTTP using Spring MVC.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class WebEndpointServletHandlerMapping extends RequestMappingInfoHandlerMapping
+ implements InitializingBean {
+
+ private final Method handle = ReflectionUtils.findMethod(OperationHandler.class,
+ "handle", HttpServletRequest.class, Map.class);
+
+ private final Method links = ReflectionUtils.findMethod(
+ WebEndpointServletHandlerMapping.class, "links", HttpServletRequest.class);
+
+ private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
+
+ private final String endpointPath;
+
+ private final Collection> webEndpoints;
+
+ private final CorsConfiguration corsConfiguration;
+
+ /**
+ * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
+ * operations of the given {@code webEndpoints}.
+ * @param endpointPath the path beneath which all endpoints should be mapped
+ * @param collection the web endpoints operations
+ */
+ public WebEndpointServletHandlerMapping(String endpointPath,
+ Collection> collection) {
+ this(endpointPath, collection, null);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
+ * operations of the given {@code webEndpoints}.
+ * @param endpointPath the path beneath which all endpoints should be mapped
+ * @param webEndpoints the web endpoints
+ * @param corsConfiguration the CORS configuraton for the endpoints
+ */
+ public WebEndpointServletHandlerMapping(String endpointPath,
+ Collection> webEndpoints,
+ CorsConfiguration corsConfiguration) {
+ this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath;
+ this.webEndpoints = webEndpoints;
+ this.corsConfiguration = corsConfiguration;
+ setOrder(-100);
+ }
+
+ @Override
+ protected void initHandlerMethods() {
+ this.webEndpoints.stream()
+ .flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
+ .forEach(this::registerMappingForOperation);
+ registerMapping(new RequestMappingInfo(patternsRequestConditionForPattern(""),
+ new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null,
+ null, null), this, this.links);
+ }
+
+ @Override
+ protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
+ RequestMappingInfo mapping) {
+ return this.corsConfiguration;
+ }
+
+ private void registerMappingForOperation(WebEndpointOperation operation) {
+ registerMapping(createRequestMappingInfo(operation),
+ new OperationHandler(operation.getOperationInvoker()), this.handle);
+ }
+
+ private RequestMappingInfo createRequestMappingInfo(
+ WebEndpointOperation operationInfo) {
+ OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
+ return new RequestMappingInfo(null,
+ patternsRequestConditionForPattern(requestPredicate.getPath()),
+ new RequestMethodsRequestCondition(
+ RequestMethod.valueOf(requestPredicate.getHttpMethod().name())),
+ null, null,
+ new ConsumesRequestCondition(
+ toStringArray(requestPredicate.getConsumes())),
+ new ProducesRequestCondition(
+ toStringArray(requestPredicate.getProduces())),
+ null);
+ }
+
+ private PatternsRequestCondition patternsRequestConditionForPattern(String path) {
+ return new PatternsRequestCondition(
+ new String[] { this.endpointPath
+ + (StringUtils.hasText(path) ? "/" + path : "") },
+ null, null, false, false);
+ }
+
+ private String[] toStringArray(Collection collection) {
+ return collection.toArray(new String[collection.size()]);
+ }
+
+ @Override
+ protected boolean isHandler(Class> beanType) {
+ return false;
+ }
+
+ @Override
+ protected RequestMappingInfo getMappingForMethod(Method method,
+ Class> handlerType) {
+ return null;
+ }
+
+ @Override
+ protected void extendInterceptors(List