Commit 9687a504 authored by Andy Wilkinson's avatar Andy Wilkinson

Add support for making endpoints accessible via HTTP

This commit adds support for exposing endpoint operations over HTTP.
Jersey, Spring MVC, and WebFlux are all supported but the programming
model remains web framework agnostic. When using WebFlux, blocking
operations are automatically performed on a separate thread using
Reactor's scheduler support. Support for web-specific extensions is
provided via a new `@WebEndpointExtension` annotation.

Closes gh-7970
Closes gh-9946
Closes gh-9947
parent 4592e071
......@@ -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
......
......@@ -29,6 +29,22 @@
</subpackage>
</subpackage>
<!-- Endpoint infrastructure -->
<subpackage name="endpoint">
<disallow pkg="org.springframework.http" />
<disallow pkg="org.springframework.web" />
<subpackage name="web">
<allow pkg="org.springframework.http" />
<allow pkg="org.springframework.web" />
<subpackage name="mvc">
<disallow pkg="org.springframework.web.reactive" />
</subpackage>
<subpackage name="reactive">
<disallow pkg="org.springframework.web.servlet" />
</subpackage>
</subpackage>
</subpackage>
<!-- Logging -->
<subpackage name="logging">
<disallow pkg="org.springframework.context" />
......@@ -109,4 +125,4 @@
</subpackage>
</subpackage>
</import-control>
</import-control>
\ No newline at end of file
......@@ -159,6 +159,11 @@
<artifactId>jetty-webapp</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
......@@ -271,6 +276,11 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
......@@ -316,6 +326,16 @@
<artifactId>jaybird-jdk18</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
......@@ -352,4 +372,4 @@
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>
\ No newline at end of file
/*
* 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<String, Link> resolveLinks(
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
String requestUrl) {
String normalizedUrl = normalizeRequestUrl(requestUrl);
Map<String, Link> links = new LinkedHashMap<String, Link>();
links.put("self", new Link(normalizedUrl));
for (EndpointInfo<WebEndpointOperation> 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));
}
}
/*
* 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
* <a href="https://tools.ietf.org/html/draft-kelly-json-hal-08">HAL</a>-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();
}
}
/*
* 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<String> consumes;
private final Collection<String> 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<String> consumes, Collection<String> 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<String> getConsumes() {
return Collections.unmodifiableCollection(this.consumes);
}
/**
* Returns the media types that the operation produces.
* @return the produced media types
*/
public Collection<String> 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;
}
}
/*
* 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<WebEndpointOperation, OperationRequestPredicate> {
/**
* 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<String, CachingConfiguration> cachingConfigurationFactory,
Collection<String> consumedMediaTypes,
Collection<String> producedMediaTypes) {
super(applicationContext,
new WebEndpointOperationFactory(operationParameterMapper,
consumedMediaTypes, producedMediaTypes),
WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory);
}
@Override
public Collection<EndpointInfo<WebEndpointOperation>> discoverEndpoints() {
Collection<EndpointInfoDescriptor<WebEndpointOperation, OperationRequestPredicate>> endpoints = discoverEndpointsWithExtension(
WebEndpointExtension.class, EndpointType.WEB);
verifyThatOperationsHaveDistinctPredicates(endpoints);
return endpoints.stream().map(EndpointInfoDescriptor::getEndpointInfo)
.collect(Collectors.toList());
}
private void verifyThatOperationsHaveDistinctPredicates(
Collection<EndpointInfoDescriptor<WebEndpointOperation, OperationRequestPredicate>> endpointDescriptors) {
List<List<WebEndpointOperation>> 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<WebEndpointOperation> {
private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent(
"org.reactivestreams.Publisher",
WebEndpointOperationFactory.class.getClassLoader());
private final OperationParameterMapper parameterMapper;
private final Collection<String> consumedMediaTypes;
private final Collection<String> producedMediaTypes;
private WebEndpointOperationFactory(OperationParameterMapper parameterMapper,
Collection<String> consumedMediaTypes,
Collection<String> 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<String> determineConsumedMediaTypes(
WebEndpointHttpMethod httpMethod, Method method) {
if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) {
return this.consumedMediaTypes;
}
return Collections.emptyList();
}
private Collection<String> 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());
}
}
}
/*
* 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();
}
/*
* 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
}
/*
* 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;
}
}
/*
* 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 <T> the type of the response body
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public final class WebEndpointResponse<T> {
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;
}
}
/*
* 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<Resource> createEndpointResources(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints) {
List<Resource> 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<String> collection) {
return collection.toArray(new String[collection.size()]);
}
private Resource createEndpointLinksResource(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> 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<ContainerRequestContext, Object> {
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<String, Object> arguments = new HashMap<>();
if (this.readBody) {
Map<String, Object> 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<String, Object> extractPathParmeters(
ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getPathParameters());
}
private Map<String, Object> extractQueryParmeters(
ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getQueryParameters());
}
private Map<String, Object> extract(
MultivaluedMap<String, String> multivaluedMap) {
Map<String, Object> 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<ContainerRequestContext, Response> {
private final Collection<EndpointInfo<WebEndpointOperation>> endpoints;
private final EndpointLinksResolver linksResolver;
private EndpointLinksInflector(
Collection<EndpointInfo<WebEndpointOperation>> endpoints,
EndpointLinksResolver linksResolver) {
this.endpoints = endpoints;
this.linksResolver = linksResolver;
}
@Override
public Response apply(ContainerRequestContext request) {
Map<String, Link> links = this.linksResolver.resolveLinks(this.endpoints,
request.getUriInfo().getAbsolutePath().toString());
return Response.ok(Collections.singletonMap("_links", links)).build();
}
}
}
/*
* 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;
/*
* 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<EndpointInfo<WebEndpointOperation>> 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<EndpointInfo<WebEndpointOperation>> 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<EndpointInfo<WebEndpointOperation>> 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<String> 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<Object> interceptors) {
interceptors.add(new SkipPathExtensionContentNegotiation());
}
@ResponseBody
private Map<String, Map<String, Link>> links(HttpServletRequest request) {
return Collections.singletonMap("_links", this.endpointLinksResolver
.resolveLinks(this.webEndpoints, request.getRequestURL().toString()));
}
/**
* A handler for an endpoint operation.
*/
final class OperationHandler {
private final OperationInvoker operationInvoker;
OperationHandler(OperationInvoker operationInvoker) {
this.operationInvoker = operationInvoker;
}
@SuppressWarnings("unchecked")
@ResponseBody
public Object handle(HttpServletRequest request,
@RequestBody(required = false) Map<String, String> body) {
Map<String, Object> arguments = new HashMap<>((Map<String, String>) request
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());
if (body != null && HttpMethod.POST == httpMethod) {
arguments.putAll(body);
}
request.getParameterMap().forEach((name, values) -> arguments.put(name,
values.length == 1 ? values[0] : Arrays.asList(values)));
try {
return handleResult(this.operationInvoker.invoke(arguments), httpMethod);
}
catch (ParameterMappingException ex) {
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
}
}
private Object handleResult(Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT);
}
if (!(result instanceof WebEndpointResponse)) {
return result;
}
WebEndpointResponse<?> response = (WebEndpointResponse<?>) result;
return new ResponseEntity<Object>(response.getBody(),
HttpStatus.valueOf(response.getStatus()));
}
}
/**
* {@link HandlerInterceptorAdapter} to ensure that
* {@link PathExtensionContentNegotiationStrategy} is skipped for web endpoints.
*/
private static final class SkipPathExtensionContentNegotiation
extends HandlerInterceptorAdapter {
private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
.getName() + ".SKIP";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(SKIP_ATTRIBUTE, Boolean.TRUE);
return true;
}
}
}
/*
* 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.
*/
/**
* Spring MVC web endpoint support.
*/
package org.springframework.boot.endpoint.web.mvc;
/*
* 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.
*/
/**
* Web endpoint support.
*/
package org.springframework.boot.endpoint.web;
/*
* 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.reactive;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
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.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ReflectionUtils;
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.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.condition.ProducesRequestCondition;
import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available
* over HTTP using Spring WebFlux.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandlerMapping
implements InitializingBean {
private static final PathPatternParser pathPatternParser = new PathPatternParser();
private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
private final Method handleWrite = ReflectionUtils.findMethod(
WriteOperationHandler.class, "handle", ServerWebExchange.class, Map.class);
private final Method links = ReflectionUtils.findMethod(getClass(), "links",
ServerHttpRequest.class);
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final String endpointPath;
private final Collection<EndpointInfo<WebEndpointOperation>> 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
*/
public WebEndpointReactiveHandlerMapping(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> 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 WebEndpointReactiveHandlerMapping(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> 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(
new PatternsRequestCondition(pathPatternParser.parse(this.endpointPath)),
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) {
EndpointOperationType operationType = operation.getType();
registerMapping(createRequestMappingInfo(operation),
operationType == EndpointOperationType.WRITE
? new WriteOperationHandler(operation.getOperationInvoker())
: new ReadOperationHandler(operation.getOperationInvoker()),
operationType == EndpointOperationType.WRITE ? this.handleWrite
: this.handleRead);
}
private RequestMappingInfo createRequestMappingInfo(
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
return new RequestMappingInfo(null,
new PatternsRequestCondition(pathPatternParser
.parse(this.endpointPath + "/" + requestPredicate.getPath())),
new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name())),
null, null,
new ConsumesRequestCondition(
toStringArray(requestPredicate.getConsumes())),
new ProducesRequestCondition(
toStringArray(requestPredicate.getProduces())),
null);
}
private String[] toStringArray(Collection<String> 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;
}
@ResponseBody
private Map<String, Map<String, Link>> links(ServerHttpRequest request) {
return Collections.singletonMap("_links",
this.endpointLinksResolver.resolveLinks(this.webEndpoints,
UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null)
.toUriString()));
}
/**
* Base class for handlers for endpoint operations.
*/
abstract class AbstractOperationHandler {
private final OperationInvoker operationInvoker;
AbstractOperationHandler(OperationInvoker operationInvoker) {
this.operationInvoker = operationInvoker;
}
@SuppressWarnings("unchecked")
ResponseEntity<?> doHandle(ServerWebExchange exchange, Map<String, String> body) {
Map<String, Object> arguments = new HashMap<>((Map<String, String>) exchange
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
if (body != null) {
arguments.putAll(body);
}
exchange.getRequest().getQueryParams().forEach((name, values) -> arguments
.put(name, values.size() == 1 ? values.get(0) : values));
try {
return handleResult(this.operationInvoker.invoke(arguments),
exchange.getRequest().getMethod());
}
catch (ParameterMappingException ex) {
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
}
}
private ResponseEntity<?> handleResult(Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT);
}
if (!(result instanceof WebEndpointResponse)) {
return new ResponseEntity<>(result, HttpStatus.OK);
}
WebEndpointResponse<?> response = (WebEndpointResponse<?>) result;
return new ResponseEntity<Object>(response.getBody(),
HttpStatus.valueOf(response.getStatus()));
}
}
/**
* A handler for an endpoint write operation.
*/
final class WriteOperationHandler extends AbstractOperationHandler {
WriteOperationHandler(OperationInvoker operationInvoker) {
super(operationInvoker);
}
@ResponseBody
public ResponseEntity<?> handle(ServerWebExchange exchange,
@RequestBody(required = false) Map<String, String> body) {
return doHandle(exchange, body);
}
}
/**
* A handler for an endpoint write operation.
*/
final class ReadOperationHandler extends AbstractOperationHandler {
ReadOperationHandler(OperationInvoker operationInvoker) {
super(operationInvoker);
}
@ResponseBody
public ResponseEntity<?> handle(ServerWebExchange exchange) {
return doHandle(exchange, null);
}
}
}
/*
* 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.
*/
/**
* Reactive web endpoint support.
*/
package org.springframework.boot.endpoint.web.reactive;
/*
* 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.Arrays;
import java.util.Collections;
import java.util.Map;
import org.assertj.core.api.Condition;
import org.junit.Test;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link EndpointLinksResolver}.
*
* @author Andy Wilkinson
*/
public class EndpointLinksResolverTests {
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver();
@Test
public void linkResolutionWithTrailingSlashStripsSlashOnSelfLink() {
Map<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(),
"https://api.example.com/application/");
assertThat(links).hasSize(1);
assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/application"));
}
@Test
public void linkResolutionWithoutTrailingSlash() {
Map<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(),
"https://api.example.com/application");
assertThat(links).hasSize(1);
assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/application"));
}
@Test
public void resolvedLinksContainsALinkForEachEndpointOperation() {
Map<String, Link> links = this.linksResolver
.resolveLinks(
Arrays.asList(new EndpointInfo<>("alpha", true,
Arrays.asList(operationWithPath("/alpha", "alpha"),
operationWithPath("/alpha/{name}",
"alpha-name")))),
"https://api.example.com/application");
assertThat(links).hasSize(3);
assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/application"));
assertThat(links).hasEntrySatisfying("alpha",
linkWithHref("https://api.example.com/application/alpha"));
assertThat(links).hasEntrySatisfying("alpha-name",
linkWithHref("https://api.example.com/application/alpha/{name}"));
}
private WebEndpointOperation operationWithPath(String path, String id) {
return new WebEndpointOperation(EndpointOperationType.READ, null, false,
new OperationRequestPredicate(path, WebEndpointHttpMethod.GET,
Collections.emptyList(), Collections.emptyList()),
id);
}
private Condition<Link> linkWithHref(String href) {
return new Condition<>((link) -> href.equals(link.getHref()),
"Link with href '%s'", href);
}
}
/*
* 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.Collections;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OperationRequestPredicate}.
*
* @author Andy Wilkinson
*/
public class OperationRequestPredicateTests {
@Test
public void predicatesWithIdenticalPathsAreEqual() {
assertThat(predicateWithPath("/path")).isEqualTo(predicateWithPath("/path"));
}
@Test
public void predicatesWithDifferentPathsAreNotEqual() {
assertThat(predicateWithPath("/one")).isNotEqualTo(predicateWithPath("/two"));
}
@Test
public void predicatesWithIdenticalPathsWithVariablesAreEqual() {
assertThat(predicateWithPath("/path/{foo}"))
.isEqualTo(predicateWithPath("/path/{foo}"));
}
@Test
public void predicatesWhereOneHasAPathAndTheOtherHasAVariableAreNotEqual() {
assertThat(predicateWithPath("/path/{foo}"))
.isNotEqualTo(predicateWithPath("/path/foo"));
}
@Test
public void predicatesWithSinglePathVariablesInTheSamplePlaceAreEqual() {
assertThat(predicateWithPath("/path/{foo1}"))
.isEqualTo(predicateWithPath("/path/{foo2}"));
}
@Test
public void predicatesWithMultiplePathVariablesInTheSamplePlaceAreEqual() {
assertThat(predicateWithPath("/path/{foo1}/more/{bar1}"))
.isEqualTo(predicateWithPath("/path/{foo2}/more/{bar2}"));
}
private OperationRequestPredicate predicateWithPath(String path) {
return new OperationRequestPredicate(path, WebEndpointHttpMethod.GET,
Collections.emptyList(), Collections.emptyList());
}
}
/*
* 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.util.Collection;
import java.util.HashSet;
import javax.ws.rs.ext.ContextResolver;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.servlet.ServletContainer;
import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests;
import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Integration tests for web endpoints exposed using Jersey.
*
* @author Andy Wilkinson
*/
public class JerseyWebEndpointIntegrationTests extends
AbstractWebEndpointIntegrationTests<AnnotationConfigServletWebServerApplicationContext> {
public JerseyWebEndpointIntegrationTests() {
super(JerseyConfiguration.class);
}
@Override
protected AnnotationConfigServletWebServerApplicationContext createApplicationContext(
Class<?>... config) {
return new AnnotationConfigServletWebServerApplicationContext(config);
}
@Override
protected int getPort(AnnotationConfigServletWebServerApplicationContext context) {
return context.getWebServer().getPort();
}
@Configuration
static class JerseyConfiguration {
@Bean
public TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(0);
}
@Bean
public ServletRegistrationBean<ServletContainer> servletContainer(
ResourceConfig resourceConfig) {
return new ServletRegistrationBean<ServletContainer>(
new ServletContainer(resourceConfig), "/*");
}
@Bean
public ResourceConfig resourceConfig(
WebAnnotationEndpointDiscoverer endpointDiscoverer) {
ResourceConfig resourceConfig = new ResourceConfig();
Collection<Resource> resources = new JerseyEndpointResourceFactory()
.createEndpointResources("endpoints",
endpointDiscoverer.discoverEndpoints());
resourceConfig.registerResources(new HashSet<Resource>(resources));
resourceConfig.register(JacksonFeature.class);
resourceConfig.register(new ObjectMapperContextResolver(new ObjectMapper()),
ContextResolver.class);
return resourceConfig;
}
}
private static final class ObjectMapperContextResolver
implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
private ObjectMapperContextResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public ObjectMapper getContext(Class<?> type) {
return this.objectMapper;
}
}
}
/*
* 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.util.Arrays;
import org.junit.Test;
import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests;
import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* Integration tests for web endpoints exposed using Spring MVC.
*
* @author Andy Wilkinson
*/
public class MvcWebEndpointIntegrationTests extends
AbstractWebEndpointIntegrationTests<AnnotationConfigServletWebServerApplicationContext> {
public MvcWebEndpointIntegrationTests() {
super(WebMvcConfiguration.class);
}
@Test
public void responseToOptionsRequestIncludesCorsHeaders() {
load(TestEndpointConfiguration.class,
(client) -> client.options().uri("/test")
.accept(MediaType.APPLICATION_JSON)
.header("Access-Control-Request-Method", "POST")
.header("Origin", "http://example.com").exchange().expectStatus()
.isOk().expectHeader()
.valueEquals("Access-Control-Allow-Origin", "http://example.com")
.expectHeader()
.valueEquals("Access-Control-Allow-Methods", "GET,POST"));
}
@Override
protected AnnotationConfigServletWebServerApplicationContext createApplicationContext(
Class<?>... config) {
return new AnnotationConfigServletWebServerApplicationContext(config);
}
@Override
protected int getPort(AnnotationConfigServletWebServerApplicationContext context) {
return context.getWebServer().getPort();
}
@Configuration
@EnableWebMvc
static class WebMvcConfiguration {
@Bean
public TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(0);
}
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public WebEndpointServletHandlerMapping webEndpointHandlerMapping(
WebAnnotationEndpointDiscoverer webEndpointDiscoverer) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new WebEndpointServletHandlerMapping("/endpoints",
webEndpointDiscoverer.discoverEndpoints(), corsConfiguration);
}
}
}
/*
* 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.reactive;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests;
import org.springframework.boot.endpoint.web.WebAnnotationEndpointDiscoverer;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
import org.springframework.boot.web.reactive.context.ReactiveWebServerInitializedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Integration tests for web endpoints exposed using WebFlux.
*
* @author Andy Wilkinson
*/
public class ReactiveWebEndpointIntegrationTests
extends AbstractWebEndpointIntegrationTests<ReactiveWebServerApplicationContext> {
public ReactiveWebEndpointIntegrationTests() {
super(ReactiveConfiguration.class);
}
@Test
public void responseToOptionsRequestIncludesCorsHeaders() {
load(TestEndpointConfiguration.class,
(client) -> client.options().uri("/test")
.accept(MediaType.APPLICATION_JSON)
.header("Access-Control-Request-Method", "POST")
.header("Origin", "http://example.com").exchange().expectStatus()
.isOk().expectHeader()
.valueEquals("Access-Control-Allow-Origin", "http://example.com")
.expectHeader()
.valueEquals("Access-Control-Allow-Methods", "GET,POST"));
}
@Override
protected ReactiveWebServerApplicationContext createApplicationContext(
Class<?>... config) {
return new ReactiveWebServerApplicationContext(config);
}
@Override
protected int getPort(ReactiveWebServerApplicationContext context) {
return context.getBean(ReactiveConfiguration.class).port;
}
@Configuration
@EnableWebFlux
static class ReactiveConfiguration {
private int port;
@Bean
public NettyReactiveWebServerFactory netty() {
return new NettyReactiveWebServerFactory(0);
}
@Bean
public HttpHandler httpHandler(ApplicationContext applicationContext) {
return WebHttpHandlerBuilder.applicationContext(applicationContext).build();
}
@Bean
public WebEndpointReactiveHandlerMapping webEndpointHandlerMapping(
WebAnnotationEndpointDiscoverer endpointDiscoverer) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("http://example.com"));
corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST"));
return new WebEndpointReactiveHandlerMapping("endpoints",
endpointDiscoverer.discoverEndpoints(), corsConfiguration);
}
@Bean
public ApplicationListener<ReactiveWebServerInitializedEvent> serverInitializedListener() {
return (event) -> this.port = event.getWebServer().getPort();
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment