Merge branch 'endpoint-infrastructure'
This commit is contained in:
commit
4f45b2bb52
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
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 java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A base {@link EndpointDiscoverer} implementation that discovers {@link Endpoint} beans
|
||||
* in an application context.
|
||||
*
|
||||
* @param <T> the type of the operation
|
||||
* @param <K> the type of the operation key
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public abstract class AnnotationEndpointDiscoverer<T extends EndpointOperation, K>
|
||||
implements EndpointDiscoverer<T> {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final EndpointOperationFactory<T> operationFactory;
|
||||
|
||||
private final Function<T, K> operationKeyFactory;
|
||||
|
||||
private final Function<String, CachingConfiguration> cachingConfigurationFactory;
|
||||
|
||||
protected AnnotationEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
EndpointOperationFactory<T> operationFactory,
|
||||
Function<T, K> operationKeyFactory,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.operationFactory = operationFactory;
|
||||
this.operationKeyFactory = operationKeyFactory;
|
||||
this.cachingConfigurationFactory = cachingConfigurationFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform endpoint discovery, including discovery and merging of extensions.
|
||||
* @param extensionType the annotation type of the extension
|
||||
* @param endpointType the {@link EndpointType} that should be considered
|
||||
* @return the list of {@link EndpointInfo EndpointInfos} that describes the
|
||||
* discovered endpoints matching the specified {@link EndpointType}
|
||||
*/
|
||||
protected Collection<EndpointInfoDescriptor<T, K>> discoverEndpointsWithExtension(
|
||||
Class<? extends Annotation> extensionType, EndpointType endpointType) {
|
||||
Map<Class<?>, EndpointInfo<T>> endpoints = discoverGenericEndpoints(endpointType);
|
||||
Map<Class<?>, EndpointExtensionInfo<T>> extensions = discoverExtensions(endpoints,
|
||||
extensionType, endpointType);
|
||||
Collection<EndpointInfoDescriptor<T, K>> result = new ArrayList<>();
|
||||
endpoints.forEach((endpointClass, endpointInfo) -> {
|
||||
EndpointExtensionInfo<T> extension = extensions.remove(endpointClass);
|
||||
result.add(createDescriptor(endpointClass, endpointInfo, extension));
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private EndpointInfoDescriptor<T, K> createDescriptor(Class<?> endpointType,
|
||||
EndpointInfo<T> endpoint, EndpointExtensionInfo<T> extension) {
|
||||
Map<OperationKey<K>, List<T>> endpointOperations = indexOperations(
|
||||
endpoint.getId(), endpointType, endpoint.getOperations());
|
||||
if (extension != null) {
|
||||
endpointOperations.putAll(indexOperations(endpoint.getId(),
|
||||
extension.getEndpointExtensionType(), extension.getOperations()));
|
||||
return new EndpointInfoDescriptor<>(mergeEndpoint(endpoint, extension),
|
||||
endpointOperations);
|
||||
}
|
||||
else {
|
||||
return new EndpointInfoDescriptor<>(endpoint, endpointOperations);
|
||||
}
|
||||
}
|
||||
|
||||
private EndpointInfo<T> mergeEndpoint(EndpointInfo<T> endpoint,
|
||||
EndpointExtensionInfo<T> extension) {
|
||||
Map<K, T> operations = new HashMap<>();
|
||||
Consumer<T> operationConsumer = (operation) -> operations
|
||||
.put(this.operationKeyFactory.apply(operation), operation);
|
||||
endpoint.getOperations().forEach(operationConsumer);
|
||||
extension.getOperations().forEach(operationConsumer);
|
||||
return new EndpointInfo<>(endpoint.getId(), endpoint.isEnabledByDefault(),
|
||||
operations.values());
|
||||
}
|
||||
|
||||
private Map<OperationKey<K>, List<T>> indexOperations(String endpointId,
|
||||
Class<?> target, Collection<T> operations) {
|
||||
LinkedMultiValueMap<OperationKey<K>, T> operationByKey = new LinkedMultiValueMap<>();
|
||||
operations
|
||||
.forEach(
|
||||
(operation) -> operationByKey.add(
|
||||
new OperationKey<>(endpointId, target,
|
||||
this.operationKeyFactory.apply(operation)),
|
||||
operation));
|
||||
return operationByKey;
|
||||
}
|
||||
|
||||
private Map<Class<?>, EndpointInfo<T>> discoverGenericEndpoints(
|
||||
EndpointType endpointType) {
|
||||
String[] endpointBeanNames = this.applicationContext
|
||||
.getBeanNamesForAnnotation(Endpoint.class);
|
||||
Map<String, EndpointInfo<T>> endpointsById = new HashMap<>();
|
||||
Map<Class<?>, EndpointInfo<T>> endpointsByClass = new HashMap<>();
|
||||
for (String beanName : endpointBeanNames) {
|
||||
Class<?> beanType = this.applicationContext.getType(beanName);
|
||||
AnnotationAttributes endpointAttributes = AnnotatedElementUtils
|
||||
.findMergedAnnotationAttributes(beanType, Endpoint.class, true, true);
|
||||
String endpointId = endpointAttributes.getString("id");
|
||||
if (matchEndpointType(endpointAttributes, endpointType)) {
|
||||
EndpointInfo<T> endpointInfo = createEndpointInfo(endpointsById, beanName,
|
||||
beanType, endpointAttributes, endpointId);
|
||||
endpointsByClass.put(beanType, endpointInfo);
|
||||
}
|
||||
}
|
||||
return endpointsByClass;
|
||||
}
|
||||
|
||||
private EndpointInfo<T> createEndpointInfo(Map<String, EndpointInfo<T>> endpointsById,
|
||||
String beanName, Class<?> beanType, AnnotationAttributes endpointAttributes,
|
||||
String endpointId) {
|
||||
Map<Method, T> operationMethods = discoverOperations(endpointId, beanName,
|
||||
beanType);
|
||||
EndpointInfo<T> endpointInfo = new EndpointInfo<>(endpointId,
|
||||
endpointAttributes.getBoolean("enabledByDefault"),
|
||||
operationMethods.values());
|
||||
EndpointInfo<T> previous = endpointsById.putIfAbsent(endpointInfo.getId(),
|
||||
endpointInfo);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Found two endpoints with the id '"
|
||||
+ endpointInfo.getId() + "': " + endpointInfo + " and " + previous);
|
||||
}
|
||||
return endpointInfo;
|
||||
}
|
||||
|
||||
private Map<Class<?>, EndpointExtensionInfo<T>> discoverExtensions(
|
||||
Map<Class<?>, EndpointInfo<T>> endpoints,
|
||||
Class<? extends Annotation> extensionType, EndpointType endpointType) {
|
||||
if (extensionType == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
String[] extensionBeanNames = this.applicationContext
|
||||
.getBeanNamesForAnnotation(extensionType);
|
||||
Map<Class<?>, EndpointExtensionInfo<T>> extensionsByEndpoint = new HashMap<>();
|
||||
for (String beanName : extensionBeanNames) {
|
||||
Class<?> beanType = this.applicationContext.getType(beanName);
|
||||
AnnotationAttributes extensionAttributes = AnnotatedElementUtils
|
||||
.getMergedAnnotationAttributes(beanType, extensionType);
|
||||
Class<?> endpointClass = (Class<?>) extensionAttributes.get("endpoint");
|
||||
AnnotationAttributes endpointAttributes = AnnotatedElementUtils
|
||||
.getMergedAnnotationAttributes(endpointClass, Endpoint.class);
|
||||
if (!matchEndpointType(endpointAttributes, endpointType)) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Invalid extension %s': "
|
||||
+ "endpoint '%s' does not support such extension",
|
||||
beanType.getName(), endpointClass.getName()));
|
||||
}
|
||||
EndpointInfo<T> endpoint = endpoints.get(endpointClass);
|
||||
if (endpoint == null) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"Invalid extension '%s': no endpoint found with type '%s'",
|
||||
beanType.getName(), endpointClass.getName()));
|
||||
}
|
||||
Map<Method, T> operationMethods = discoverOperations(endpoint.getId(),
|
||||
beanName, beanType);
|
||||
EndpointExtensionInfo<T> extension = new EndpointExtensionInfo<>(beanType,
|
||||
operationMethods.values());
|
||||
EndpointExtensionInfo<T> previous = extensionsByEndpoint
|
||||
.putIfAbsent(endpointClass, extension);
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException(
|
||||
"Found two extensions for the same endpoint '"
|
||||
+ endpointClass.getName() + "': "
|
||||
+ extension.getEndpointExtensionType().getName() + " and "
|
||||
+ previous.getEndpointExtensionType().getName());
|
||||
}
|
||||
}
|
||||
return extensionsByEndpoint;
|
||||
}
|
||||
|
||||
private boolean matchEndpointType(AnnotationAttributes endpointAttributes,
|
||||
EndpointType endpointType) {
|
||||
if (endpointType == null) {
|
||||
return true;
|
||||
}
|
||||
Object types = endpointAttributes.get("types");
|
||||
if (ObjectUtils.isEmpty(types)) {
|
||||
return true;
|
||||
}
|
||||
return Arrays.stream((EndpointType[]) types)
|
||||
.anyMatch((t) -> t.equals(endpointType));
|
||||
}
|
||||
|
||||
private Map<Method, T> discoverOperations(String endpointId, String beanName,
|
||||
Class<?> beanType) {
|
||||
return MethodIntrospector.selectMethods(beanType,
|
||||
(MethodIntrospector.MetadataLookup<T>) (
|
||||
method) -> createOperationIfPossible(endpointId, beanName,
|
||||
method));
|
||||
}
|
||||
|
||||
private T createOperationIfPossible(String endpointId, String beanName,
|
||||
Method method) {
|
||||
T readOperation = createReadOperationIfPossible(endpointId, beanName, method);
|
||||
return readOperation != null ? readOperation
|
||||
: createWriteOperationIfPossible(endpointId, beanName, method);
|
||||
}
|
||||
|
||||
private T createReadOperationIfPossible(String endpointId, String beanName,
|
||||
Method method) {
|
||||
return createOperationIfPossible(endpointId, beanName, method,
|
||||
ReadOperation.class, EndpointOperationType.READ);
|
||||
}
|
||||
|
||||
private T createWriteOperationIfPossible(String endpointId, String beanName,
|
||||
Method method) {
|
||||
return createOperationIfPossible(endpointId, beanName, method,
|
||||
WriteOperation.class, EndpointOperationType.WRITE);
|
||||
}
|
||||
|
||||
private T createOperationIfPossible(String endpointId, String beanName, Method method,
|
||||
Class<? extends Annotation> operationAnnotation,
|
||||
EndpointOperationType operationType) {
|
||||
AnnotationAttributes operationAttributes = AnnotatedElementUtils
|
||||
.getMergedAnnotationAttributes(method, operationAnnotation);
|
||||
if (operationAttributes == null) {
|
||||
return null;
|
||||
}
|
||||
CachingConfiguration cachingConfiguration = this.cachingConfigurationFactory
|
||||
.apply(endpointId);
|
||||
return this.operationFactory.createOperation(endpointId, operationAttributes,
|
||||
this.applicationContext.getBean(beanName), method, operationType,
|
||||
determineTimeToLive(cachingConfiguration, operationType, method));
|
||||
}
|
||||
|
||||
private long determineTimeToLive(CachingConfiguration cachingConfiguration,
|
||||
EndpointOperationType operationType, Method method) {
|
||||
if (cachingConfiguration != null && cachingConfiguration.getTimeToLive() > 0
|
||||
&& operationType == EndpointOperationType.READ
|
||||
&& method.getParameters().length == 0) {
|
||||
return cachingConfiguration.getTimeToLive();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@code EndpointOperationFactory} creates an {@link EndpointOperation} for an
|
||||
* operation on an endpoint.
|
||||
*
|
||||
* @param <T> the {@link EndpointOperation} type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
protected interface EndpointOperationFactory<T extends EndpointOperation> {
|
||||
|
||||
/**
|
||||
* Creates an {@code EndpointOperation} for an operation on an endpoint.
|
||||
* @param endpointId the id of the endpoint
|
||||
* @param operationAttributes the annotation attributes for the operation
|
||||
* @param target the target that implements the operation
|
||||
* @param operationMethod the method on the bean that implements the operation
|
||||
* @param operationType the type of the operation
|
||||
* @param timeToLive the caching period in milliseconds
|
||||
* @return the operation info that describes the operation
|
||||
*/
|
||||
T createOperation(String endpointId, AnnotationAttributes operationAttributes,
|
||||
Object target, Method operationMethod,
|
||||
EndpointOperationType operationType, long timeToLive);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a tech-specific extension of an endpoint.
|
||||
* @param <T> the type of the operation
|
||||
*/
|
||||
private static final class EndpointExtensionInfo<T extends EndpointOperation> {
|
||||
|
||||
private final Class<?> endpointExtensionType;
|
||||
|
||||
private final Collection<T> operations;
|
||||
|
||||
private EndpointExtensionInfo(Class<?> endpointExtensionType,
|
||||
Collection<T> operations) {
|
||||
this.endpointExtensionType = endpointExtensionType;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
private Class<?> getEndpointExtensionType() {
|
||||
return this.endpointExtensionType;
|
||||
}
|
||||
|
||||
private Collection<T> getOperations() {
|
||||
return this.operations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an {@link EndpointInfo endpoint} and whether or not it is valid.
|
||||
*
|
||||
* @param <T> the type of the operation
|
||||
* @param <K> the type of the operation key
|
||||
*/
|
||||
protected static class EndpointInfoDescriptor<T extends EndpointOperation, K> {
|
||||
|
||||
private final EndpointInfo<T> endpointInfo;
|
||||
|
||||
private final Map<OperationKey<K>, List<T>> operations;
|
||||
|
||||
protected EndpointInfoDescriptor(EndpointInfo<T> endpointInfo,
|
||||
Map<OperationKey<K>, List<T>> operations) {
|
||||
this.endpointInfo = endpointInfo;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
public EndpointInfo<T> getEndpointInfo() {
|
||||
return this.endpointInfo;
|
||||
}
|
||||
|
||||
public Map<OperationKey<K>, List<T>> findDuplicateOperations() {
|
||||
Map<OperationKey<K>, List<T>> duplicateOperations = new HashMap<>();
|
||||
this.operations.forEach((k, list) -> {
|
||||
if (list.size() > 1) {
|
||||
duplicateOperations.put(k, list);
|
||||
}
|
||||
});
|
||||
return duplicateOperations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the key of an operation in the context of an operation's implementation.
|
||||
*
|
||||
* @param <K> the type of the key
|
||||
*/
|
||||
protected static final class OperationKey<K> {
|
||||
|
||||
private final String endpointId;
|
||||
|
||||
private final Class<?> endpointType;
|
||||
|
||||
private final K key;
|
||||
|
||||
public OperationKey(String endpointId, Class<?> endpointType, K key) {
|
||||
this.endpointId = endpointId;
|
||||
this.endpointType = endpointType;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
OperationKey<?> that = (OperationKey<?>) o;
|
||||
|
||||
if (!this.endpointId.equals(that.endpointId)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.endpointType.equals(that.endpointType)) {
|
||||
return false;
|
||||
}
|
||||
return this.key.equals(that.key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.endpointId.hashCode();
|
||||
result = 31 * result + this.endpointType.hashCode();
|
||||
result = 31 * result + this.key.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* The caching configuration of an endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class CachingConfiguration {
|
||||
|
||||
private final long timeToLive;
|
||||
|
||||
/**
|
||||
* Create a new instance with the given {@code timeToLive}.
|
||||
* @param timeToLive the time to live of an operation result in milliseconds
|
||||
*/
|
||||
public CachingConfiguration(long timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time to live of a cached operation result.
|
||||
* @return the time to live of an operation result
|
||||
*/
|
||||
public long getTimeToLive() {
|
||||
return this.timeToLive;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link OperationInvoker} that caches the response of an operation with a
|
||||
* configurable time to live.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class CachingOperationInvoker implements OperationInvoker {
|
||||
|
||||
private final OperationInvoker target;
|
||||
|
||||
private final long timeToLive;
|
||||
|
||||
private volatile CachedResponse cachedResponse;
|
||||
|
||||
/**
|
||||
* Create a new instance with the target {@link OperationInvoker} to use to compute
|
||||
* the response and the time to live for the cache.
|
||||
* @param target the {@link OperationInvoker} this instance wraps
|
||||
* @param timeToLive the maximum time in milliseconds that a response can be cached
|
||||
*/
|
||||
public CachingOperationInvoker(OperationInvoker target, long timeToLive) {
|
||||
Assert.state(timeToLive > 0, "TimeToLive must be strictly positive");
|
||||
this.target = target;
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum time in milliseconds that a response can be cached.
|
||||
* @return the time to live of a response
|
||||
*/
|
||||
public long getTimeToLive() {
|
||||
return this.timeToLive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Map<String, Object> arguments) {
|
||||
long accessTime = System.currentTimeMillis();
|
||||
CachedResponse cached = this.cachedResponse;
|
||||
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
|
||||
Object response = this.target.invoke(arguments);
|
||||
this.cachedResponse = new CachedResponse(response, accessTime);
|
||||
return response;
|
||||
}
|
||||
return cached.getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* A cached response that encapsulates the response itself and the time at which it
|
||||
* was created.
|
||||
*/
|
||||
static class CachedResponse {
|
||||
|
||||
private final Object response;
|
||||
|
||||
private final long creationTime;
|
||||
|
||||
CachedResponse(Object response, long creationTime) {
|
||||
this.response = response;
|
||||
this.creationTime = creationTime;
|
||||
}
|
||||
|
||||
public boolean isStale(long accessTime, long timeToLive) {
|
||||
return (accessTime - this.creationTime) >= timeToLive;
|
||||
}
|
||||
|
||||
public Object getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
/**
|
||||
* {@link OperationParameterMapper} that uses a {@link ConversionService} to map parameter
|
||||
* values if necessary.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ConversionServiceOperationParameterMapper
|
||||
implements OperationParameterMapper {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
/**
|
||||
* Create a new instance with the {@link ConversionService} to use.
|
||||
* @param conversionService the conversion service
|
||||
*/
|
||||
public ConversionServiceOperationParameterMapper(
|
||||
ConversionService conversionService) {
|
||||
this.conversionService = new BinderConversionService(conversionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T mapParameter(Object input, Class<T> parameterType) {
|
||||
if (input == null || parameterType.isAssignableFrom(input.getClass())) {
|
||||
return parameterType.cast(input);
|
||||
}
|
||||
try {
|
||||
return this.conversionService.convert(input, parameterType);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new ParameterMappingException(input, parameterType, ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Identifies a type as being an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
* @see EndpointDiscoverer
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Endpoint {
|
||||
|
||||
/**
|
||||
* The id of the endpoint.
|
||||
* @return the id
|
||||
*/
|
||||
String id();
|
||||
|
||||
/**
|
||||
* Defines the endpoint {@link EndpointType types} that should be exposed. By default,
|
||||
* all types are exposed.
|
||||
* @return the endpoint types to expose
|
||||
*/
|
||||
EndpointType[] types() default {};
|
||||
|
||||
/**
|
||||
* Whether or not the endpoint is enabled by default.
|
||||
* @return {@code true} if the endpoint is enabled by default, otherwise {@code false}
|
||||
*/
|
||||
boolean enabledByDefault() default true;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Discovers endpoints and provides an {@link EndpointInfo} for each of them.
|
||||
*
|
||||
* @param <T> the type of the operation
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EndpointDiscoverer<T extends EndpointOperation> {
|
||||
|
||||
/**
|
||||
* Perform endpoint discovery.
|
||||
* @return the discovered endpoints
|
||||
*/
|
||||
Collection<EndpointInfo<T>> discoverEndpoints();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Information describing an endpoint.
|
||||
*
|
||||
* @param <T> the type of the endpoint's operations
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class EndpointInfo<T extends EndpointOperation> {
|
||||
|
||||
private final String id;
|
||||
|
||||
private final boolean enabledByDefault;
|
||||
|
||||
private final Collection<T> operations;
|
||||
|
||||
/**
|
||||
* Creates a new {@code EndpointInfo} describing an endpoint with the given {@code id}
|
||||
* and {@code operations}.
|
||||
* @param id the id of the endpoint
|
||||
* @param enabledByDefault whether or not the endpoint is enabled by default
|
||||
* @param operations the operations of the endpoint
|
||||
*/
|
||||
public EndpointInfo(String id, boolean enabledByDefault, Collection<T> operations) {
|
||||
this.id = id;
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the endpoint.
|
||||
* @return the id
|
||||
*/
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this endpoint is enabled by default.
|
||||
* @return {@code true} if it is enabled by default, otherwise {@code false}
|
||||
*/
|
||||
public boolean isEnabledByDefault() {
|
||||
return this.enabledByDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operations of the endpoint.
|
||||
* @return the operations
|
||||
*/
|
||||
public Collection<T> getOperations() {
|
||||
return this.operations;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* An operation on an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class EndpointOperation {
|
||||
|
||||
private final EndpointOperationType type;
|
||||
|
||||
private final OperationInvoker operationInvoker;
|
||||
|
||||
private final boolean blocking;
|
||||
|
||||
/**
|
||||
* Creates a new {@code EndpointOperation} for an operation of the given {@code type}.
|
||||
* The operation can be performed using the given {@code operationInvoker}.
|
||||
* @param type the type of the operation
|
||||
* @param operationInvoker used to perform the operation
|
||||
* @param blocking whether or not this is a blocking operation
|
||||
*/
|
||||
public EndpointOperation(EndpointOperationType type,
|
||||
OperationInvoker operationInvoker, boolean blocking) {
|
||||
this.type = type;
|
||||
this.operationInvoker = operationInvoker;
|
||||
this.blocking = blocking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link EndpointOperationType type} of the operation.
|
||||
* @return the type
|
||||
*/
|
||||
public EndpointOperationType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code OperationInvoker} that can be used to invoke this endpoint
|
||||
* operation.
|
||||
* @return the operation invoker
|
||||
*/
|
||||
public OperationInvoker getOperationInvoker() {
|
||||
return this.operationInvoker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this is a blocking operation.
|
||||
*
|
||||
* @return {@code true} if it is a blocking operation, otherwise {@code false}.
|
||||
*/
|
||||
public boolean isBlocking() {
|
||||
return this.blocking;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* An enumeration of the different types of operation supported by an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public enum EndpointOperationType {
|
||||
|
||||
/**
|
||||
* A read operation.
|
||||
*/
|
||||
READ,
|
||||
|
||||
/**
|
||||
* A write operation.
|
||||
*/
|
||||
WRITE
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* An enumeration of the available {@link Endpoint} types.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public enum EndpointType {
|
||||
|
||||
/**
|
||||
* Expose the endpoint as a JMX MBean.
|
||||
*/
|
||||
JMX,
|
||||
|
||||
/**
|
||||
* Expose the endpoint as a Web endpoint.
|
||||
*/
|
||||
WEB
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An {@code OperationInvoker} is used to invoke an operation on an endpoint.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OperationInvoker {
|
||||
|
||||
/**
|
||||
* Invoke the underlying operation using the given {@code arguments}.
|
||||
* @param arguments the arguments to pass to the operation
|
||||
* @return the result of the operation, may be {@code null}
|
||||
*/
|
||||
Object invoke(Map<String, Object> arguments);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* An {@code OperationParameterMapper} is used to map parameters to the required type when
|
||||
* invoking an endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface OperationParameterMapper {
|
||||
|
||||
/**
|
||||
* Map the specified {@code input} parameter to the given {@code parameterType}.
|
||||
* @param input a parameter value
|
||||
* @param parameterType the required type of the parameter
|
||||
* @return a value suitable for that parameter
|
||||
* @param <T> the actual type of the parameter
|
||||
* @throws ParameterMappingException when a mapping failure occurs
|
||||
*/
|
||||
<T> T mapParameter(Object input, Class<T> parameterType);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* A {@code ParameterMappingException} is thrown when a failure occurs during
|
||||
* {@link OperationParameterMapper#mapParameter(Object, Class) operation parameter
|
||||
* mapping}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ParameterMappingException extends RuntimeException {
|
||||
|
||||
private final Object input;
|
||||
|
||||
private final Class<?> type;
|
||||
|
||||
/**
|
||||
* Creates a new {@code ParameterMappingException} for a failure that occurred when
|
||||
* trying to map the given {@code input} to the given {@code type}.
|
||||
*
|
||||
* @param input the input that was being mapped
|
||||
* @param type the type that was being mapped to
|
||||
* @param cause the cause of the mapping failure
|
||||
*/
|
||||
public ParameterMappingException(Object input, Class<?> type, Throwable cause) {
|
||||
super("Failed to map " + input + " of type " + input.getClass() + " to type "
|
||||
+ type, cause);
|
||||
this.input = input;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the input that was to be mapped.
|
||||
*
|
||||
* @return the input
|
||||
*/
|
||||
public Object getInput() {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type to be mapped to.
|
||||
*
|
||||
* @return the type
|
||||
*/
|
||||
public Class<?> getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Identifies a method on an {@link Endpoint} as being a read operation.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface ReadOperation {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* An {@code OperationInvoker} that invokes an operation using reflection.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class ReflectiveOperationInvoker implements OperationInvoker {
|
||||
|
||||
private final OperationParameterMapper parameterMapper;
|
||||
|
||||
private final Object target;
|
||||
|
||||
private final Method method;
|
||||
|
||||
/**
|
||||
* Creates a new {code ReflectiveOperationInvoker} that will invoke the given
|
||||
* {@code method} on the given {@code target}. The given {@code parameterMapper} will
|
||||
* be used to map parameters to the required types.
|
||||
*
|
||||
* @param parameterMapper the parameter mapper
|
||||
* @param target the target of the reflective call
|
||||
* @param method the method to call
|
||||
*/
|
||||
public ReflectiveOperationInvoker(OperationParameterMapper parameterMapper,
|
||||
Object target, Method method) {
|
||||
this.parameterMapper = parameterMapper;
|
||||
this.target = target;
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Map<String, Object> arguments) {
|
||||
return ReflectionUtils.invokeMethod(this.method, this.target,
|
||||
resolveArguments(arguments));
|
||||
}
|
||||
|
||||
private Object[] resolveArguments(Map<String, Object> arguments) {
|
||||
return Stream.of(this.method.getParameters())
|
||||
.map((parameter) -> resolveArgument(parameter, arguments))
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(),
|
||||
(list) -> list.toArray(new Object[list.size()])));
|
||||
}
|
||||
|
||||
private Object resolveArgument(Parameter parameter, Map<String, Object> arguments) {
|
||||
Object resolved = arguments.get(parameter.getName());
|
||||
return this.parameterMapper.mapParameter(resolved, parameter.getType());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* A {@code Selector} can be used on a parameter of an {@link Endpoint} method to indicate
|
||||
* that the parameter is used to select a subset of the endpoint's data.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Selector {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Identifies a method on an {@link Endpoint} as being a write operation.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface WriteOperation {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.management.Attribute;
|
||||
import javax.management.AttributeList;
|
||||
import javax.management.AttributeNotFoundException;
|
||||
import javax.management.DynamicMBean;
|
||||
import javax.management.InvalidAttributeValueException;
|
||||
import javax.management.MBeanException;
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.ReflectionException;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* A {@link DynamicMBean} that invokes operations on an {@link EndpointInfo endpoint}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
* @see EndpointMBeanInfoAssembler
|
||||
*/
|
||||
public class EndpointMBean implements DynamicMBean {
|
||||
|
||||
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
|
||||
"reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
|
||||
|
||||
private final Function<Object, Object> operationResponseConverter;
|
||||
|
||||
private final EndpointMBeanInfo endpointInfo;
|
||||
|
||||
EndpointMBean(Function<Object, Object> operationResponseConverter,
|
||||
EndpointMBeanInfo endpointInfo) {
|
||||
this.operationResponseConverter = operationResponseConverter;
|
||||
this.endpointInfo = endpointInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the related endpoint.
|
||||
* @return the endpoint id
|
||||
*/
|
||||
public String getEndpointId() {
|
||||
return this.endpointInfo.getEndpointId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MBeanInfo getMBeanInfo() {
|
||||
return this.endpointInfo.getMbeanInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(String actionName, Object[] params, String[] signature)
|
||||
throws MBeanException, ReflectionException {
|
||||
JmxEndpointOperation operationInfo = this.endpointInfo.getOperations()
|
||||
.get(actionName);
|
||||
if (operationInfo != null) {
|
||||
Map<String, Object> arguments = new HashMap<>();
|
||||
List<JmxEndpointOperationParameterInfo> parameters = operationInfo
|
||||
.getParameters();
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
arguments.put(parameters.get(i).getName(), params[i]);
|
||||
}
|
||||
Object result = operationInfo.getOperationInvoker().invoke(arguments);
|
||||
if (REACTOR_PRESENT) {
|
||||
result = ReactiveHandler.handle(result);
|
||||
}
|
||||
return this.operationResponseConverter.apply(result);
|
||||
}
|
||||
throw new ReflectionException(new IllegalArgumentException(
|
||||
String.format("Endpoint with id '%s' has no operation named %s",
|
||||
this.endpointInfo.getEndpointId(), actionName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String attribute)
|
||||
throws AttributeNotFoundException, MBeanException, ReflectionException {
|
||||
throw new AttributeNotFoundException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
|
||||
InvalidAttributeValueException, MBeanException, ReflectionException {
|
||||
throw new AttributeNotFoundException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeList getAttributes(String[] attributes) {
|
||||
return new AttributeList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeList setAttributes(AttributeList attributes) {
|
||||
return new AttributeList();
|
||||
}
|
||||
|
||||
private static class ReactiveHandler {
|
||||
|
||||
public static Object handle(Object result) {
|
||||
if (result instanceof Mono) {
|
||||
return ((Mono<?>) result).block();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.management.MBeanInfo;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointOperation;
|
||||
|
||||
/**
|
||||
* The {@link MBeanInfo} for a particular {@link EndpointInfo endpoint}. Maps operation
|
||||
* names to an {@link EndpointOperation}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public final class EndpointMBeanInfo {
|
||||
|
||||
private final String endpointId;
|
||||
|
||||
private final MBeanInfo mBeanInfo;
|
||||
|
||||
private final Map<String, JmxEndpointOperation> operations;
|
||||
|
||||
public EndpointMBeanInfo(String endpointId, MBeanInfo mBeanInfo,
|
||||
Map<String, JmxEndpointOperation> operations) {
|
||||
this.endpointId = endpointId;
|
||||
this.mBeanInfo = mBeanInfo;
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
public String getEndpointId() {
|
||||
return this.endpointId;
|
||||
}
|
||||
|
||||
public MBeanInfo getMbeanInfo() {
|
||||
return this.mBeanInfo;
|
||||
}
|
||||
|
||||
public Map<String, JmxEndpointOperation> getOperations() {
|
||||
return this.operations;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanParameterInfo;
|
||||
import javax.management.modelmbean.ModelMBeanAttributeInfo;
|
||||
import javax.management.modelmbean.ModelMBeanConstructorInfo;
|
||||
import javax.management.modelmbean.ModelMBeanInfoSupport;
|
||||
import javax.management.modelmbean.ModelMBeanNotificationInfo;
|
||||
import javax.management.modelmbean.ModelMBeanOperationInfo;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointOperationType;
|
||||
|
||||
/**
|
||||
* Gathers the management operations of a particular {@link EndpointInfo endpoint}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class EndpointMBeanInfoAssembler {
|
||||
|
||||
private final JmxOperationResponseMapper responseMapper;
|
||||
|
||||
EndpointMBeanInfoAssembler(JmxOperationResponseMapper responseMapper) {
|
||||
this.responseMapper = responseMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link EndpointMBeanInfo} for the specified {@link EndpointInfo
|
||||
* endpoint}.
|
||||
* @param endpointInfo the endpoint to handle
|
||||
* @return the mbean info for the endpoint
|
||||
*/
|
||||
EndpointMBeanInfo createEndpointMBeanInfo(
|
||||
EndpointInfo<JmxEndpointOperation> endpointInfo) {
|
||||
Map<String, OperationInfos> operationsMapping = getOperationInfo(endpointInfo);
|
||||
ModelMBeanOperationInfo[] operationsMBeanInfo = operationsMapping.values()
|
||||
.stream().map((t) -> t.mBeanOperationInfo).collect(Collectors.toList())
|
||||
.toArray(new ModelMBeanOperationInfo[] {});
|
||||
Map<String, JmxEndpointOperation> operationsInfo = new LinkedHashMap<>();
|
||||
operationsMapping.forEach((name, t) -> operationsInfo.put(name, t.operation));
|
||||
|
||||
MBeanInfo info = new ModelMBeanInfoSupport(EndpointMBean.class.getName(),
|
||||
getDescription(endpointInfo), new ModelMBeanAttributeInfo[0],
|
||||
new ModelMBeanConstructorInfo[0], operationsMBeanInfo,
|
||||
new ModelMBeanNotificationInfo[0]);
|
||||
return new EndpointMBeanInfo(endpointInfo.getId(), info, operationsInfo);
|
||||
}
|
||||
|
||||
private String getDescription(EndpointInfo<?> endpointInfo) {
|
||||
return "MBean operations for endpoint " + endpointInfo.getId();
|
||||
}
|
||||
|
||||
private Map<String, OperationInfos> getOperationInfo(
|
||||
EndpointInfo<JmxEndpointOperation> endpointInfo) {
|
||||
Map<String, OperationInfos> operationInfos = new HashMap<>();
|
||||
endpointInfo.getOperations().forEach((operationInfo) -> {
|
||||
String name = operationInfo.getOperationName();
|
||||
ModelMBeanOperationInfo mBeanOperationInfo = new ModelMBeanOperationInfo(
|
||||
operationInfo.getOperationName(), operationInfo.getDescription(),
|
||||
getMBeanParameterInfos(operationInfo), this.responseMapper
|
||||
.mapResponseType(operationInfo.getOutputType()).getName(),
|
||||
mapOperationType(operationInfo.getType()));
|
||||
operationInfos.put(name,
|
||||
new OperationInfos(mBeanOperationInfo, operationInfo));
|
||||
});
|
||||
return operationInfos;
|
||||
}
|
||||
|
||||
private MBeanParameterInfo[] getMBeanParameterInfos(JmxEndpointOperation operation) {
|
||||
return operation.getParameters().stream()
|
||||
.map((operationParameter) -> new MBeanParameterInfo(
|
||||
operationParameter.getName(),
|
||||
operationParameter.getType().getName(),
|
||||
operationParameter.getDescription()))
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(),
|
||||
(parameterInfos) -> parameterInfos
|
||||
.toArray(new MBeanParameterInfo[parameterInfos.size()])));
|
||||
}
|
||||
|
||||
private int mapOperationType(EndpointOperationType type) {
|
||||
if (type == EndpointOperationType.READ) {
|
||||
return MBeanOperationInfo.INFO;
|
||||
}
|
||||
if (type == EndpointOperationType.WRITE) {
|
||||
return MBeanOperationInfo.ACTION;
|
||||
}
|
||||
return MBeanOperationInfo.UNKNOWN;
|
||||
}
|
||||
|
||||
private static class OperationInfos {
|
||||
|
||||
private final ModelMBeanOperationInfo mBeanOperationInfo;
|
||||
|
||||
private final JmxEndpointOperation operation;
|
||||
|
||||
OperationInfos(ModelMBeanOperationInfo mBeanOperationInfo,
|
||||
JmxEndpointOperation operation) {
|
||||
this.mBeanOperationInfo = mBeanOperationInfo;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import javax.management.InstanceNotFoundException;
|
||||
import javax.management.MBeanRegistrationException;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.jmx.JmxException;
|
||||
import org.springframework.jmx.export.MBeanExportException;
|
||||
import org.springframework.jmx.export.MBeanExporter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* JMX Registrar for {@link EndpointMBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
* @see EndpointObjectNameFactory
|
||||
*/
|
||||
public class EndpointMBeanRegistrar {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(EndpointMBeanRegistrar.class);
|
||||
|
||||
private final MBeanServer mBeanServer;
|
||||
|
||||
private final EndpointObjectNameFactory objectNameFactory;
|
||||
|
||||
/**
|
||||
* Create a new instance with the {@link MBeanExporter} and
|
||||
* {@link EndpointObjectNameFactory} to use.
|
||||
* @param mBeanServer the mbean exporter
|
||||
* @param objectNameFactory the {@link ObjectName} factory
|
||||
*/
|
||||
public EndpointMBeanRegistrar(MBeanServer mBeanServer,
|
||||
EndpointObjectNameFactory objectNameFactory) {
|
||||
Assert.notNull(mBeanServer, "MBeanServer must not be null");
|
||||
Assert.notNull(objectNameFactory, "ObjectNameFactory must not be null");
|
||||
this.mBeanServer = mBeanServer;
|
||||
this.objectNameFactory = objectNameFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the specified {@link EndpointMBean} and return its {@link ObjectName}.
|
||||
* @param endpoint the endpoint to register
|
||||
* @return the {@link ObjectName} used to register the {@code endpoint}
|
||||
*/
|
||||
public ObjectName registerEndpointMBean(EndpointMBean endpoint) {
|
||||
Assert.notNull(endpoint, "Endpoint must not be null");
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format(
|
||||
"Registering endpoint with id '%s' to " + "the JMX domain",
|
||||
endpoint.getEndpointId()));
|
||||
}
|
||||
ObjectName objectName = this.objectNameFactory.generate(endpoint);
|
||||
this.mBeanServer.registerMBean(endpoint, objectName);
|
||||
return objectName;
|
||||
}
|
||||
catch (MalformedObjectNameException ex) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Invalid ObjectName for " + "endpoint with id '%s'",
|
||||
endpoint.getEndpointId()),
|
||||
ex);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new MBeanExportException(
|
||||
String.format("Failed to register MBean for endpoint with id '%s'",
|
||||
endpoint.getEndpointId()),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the specified {@link ObjectName} if necessary.
|
||||
* @param objectName the {@link ObjectName} of the endpoint to unregister
|
||||
* @return {@code true} if the endpoint was unregistered, {@code false} if no such
|
||||
* endpoint was found
|
||||
*/
|
||||
public boolean unregisterEndpointMbean(ObjectName objectName) {
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("Unregister endpoint with ObjectName '%s' "
|
||||
+ "from the JMX domain", objectName));
|
||||
}
|
||||
this.mBeanServer.unregisterMBean(objectName);
|
||||
return true;
|
||||
}
|
||||
catch (InstanceNotFoundException ex) {
|
||||
return false;
|
||||
}
|
||||
catch (MBeanRegistrationException ex) {
|
||||
throw new JmxException(
|
||||
String.format("Failed to unregister MBean with" + "ObjectName '%s'",
|
||||
objectName),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
/**
|
||||
* A factory to create an {@link ObjectName} for an {@link EndpointMBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EndpointObjectNameFactory {
|
||||
|
||||
/**
|
||||
* Generate an {@link ObjectName} for the specified {@link EndpointMBean endpoint}.
|
||||
* @param mBean the endpoint to handle
|
||||
* @return the {@link ObjectName} to use for the endpoint
|
||||
* @throws MalformedObjectNameException if the object name is invalid
|
||||
*/
|
||||
ObjectName generate(EndpointMBean mBean) throws MalformedObjectNameException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
|
||||
import org.springframework.jmx.export.metadata.ManagedOperation;
|
||||
import org.springframework.jmx.export.metadata.ManagedOperationParameter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
|
||||
* {@link JmxEndpointExtension JMX extensions} applied to them.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxAnnotationEndpointDiscoverer
|
||||
extends AnnotationEndpointDiscoverer<JmxEndpointOperation, String> {
|
||||
|
||||
private static final AnnotationJmxAttributeSource jmxAttributeSource = new AnnotationJmxAttributeSource();
|
||||
|
||||
/**
|
||||
* Creates a new {@link JmxAnnotationEndpointDiscoverer} that will discover
|
||||
* {@link Endpoint endpoints} and {@link JmxEndpointExtension jmx extensions} using
|
||||
* the given {@link ApplicationContext}.
|
||||
*
|
||||
* @param applicationContext the application context
|
||||
* @param parameterMapper the {@link OperationParameterMapper} used to convert
|
||||
* arguments when an operation is invoked
|
||||
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
|
||||
*/
|
||||
public JmxAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
OperationParameterMapper parameterMapper,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory) {
|
||||
super(applicationContext, new JmxEndpointOperationFactory(parameterMapper),
|
||||
JmxEndpointOperation::getOperationName, cachingConfigurationFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<EndpointInfo<JmxEndpointOperation>> discoverEndpoints() {
|
||||
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors = discoverEndpointsWithExtension(
|
||||
JmxEndpointExtension.class, EndpointType.JMX);
|
||||
verifyThatOperationsHaveDistinctName(endpointDescriptors);
|
||||
return endpointDescriptors.stream().map(EndpointInfoDescriptor::getEndpointInfo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void verifyThatOperationsHaveDistinctName(
|
||||
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors) {
|
||||
List<List<JmxEndpointOperation>> clashes = new ArrayList<>();
|
||||
endpointDescriptors.forEach((descriptor) -> clashes
|
||||
.addAll(descriptor.findDuplicateOperations().values()));
|
||||
if (!clashes.isEmpty()) {
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append(
|
||||
String.format("Found multiple JMX operations with the same name:%n"));
|
||||
clashes.forEach((clash) -> {
|
||||
message.append(" ").append(clash.get(0).getOperationName())
|
||||
.append(String.format(":%n"));
|
||||
clash.forEach((operation) -> message.append(" ")
|
||||
.append(String.format("%s%n", operation)));
|
||||
});
|
||||
throw new IllegalStateException(message.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class JmxEndpointOperationFactory
|
||||
implements EndpointOperationFactory<JmxEndpointOperation> {
|
||||
|
||||
private final OperationParameterMapper parameterMapper;
|
||||
|
||||
JmxEndpointOperationFactory(OperationParameterMapper parameterMapper) {
|
||||
this.parameterMapper = parameterMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JmxEndpointOperation createOperation(String endpointId,
|
||||
AnnotationAttributes operationAttributes, Object target, Method method,
|
||||
EndpointOperationType type, long timeToLive) {
|
||||
String operationName = method.getName();
|
||||
Class<?> outputType = mapParameterType(method.getReturnType());
|
||||
String description = getDescription(method,
|
||||
() -> "Invoke " + operationName + " for endpoint " + endpointId);
|
||||
List<JmxEndpointOperationParameterInfo> parameters = getParameters(method);
|
||||
OperationInvoker invoker = new ReflectiveOperationInvoker(
|
||||
this.parameterMapper, target, method);
|
||||
if (timeToLive > 0) {
|
||||
invoker = new CachingOperationInvoker(invoker, timeToLive);
|
||||
}
|
||||
return new JmxEndpointOperation(type, invoker, operationName, outputType,
|
||||
description, parameters);
|
||||
}
|
||||
|
||||
private String getDescription(Method method, Supplier<String> fallback) {
|
||||
ManagedOperation managedOperation = jmxAttributeSource
|
||||
.getManagedOperation(method);
|
||||
if (managedOperation != null
|
||||
&& StringUtils.hasText(managedOperation.getDescription())) {
|
||||
return managedOperation.getDescription();
|
||||
}
|
||||
return fallback.get();
|
||||
}
|
||||
|
||||
private List<JmxEndpointOperationParameterInfo> getParameters(Method method) {
|
||||
List<JmxEndpointOperationParameterInfo> parameters = new ArrayList<>();
|
||||
Parameter[] methodParameters = method.getParameters();
|
||||
if (methodParameters.length == 0) {
|
||||
return parameters;
|
||||
}
|
||||
ManagedOperationParameter[] managedOperationParameters = jmxAttributeSource
|
||||
.getManagedOperationParameters(method);
|
||||
if (managedOperationParameters.length > 0) {
|
||||
for (int i = 0; i < managedOperationParameters.length; i++) {
|
||||
ManagedOperationParameter mBeanParameter = managedOperationParameters[i];
|
||||
Parameter methodParameter = methodParameters[i];
|
||||
parameters.add(new JmxEndpointOperationParameterInfo(
|
||||
mBeanParameter.getName(),
|
||||
mapParameterType(methodParameter.getType()),
|
||||
mBeanParameter.getDescription()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (Parameter parameter : methodParameters) {
|
||||
parameters.add(
|
||||
new JmxEndpointOperationParameterInfo(parameter.getName(),
|
||||
mapParameterType(parameter.getType()), null));
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private Class<?> mapParameterType(Class<?> parameter) {
|
||||
if (parameter.isEnum()) {
|
||||
return String.class;
|
||||
}
|
||||
if (Date.class.isAssignableFrom(parameter)) {
|
||||
return String.class;
|
||||
}
|
||||
if (parameter.getName().startsWith("java.")) {
|
||||
return parameter;
|
||||
}
|
||||
if (parameter.equals(Void.TYPE)) {
|
||||
return parameter;
|
||||
}
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
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 JMX-specific extension of an {@link Endpoint}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
* @see Endpoint
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface JmxEndpointExtension {
|
||||
|
||||
/**
|
||||
* The {@link Endpoint endpoint} class to which this JMX extension relates.
|
||||
* @return the endpoint class
|
||||
*/
|
||||
Class<?> endpoint();
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
|
||||
/**
|
||||
* A factory for creating JMX MBeans for endpoint operations.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxEndpointMBeanFactory {
|
||||
|
||||
private final EndpointMBeanInfoAssembler assembler;
|
||||
|
||||
private final JmxOperationResponseMapper resultMapper;
|
||||
|
||||
/**
|
||||
* Create a new {@link JmxEndpointMBeanFactory} instance that will use the given
|
||||
* {@code responseMapper} to convert an operation's response to a JMX-friendly form.
|
||||
* @param responseMapper the response mapper
|
||||
*/
|
||||
public JmxEndpointMBeanFactory(JmxOperationResponseMapper responseMapper) {
|
||||
this.assembler = new EndpointMBeanInfoAssembler(responseMapper);
|
||||
this.resultMapper = responseMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates MBeans for the given {@code endpoints}.
|
||||
* @param endpoints the endpoints
|
||||
* @return the MBeans
|
||||
*/
|
||||
public Collection<EndpointMBean> createMBeans(
|
||||
Collection<EndpointInfo<JmxEndpointOperation>> endpoints) {
|
||||
return endpoints.stream().map((endpointInfo) -> {
|
||||
EndpointMBeanInfo endpointMBeanInfo = this.assembler
|
||||
.createEndpointMBeanInfo(endpointInfo);
|
||||
return new EndpointMBean(this.resultMapper::mapResponse, endpointMBeanInfo);
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointOperation;
|
||||
import org.springframework.boot.endpoint.EndpointOperationType;
|
||||
import org.springframework.boot.endpoint.OperationInvoker;
|
||||
|
||||
/**
|
||||
* An operation on a JMX endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxEndpointOperation extends EndpointOperation {
|
||||
|
||||
private final String operationName;
|
||||
|
||||
private final Class<?> outputType;
|
||||
|
||||
private final String description;
|
||||
|
||||
private final List<JmxEndpointOperationParameterInfo> parameters;
|
||||
|
||||
/**
|
||||
* Creates a new {@code JmxEndpointOperation} for an operation of the given
|
||||
* {@code type}. The operation can be performed using the given {@code invoker}.
|
||||
* @param type the type of the operation
|
||||
* @param invoker used to perform the operation
|
||||
* @param operationName the name of the operation
|
||||
* @param outputType the type of the output of the operation
|
||||
* @param description the description of the operation
|
||||
* @param parameters the parameters of the operation
|
||||
*/
|
||||
public JmxEndpointOperation(EndpointOperationType type, OperationInvoker invoker,
|
||||
String operationName, Class<?> outputType, String description,
|
||||
List<JmxEndpointOperationParameterInfo> parameters) {
|
||||
super(type, invoker, true);
|
||||
this.operationName = operationName;
|
||||
this.outputType = outputType;
|
||||
this.description = description;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the operation.
|
||||
* @return the operation name
|
||||
*/
|
||||
public String getOperationName() {
|
||||
return this.operationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the output of the operation.
|
||||
* @return the output type
|
||||
*/
|
||||
public Class<?> getOutputType() {
|
||||
return this.outputType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the operation.
|
||||
* @return the operation description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parameters of the operation.
|
||||
* @return the operation parameters
|
||||
*/
|
||||
public List<JmxEndpointOperationParameterInfo> getParameters() {
|
||||
return Collections.unmodifiableList(this.parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
/**
|
||||
* Describes the parameters of an operation on a JMX endpoint.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class JmxEndpointOperationParameterInfo {
|
||||
|
||||
private final String name;
|
||||
|
||||
private final Class<?> type;
|
||||
|
||||
private final String description;
|
||||
|
||||
public JmxEndpointOperationParameterInfo(String name, Class<?> type,
|
||||
String description) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the description of the parameter or {@code null} if none is available.
|
||||
* @return the description or {@code null}
|
||||
*/
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
/**
|
||||
* A {@code JmxOperationResponseMapper} maps an operation's response to a JMX-friendly
|
||||
* form.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public interface JmxOperationResponseMapper {
|
||||
|
||||
/**
|
||||
* Map the operation's response so that it can be consumed by a JMX compliant client.
|
||||
* @param response the operation's response
|
||||
* @return the {@code response}, in a JMX compliant format
|
||||
*/
|
||||
Object mapResponse(Object response);
|
||||
|
||||
/**
|
||||
* Map the response type to its JMX compliant counterpart.
|
||||
* @param responseType the operation's response type
|
||||
* @return the JMX compliant type
|
||||
*/
|
||||
Class<?> mapResponseType(Class<?> responseType);
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JMX endpoint support.
|
||||
*/
|
||||
package org.springframework.boot.endpoint.jmx;
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Endpoint support.
|
||||
*/
|
||||
package org.springframework.boot.endpoint;
|
||||
|
|
@ -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<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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
* <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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 <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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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<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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Spring MVC web endpoint support.
|
||||
*/
|
||||
package org.springframework.boot.endpoint.web.mvc;
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Web endpoint support.
|
||||
*/
|
||||
package org.springframework.boot.endpoint.web;
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reactive web endpoint support.
|
||||
*/
|
||||
package org.springframework.boot.endpoint.web.reactive;
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link AnnotationEndpointDiscoverer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class AnnotationEndpointDiscovererTests {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void discoverWorksWhenThereAreNoEndpoints() {
|
||||
load(EmptyConfiguration.class,
|
||||
(context) -> assertThat(new TestAnnotationEndpointDiscoverer(context)
|
||||
.discoverEndpoints().isEmpty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointIsDiscovered() {
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(3);
|
||||
assertThat(operations).containsKeys(
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getAll"),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getOne",
|
||||
String.class),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class,
|
||||
String.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subclassedEndpointIsDiscovered() {
|
||||
load(TestEndpointSubclassConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(4);
|
||||
assertThat(operations).containsKeys(
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getAll"),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "getOne",
|
||||
String.class),
|
||||
ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class,
|
||||
String.class),
|
||||
ReflectionUtils.findMethod(TestEndpointSubclass.class,
|
||||
"updateWithMoreArguments", String.class, String.class,
|
||||
String.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoEndpointsHaveTheSameId() {
|
||||
load(ClashingEndpointConfiguration.class, (context) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two endpoints with the id 'test': ");
|
||||
new TestAnnotationEndpointDiscoverer(context).discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsNotCachedWithTtlSetToZero() {
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context,
|
||||
(endpointId) -> new CachingConfiguration(0))
|
||||
.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(3);
|
||||
operations.values()
|
||||
.forEach(operation -> assertThat(operation.getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsNotCachedWithNonMatchingId() {
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory = (
|
||||
endpointId) -> (endpointId.equals("foo") ? new CachingConfiguration(500)
|
||||
: new CachingConfiguration(0));
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context,
|
||||
cachingConfigurationFactory).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
assertThat(operations).hasSize(3);
|
||||
operations.values()
|
||||
.forEach(operation -> assertThat(operation.getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsCachedWithMatchingId() {
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory = (
|
||||
endpointId) -> (endpointId.equals("test") ? new CachingConfiguration(500)
|
||||
: new CachingConfiguration(0));
|
||||
load(TestEndpointConfiguration.class, (context) -> {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpoints = mapEndpoints(
|
||||
new TestAnnotationEndpointDiscoverer(context,
|
||||
cachingConfigurationFactory).discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> operations = mapOperations(
|
||||
endpoints.get("test"));
|
||||
OperationInvoker getAllOperationInvoker = operations
|
||||
.get(ReflectionUtils.findMethod(TestEndpoint.class, "getAll"))
|
||||
.getOperationInvoker();
|
||||
assertThat(getAllOperationInvoker)
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(((CachingOperationInvoker) getAllOperationInvoker).getTimeToLive())
|
||||
.isEqualTo(500);
|
||||
assertThat(operations.get(ReflectionUtils.findMethod(TestEndpoint.class,
|
||||
"getOne", String.class)).getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(operations.get(ReflectionUtils.findMethod(TestEndpoint.class,
|
||||
"update", String.class, String.class)).getOperationInvoker())
|
||||
.isNotInstanceOf(CachingOperationInvoker.class);
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, EndpointInfo<TestEndpointOperation>> mapEndpoints(
|
||||
Collection<EndpointInfo<TestEndpointOperation>> endpoints) {
|
||||
Map<String, EndpointInfo<TestEndpointOperation>> endpointById = new LinkedHashMap<>();
|
||||
endpoints.forEach((endpoint) -> {
|
||||
EndpointInfo<TestEndpointOperation> existing = endpointById
|
||||
.put(endpoint.getId(), endpoint);
|
||||
if (existing != null) {
|
||||
throw new AssertionError(String.format(
|
||||
"Found endpoints with duplicate id '%s'", endpoint.getId()));
|
||||
}
|
||||
});
|
||||
return endpointById;
|
||||
}
|
||||
|
||||
private Map<Method, TestEndpointOperation> mapOperations(
|
||||
EndpointInfo<TestEndpointOperation> endpoint) {
|
||||
Map<Method, TestEndpointOperation> operationByMethod = new HashMap<>();
|
||||
endpoint.getOperations().forEach((operation) -> {
|
||||
EndpointOperation existing = operationByMethod
|
||||
.put(operation.getOperationMethod(), operation);
|
||||
if (existing != null) {
|
||||
throw new AssertionError(String.format(
|
||||
"Found endpoint with duplicate operation method '%s'",
|
||||
operation.getOperationMethod()));
|
||||
}
|
||||
});
|
||||
return operationByMethod;
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Consumer<AnnotationConfigApplicationContext> consumer) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configuration);
|
||||
try {
|
||||
consumer.accept(context);
|
||||
}
|
||||
finally {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
static class TestEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getOne(@Selector String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void update(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
public void someOtherMethod() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestEndpointSubclass extends TestEndpoint {
|
||||
|
||||
@WriteOperation
|
||||
public void updateWithMoreArguments(String foo, String bar, String baz) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestEndpointSubclassConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpointSubclass testEndpointSubclass() {
|
||||
return new TestEndpointSubclass();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointTwo() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointOne() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TestEndpointOperation extends EndpointOperation {
|
||||
|
||||
private final Method operationMethod;
|
||||
|
||||
private TestEndpointOperation(EndpointOperationType type,
|
||||
OperationInvoker operationInvoker, Method operationMethod) {
|
||||
super(type, operationInvoker, true);
|
||||
this.operationMethod = operationMethod;
|
||||
}
|
||||
|
||||
private Method getOperationMethod() {
|
||||
return this.operationMethod;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestAnnotationEndpointDiscoverer
|
||||
extends AnnotationEndpointDiscoverer<TestEndpointOperation, Method> {
|
||||
|
||||
TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory) {
|
||||
super(applicationContext, endpointOperationFactory(),
|
||||
TestEndpointOperation::getOperationMethod,
|
||||
cachingConfigurationFactory);
|
||||
}
|
||||
|
||||
TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext) {
|
||||
this(applicationContext, (id) -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<EndpointInfo<TestEndpointOperation>> discoverEndpoints() {
|
||||
return discoverEndpointsWithExtension(null, null).stream()
|
||||
.map(EndpointInfoDescriptor::getEndpointInfo)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static EndpointOperationFactory<TestEndpointOperation> endpointOperationFactory() {
|
||||
return new EndpointOperationFactory<TestEndpointOperation>() {
|
||||
|
||||
@Override
|
||||
public TestEndpointOperation createOperation(String endpointId,
|
||||
AnnotationAttributes operationAttributes, Object target,
|
||||
Method operationMethod, EndpointOperationType operationType,
|
||||
long timeToLive) {
|
||||
return new TestEndpointOperation(operationType,
|
||||
createOperationInvoker(timeToLive), operationMethod);
|
||||
}
|
||||
|
||||
private OperationInvoker createOperationInvoker(long timeToLive) {
|
||||
OperationInvoker invoker = (arguments) -> null;
|
||||
if (timeToLive > 0) {
|
||||
return new CachingOperationInvoker(invoker, timeToLive);
|
||||
}
|
||||
else {
|
||||
return invoker;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link CachingOperationInvoker}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class CachingOperationInvokerTests {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void createInstanceWithTllSetToZero() {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("TimeToLive");
|
||||
new CachingOperationInvoker(mock(OperationInvoker.class), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheInTtlRange() {
|
||||
Object expected = new Object();
|
||||
OperationInvoker target = mock(OperationInvoker.class);
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
given(target.invoke(parameters)).willReturn(expected);
|
||||
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
|
||||
Object response = invoker.invoke(parameters);
|
||||
assertThat(response).isSameAs(expected);
|
||||
verify(target, times(1)).invoke(parameters);
|
||||
Object cachedResponse = invoker.invoke(parameters);
|
||||
assertThat(cachedResponse).isSameAs(response);
|
||||
verifyNoMoreInteractions(target);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void targetInvokedWhenCacheExpires() throws InterruptedException {
|
||||
OperationInvoker target = mock(OperationInvoker.class);
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
given(target.invoke(parameters)).willReturn(new Object());
|
||||
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 50L);
|
||||
invoker.invoke(parameters);
|
||||
Thread.sleep(55);
|
||||
invoker.invoke(parameters);
|
||||
verify(target, times(2)).invoke(parameters);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanParameterInfo;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointOperationType;
|
||||
import org.springframework.boot.endpoint.OperationInvoker;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
||||
/**
|
||||
* Tests for {@link EndpointMBeanInfoAssembler}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class EndpointMBeanInfoAssemblerTests {
|
||||
|
||||
private final EndpointMBeanInfoAssembler mBeanInfoAssembler = new EndpointMBeanInfoAssembler(
|
||||
new DummyOperationResponseMapper());
|
||||
|
||||
@Test
|
||||
public void exposeSimpleReadOperation() {
|
||||
JmxEndpointOperation operation = new JmxEndpointOperation(
|
||||
EndpointOperationType.READ, new DummyOperationInvoker(), "getAll",
|
||||
Object.class, "Test operation", Collections.emptyList());
|
||||
EndpointInfo<JmxEndpointOperation> endpoint = new EndpointInfo<>("test", true,
|
||||
Collections.singletonList(operation));
|
||||
EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler
|
||||
.createEndpointMBeanInfo(endpoint);
|
||||
assertThat(endpointMBeanInfo).isNotNull();
|
||||
assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("test");
|
||||
assertThat(endpointMBeanInfo.getOperations())
|
||||
.containsOnly(entry("getAll", operation));
|
||||
MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo();
|
||||
assertThat(mbeanInfo).isNotNull();
|
||||
assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName());
|
||||
assertThat(mbeanInfo.getDescription())
|
||||
.isEqualTo("MBean operations for endpoint test");
|
||||
assertThat(mbeanInfo.getAttributes()).isEmpty();
|
||||
assertThat(mbeanInfo.getNotifications()).isEmpty();
|
||||
assertThat(mbeanInfo.getConstructors()).isEmpty();
|
||||
assertThat(mbeanInfo.getOperations()).hasSize(1);
|
||||
MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0];
|
||||
assertThat(mBeanOperationInfo.getName()).isEqualTo("getAll");
|
||||
assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName());
|
||||
assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.INFO);
|
||||
assertThat(mBeanOperationInfo.getSignature()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exposeSimpleWriteOperation() {
|
||||
JmxEndpointOperation operation = new JmxEndpointOperation(
|
||||
EndpointOperationType.WRITE, new DummyOperationInvoker(), "update",
|
||||
Object.class, "Update operation",
|
||||
Collections.singletonList(new JmxEndpointOperationParameterInfo("test",
|
||||
String.class, "Test argument")));
|
||||
EndpointInfo<JmxEndpointOperation> endpoint = new EndpointInfo<>("another", true,
|
||||
Collections.singletonList(operation));
|
||||
EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler
|
||||
.createEndpointMBeanInfo(endpoint);
|
||||
assertThat(endpointMBeanInfo).isNotNull();
|
||||
assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("another");
|
||||
assertThat(endpointMBeanInfo.getOperations())
|
||||
.containsOnly(entry("update", operation));
|
||||
MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo();
|
||||
assertThat(mbeanInfo).isNotNull();
|
||||
assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName());
|
||||
assertThat(mbeanInfo.getDescription())
|
||||
.isEqualTo("MBean operations for endpoint another");
|
||||
assertThat(mbeanInfo.getAttributes()).isEmpty();
|
||||
assertThat(mbeanInfo.getNotifications()).isEmpty();
|
||||
assertThat(mbeanInfo.getConstructors()).isEmpty();
|
||||
assertThat(mbeanInfo.getOperations()).hasSize(1);
|
||||
MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0];
|
||||
assertThat(mBeanOperationInfo.getName()).isEqualTo("update");
|
||||
assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName());
|
||||
assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.ACTION);
|
||||
assertThat(mBeanOperationInfo.getSignature()).hasSize(1);
|
||||
MBeanParameterInfo mBeanParameterInfo = mBeanOperationInfo.getSignature()[0];
|
||||
assertThat(mBeanParameterInfo.getName()).isEqualTo("test");
|
||||
assertThat(mBeanParameterInfo.getType()).isEqualTo(String.class.getName());
|
||||
assertThat(mBeanParameterInfo.getDescription()).isEqualTo("Test argument");
|
||||
}
|
||||
|
||||
private static class DummyOperationInvoker implements OperationInvoker {
|
||||
|
||||
@Override
|
||||
public Object invoke(Map<String, Object> arguments) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class DummyOperationResponseMapper
|
||||
implements JmxOperationResponseMapper {
|
||||
|
||||
@Override
|
||||
public Object mapResponse(Object response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> mapResponseType(Class<?> responseType) {
|
||||
return responseType;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import javax.management.InstanceNotFoundException;
|
||||
import javax.management.MBeanRegistrationException;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.jmx.JmxException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link EndpointMBeanRegistrar}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class EndpointMBeanRegistrarTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private MBeanServer mBeanServer = mock(MBeanServer.class);
|
||||
|
||||
@Test
|
||||
public void mBeanServerMustNotBeNull() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
new EndpointMBeanRegistrar(null, (e) -> new ObjectName("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectNameFactoryMustNotBeNull() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
new EndpointMBeanRegistrar(this.mBeanServer, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMustNotBeNull() {
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
(e) -> new ObjectName("foo"));
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Endpoint must not be null");
|
||||
registrar.registerEndpointMBean(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerEndpointInvokesObjectNameFactory()
|
||||
throws MalformedObjectNameException {
|
||||
EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class);
|
||||
EndpointMBean endpointMBean = mock(EndpointMBean.class);
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
given(factory.generate(endpointMBean)).willReturn(objectName);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
factory);
|
||||
ObjectName actualObjectName = registrar.registerEndpointMBean(endpointMBean);
|
||||
assertThat(actualObjectName).isSameAs(objectName);
|
||||
verify(factory).generate(endpointMBean);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerEndpointInvalidObjectName() throws MalformedObjectNameException {
|
||||
EndpointMBean endpointMBean = mock(EndpointMBean.class);
|
||||
given(endpointMBean.getEndpointId()).willReturn("test");
|
||||
EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class);
|
||||
given(factory.generate(endpointMBean))
|
||||
.willThrow(new MalformedObjectNameException());
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
factory);
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid ObjectName for endpoint with id 'test'");
|
||||
registrar.registerEndpointMBean(endpointMBean);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerEndpointFailure() throws Exception {
|
||||
EndpointMBean endpointMBean = mock(EndpointMBean.class);
|
||||
given(endpointMBean.getEndpointId()).willReturn("test");
|
||||
EndpointObjectNameFactory factory = mock(EndpointObjectNameFactory.class);
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
given(factory.generate(endpointMBean)).willReturn(objectName);
|
||||
given(this.mBeanServer.registerMBean(endpointMBean, objectName))
|
||||
.willThrow(MBeanRegistrationException.class);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
factory);
|
||||
this.thrown.expect(JmxException.class);
|
||||
this.thrown.expectMessage("Failed to register MBean for endpoint with id 'test'");
|
||||
registrar.registerEndpointMBean(endpointMBean);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisterEndpoint() throws Exception {
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
mock(EndpointObjectNameFactory.class));
|
||||
assertThat(registrar.unregisterEndpointMbean(objectName)).isTrue();
|
||||
verify(this.mBeanServer).unregisterMBean(objectName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unregisterUnknownEndpoint() throws Exception {
|
||||
ObjectName objectName = mock(ObjectName.class);
|
||||
willThrow(InstanceNotFoundException.class).given(this.mBeanServer)
|
||||
.unregisterMBean(objectName);
|
||||
EndpointMBeanRegistrar registrar = new EndpointMBeanRegistrar(this.mBeanServer,
|
||||
mock(EndpointObjectNameFactory.class));
|
||||
assertThat(registrar.unregisterEndpointMbean(objectName)).isFalse();
|
||||
verify(this.mBeanServer).unregisterMBean(objectName);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.management.Attribute;
|
||||
import javax.management.AttributeList;
|
||||
import javax.management.AttributeNotFoundException;
|
||||
import javax.management.InstanceNotFoundException;
|
||||
import javax.management.MBeanException;
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanParameterInfo;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MBeanServerFactory;
|
||||
import javax.management.ObjectName;
|
||||
import javax.management.ReflectionException;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.boot.endpoint.CachingConfiguration;
|
||||
import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
import org.springframework.boot.endpoint.ReadOperation;
|
||||
import org.springframework.boot.endpoint.WriteOperation;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link EndpointMBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class EndpointMBeanTests {
|
||||
|
||||
private final JmxEndpointMBeanFactory jmxEndpointMBeanFactory = new JmxEndpointMBeanFactory(
|
||||
new TestJmxOperationResponseMapper());
|
||||
|
||||
private MBeanServer server;
|
||||
|
||||
private EndpointMBeanRegistrar endpointMBeanRegistrar;
|
||||
|
||||
private EndpointObjectNameFactory objectNameFactory = (endpoint) -> new ObjectName(
|
||||
String.format("org.springframework.boot.test:type=Endpoint,name=%s",
|
||||
UUID.randomUUID().toString()));
|
||||
|
||||
@Before
|
||||
public void createMBeanServer() {
|
||||
this.server = MBeanServerFactory.createMBeanServer();
|
||||
this.endpointMBeanRegistrar = new EndpointMBeanRegistrar(this.server,
|
||||
this.objectNameFactory);
|
||||
}
|
||||
|
||||
@After
|
||||
public void disposeMBeanServer() {
|
||||
if (this.server != null) {
|
||||
MBeanServerFactory.releaseMBeanServer(this.server);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeSimpleEndpoint() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
// getAll
|
||||
Object allResponse = this.server.invoke(objectName, "getAll",
|
||||
new Object[0], new String[0]);
|
||||
assertThat(allResponse).isEqualTo("[ONE, TWO]");
|
||||
|
||||
// getOne
|
||||
Object oneResponse = this.server.invoke(objectName, "getOne",
|
||||
new Object[] { "one" }, new String[] { String.class.getName() });
|
||||
assertThat(oneResponse).isEqualTo("ONE");
|
||||
|
||||
// update
|
||||
Object updateResponse = this.server.invoke(objectName, "update",
|
||||
new Object[] { "one", "1" },
|
||||
new String[] { String.class.getName(), String.class.getName() });
|
||||
assertThat(updateResponse).isNull();
|
||||
|
||||
// getOne validation after update
|
||||
Object updatedOneResponse = this.server.invoke(objectName, "getOne",
|
||||
new Object[] { "one" }, new String[] { String.class.getName() });
|
||||
assertThat(updatedOneResponse).isEqualTo("1");
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke method on FooEndpoint", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxTypesAreProperlyMapped() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
MBeanInfo mBeanInfo = this.server.getMBeanInfo(objectName);
|
||||
Map<String, MBeanOperationInfo> operations = mapOperations(mBeanInfo);
|
||||
assertThat(operations).containsOnlyKeys("getAll", "getOne", "update");
|
||||
assertOperation(operations.get("getAll"), String.class,
|
||||
MBeanOperationInfo.INFO, new Class<?>[0]);
|
||||
assertOperation(operations.get("getOne"), String.class,
|
||||
MBeanOperationInfo.INFO, new Class<?>[] { String.class });
|
||||
assertOperation(operations.get("update"), Void.TYPE,
|
||||
MBeanOperationInfo.ACTION,
|
||||
new Class<?>[] { String.class, String.class });
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to retrieve MBeanInfo of FooEndpoint",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void assertOperation(MBeanOperationInfo operation, Class<?> returnType,
|
||||
int impact, Class<?>[] types) {
|
||||
assertThat(operation.getReturnType()).isEqualTo(returnType.getName());
|
||||
assertThat(operation.getImpact()).isEqualTo(impact);
|
||||
MBeanParameterInfo[] signature = operation.getSignature();
|
||||
assertThat(signature).hasSize(types.length);
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
assertThat(signature[i].getType()).isEqualTo(types[0].getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeReactiveOperation() {
|
||||
load(ReactiveEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "reactive");
|
||||
try {
|
||||
Object allResponse = this.server.invoke(objectName, "getInfo",
|
||||
new Object[0], new String[0]);
|
||||
assertThat(allResponse).isInstanceOf(String.class);
|
||||
assertThat(allResponse).isEqualTo("HELLO WORLD");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke getInfo method", ex);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeUnknownOperation() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
this.server.invoke(objectName, "doesNotExist", new Object[0],
|
||||
new String[0]);
|
||||
throw new AssertionError(
|
||||
"Should have failed to invoke unknown operation");
|
||||
}
|
||||
catch (ReflectionException ex) {
|
||||
assertThat(ex.getCause()).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(ex.getCause().getMessage()).contains("doesNotExist", "foo");
|
||||
}
|
||||
catch (MBeanException | InstanceNotFoundException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotReadAttribute() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
this.server.getAttribute(objectName, "foo");
|
||||
throw new AssertionError("Should have failed to read attribute foo");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
assertThat(ex).isInstanceOf(AttributeNotFoundException.class);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotWriteAttribute() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
this.server.setAttribute(objectName, new Attribute("foo", "bar"));
|
||||
throw new AssertionError("Should have failed to write attribute foo");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
assertThat(ex).isInstanceOf(AttributeNotFoundException.class);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotReadAttributes() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
AttributeList attributes = this.server.getAttributes(objectName,
|
||||
new String[] { "foo", "bar" });
|
||||
assertThat(attributes).isNotNull();
|
||||
assertThat(attributes).isEmpty();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke getAttributes", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dynamicMBeanCannotWriteAttributes() {
|
||||
load(FooEndpoint.class, (discoverer) -> {
|
||||
ObjectName objectName = registerEndpoint(discoverer, "foo");
|
||||
try {
|
||||
AttributeList attributes = new AttributeList();
|
||||
attributes.add(new Attribute("foo", 1));
|
||||
attributes.add(new Attribute("bar", 42));
|
||||
AttributeList attributesSet = this.server.setAttributes(objectName,
|
||||
attributes);
|
||||
assertThat(attributesSet).isNotNull();
|
||||
assertThat(attributesSet).isEmpty();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new AssertionError("Failed to invoke setAttributes", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ObjectName registerEndpoint(JmxAnnotationEndpointDiscoverer discoverer,
|
||||
String endpointId) {
|
||||
Collection<EndpointMBean> mBeans = this.jmxEndpointMBeanFactory
|
||||
.createMBeans(discoverer.discoverEndpoints());
|
||||
assertThat(mBeans).hasSize(1);
|
||||
EndpointMBean endpointMBean = mBeans.iterator().next();
|
||||
assertThat(endpointMBean.getEndpointId()).isEqualTo(endpointId);
|
||||
return this.endpointMBeanRegistrar.registerEndpointMBean(endpointMBean);
|
||||
}
|
||||
|
||||
private Map<String, MBeanOperationInfo> mapOperations(MBeanInfo info) {
|
||||
Map<String, MBeanOperationInfo> operations = new HashMap<>();
|
||||
for (MBeanOperationInfo mBeanOperationInfo : info.getOperations()) {
|
||||
operations.put(mBeanOperationInfo.getName(), mBeanOperationInfo);
|
||||
}
|
||||
return operations;
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Consumer<JmxAnnotationEndpointDiscoverer> consumer) {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configuration)) {
|
||||
consumer.accept(new JmxAnnotationEndpointDiscoverer(context,
|
||||
new ConversionServiceOperationParameterMapper(
|
||||
DefaultConversionService.getSharedInstance()),
|
||||
(id) -> new CachingConfiguration(0)));
|
||||
}
|
||||
}
|
||||
|
||||
@Endpoint(id = "foo")
|
||||
static class FooEndpoint {
|
||||
|
||||
private final Map<FooName, Foo> all = new LinkedHashMap<>();
|
||||
|
||||
FooEndpoint() {
|
||||
this.all.put(FooName.ONE, new Foo("one"));
|
||||
this.all.put(FooName.TWO, new Foo("two"));
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Collection<Foo> getAll() {
|
||||
return this.all.values();
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Foo getOne(FooName name) {
|
||||
return this.all.get(name);
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void update(FooName name, String value) {
|
||||
this.all.put(name, new Foo(value));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "reactive")
|
||||
static class ReactiveEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Mono<String> getInfo() {
|
||||
return Mono.defer(() -> Mono.just("Hello World"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum FooName {
|
||||
|
||||
ONE, TWO, THREE
|
||||
|
||||
}
|
||||
|
||||
static class Foo {
|
||||
|
||||
private final String name;
|
||||
|
||||
Foo(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestJmxOperationResponseMapper
|
||||
implements JmxOperationResponseMapper {
|
||||
|
||||
@Override
|
||||
public Object mapResponse(Object response) {
|
||||
return (response != null ? response.toString().toUpperCase() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> mapResponseType(Class<?> responseType) {
|
||||
if (responseType == Void.TYPE) {
|
||||
return Void.TYPE;
|
||||
}
|
||||
return String.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,555 @@
|
|||
/*
|
||||
* 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.jmx;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.boot.endpoint.CachingConfiguration;
|
||||
import org.springframework.boot.endpoint.CachingOperationInvoker;
|
||||
import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointType;
|
||||
import org.springframework.boot.endpoint.ReadOperation;
|
||||
import org.springframework.boot.endpoint.ReflectiveOperationInvoker;
|
||||
import org.springframework.boot.endpoint.WriteOperation;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JmxAnnotationEndpointDiscoverer}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class JmxAnnotationEndpointDiscovererTests {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void discoveryWorksWhenThereAreNoEndpoints() {
|
||||
load(EmptyConfiguration.class,
|
||||
(discoverer) -> assertThat(discoverer.discoverEndpoints()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void standardEndpointIsDiscovered() {
|
||||
load(TestEndpoint.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getDescription())
|
||||
.isEqualTo("Invoke getAll for endpoint test");
|
||||
assertThat(getAll.getOutputType()).isEqualTo(Object.class);
|
||||
assertThat(getAll.getParameters()).isEmpty();
|
||||
assertThat(getAll.getOperationInvoker())
|
||||
.isInstanceOf(ReflectiveOperationInvoker.class);
|
||||
JmxEndpointOperation getSomething = operationByName.get("getSomething");
|
||||
assertThat(getSomething.getDescription())
|
||||
.isEqualTo("Invoke getSomething for endpoint test");
|
||||
assertThat(getSomething.getOutputType()).isEqualTo(String.class);
|
||||
assertThat(getSomething.getParameters()).hasSize(1);
|
||||
hasDefaultParameter(getSomething, 0, String.class);
|
||||
JmxEndpointOperation update = operationByName.get("update");
|
||||
assertThat(update.getDescription())
|
||||
.isEqualTo("Invoke update for endpoint test");
|
||||
assertThat(update.getOutputType()).isEqualTo(Void.TYPE);
|
||||
assertThat(update.getParameters()).hasSize(2);
|
||||
hasDefaultParameter(update, 0, String.class);
|
||||
hasDefaultParameter(update, 1, String.class);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyJmxEndpointsAreDiscovered() {
|
||||
load(MultipleEndpointsConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test", "jmx");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxExtensionMustHaveEndpoint() {
|
||||
load(TestJmxEndpointExtension.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid extension");
|
||||
this.thrown.expectMessage(TestJmxEndpointExtension.class.getName());
|
||||
this.thrown.expectMessage("no endpoint found");
|
||||
this.thrown.expectMessage(TestEndpoint.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxEndpointOverridesStandardEndpoint() {
|
||||
load(OverriddenOperationJmxEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
assertJmxTestEndpoint(endpoints.get("test"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jmxEndpointAddsExtraOperation() {
|
||||
load(AdditionalOperationJmxEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update", "getAnother");
|
||||
JmxEndpointOperation getAnother = operationByName.get("getAnother");
|
||||
assertThat(getAnother.getDescription()).isEqualTo("Get another thing");
|
||||
assertThat(getAnother.getOutputType()).isEqualTo(Object.class);
|
||||
assertThat(getAnother.getParameters()).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsCachedWithMatchingId() {
|
||||
load(TestEndpoint.class, (id) -> new CachingConfiguration(500), (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getOperationInvoker())
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(((CachingOperationInvoker) getAll.getOperationInvoker())
|
||||
.getTimeToLive()).isEqualTo(500);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extraReadOperationsAreCached() {
|
||||
load(AdditionalOperationJmxEndpointConfiguration.class,
|
||||
(id) -> new CachingConfiguration(500), (discoverer) -> {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpoints = discover(
|
||||
discoverer);
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoints.get("test").getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething",
|
||||
"update", "getAnother");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getOperationInvoker())
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(((CachingOperationInvoker) getAll.getOperationInvoker())
|
||||
.getTimeToLive()).isEqualTo(500);
|
||||
JmxEndpointOperation getAnother = operationByName.get("getAnother");
|
||||
assertThat(getAnother.getOperationInvoker())
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(
|
||||
((CachingOperationInvoker) getAnother.getOperationInvoker())
|
||||
.getTimeToLive()).isEqualTo(500);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoExtensionsHaveTheSameEndpointType() {
|
||||
load(ClashingJmxEndpointConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two extensions for the same endpoint");
|
||||
this.thrown.expectMessage(TestEndpoint.class.getName());
|
||||
this.thrown.expectMessage(TestJmxEndpointExtension.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoStandardEndpointsHaveTheSameId() {
|
||||
load(ClashingStandardEndpointConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two endpoints with the id 'test': ");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenEndpointHasTwoOperationsWithTheSameName() {
|
||||
load(ClashingOperationsEndpoint.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found multiple JMX operations with the same name");
|
||||
this.thrown.expectMessage("getAll");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenExtensionHasTwoOperationsWithTheSameName() {
|
||||
load(AdditionalClashingOperationsConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found multiple JMX operations with the same name");
|
||||
this.thrown.expectMessage("getAll");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenExtensionIsNotCompatibleWithTheEndpointType() {
|
||||
load(InvalidJmxExtensionConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid extension");
|
||||
this.thrown.expectMessage(NonJmxJmxEndpointExtension.class.getName());
|
||||
this.thrown.expectMessage(NonJmxEndpoint.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
private void assertJmxTestEndpoint(EndpointInfo<JmxEndpointOperation> endpoint) {
|
||||
Map<String, JmxEndpointOperation> operationByName = mapOperations(
|
||||
endpoint.getOperations());
|
||||
assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", "update");
|
||||
JmxEndpointOperation getAll = operationByName.get("getAll");
|
||||
assertThat(getAll.getDescription()).isEqualTo("Get all the things");
|
||||
assertThat(getAll.getOutputType()).isEqualTo(Object.class);
|
||||
assertThat(getAll.getParameters()).isEmpty();
|
||||
JmxEndpointOperation getSomething = operationByName.get("getSomething");
|
||||
assertThat(getSomething.getDescription())
|
||||
.isEqualTo("Get something based on a timeUnit");
|
||||
assertThat(getSomething.getOutputType()).isEqualTo(String.class);
|
||||
assertThat(getSomething.getParameters()).hasSize(1);
|
||||
hasDocumentedParameter(getSomething, 0, "unitMs", Long.class,
|
||||
"Number of milliseconds");
|
||||
JmxEndpointOperation update = operationByName.get("update");
|
||||
assertThat(update.getDescription()).isEqualTo("Update something based on bar");
|
||||
assertThat(update.getOutputType()).isEqualTo(Void.TYPE);
|
||||
assertThat(update.getParameters()).hasSize(2);
|
||||
hasDocumentedParameter(update, 0, "foo", String.class, "Foo identifier");
|
||||
hasDocumentedParameter(update, 1, "bar", String.class, "Bar value");
|
||||
}
|
||||
|
||||
private void hasDefaultParameter(JmxEndpointOperation operation, int index,
|
||||
Class<?> type) {
|
||||
assertThat(index).isLessThan(operation.getParameters().size());
|
||||
JmxEndpointOperationParameterInfo parameter = operation.getParameters()
|
||||
.get(index);
|
||||
assertThat(parameter.getType()).isEqualTo(type);
|
||||
assertThat(parameter.getDescription()).isNull();
|
||||
}
|
||||
|
||||
private void hasDocumentedParameter(JmxEndpointOperation operation, int index,
|
||||
String name, Class<?> type, String description) {
|
||||
assertThat(index).isLessThan(operation.getParameters().size());
|
||||
JmxEndpointOperationParameterInfo parameter = operation.getParameters()
|
||||
.get(index);
|
||||
assertThat(parameter.getName()).isEqualTo(name);
|
||||
assertThat(parameter.getType()).isEqualTo(type);
|
||||
assertThat(parameter.getDescription()).isEqualTo(description);
|
||||
}
|
||||
|
||||
private Map<String, EndpointInfo<JmxEndpointOperation>> discover(
|
||||
JmxAnnotationEndpointDiscoverer discoverer) {
|
||||
Map<String, EndpointInfo<JmxEndpointOperation>> endpointsById = new HashMap<>();
|
||||
discoverer.discoverEndpoints()
|
||||
.forEach((endpoint) -> endpointsById.put(endpoint.getId(), endpoint));
|
||||
return endpointsById;
|
||||
}
|
||||
|
||||
private Map<String, JmxEndpointOperation> mapOperations(
|
||||
Collection<JmxEndpointOperation> operations) {
|
||||
Map<String, JmxEndpointOperation> operationByName = new HashMap<>();
|
||||
operations.forEach((operation) -> operationByName
|
||||
.put(operation.getOperationName(), operation));
|
||||
return operationByName;
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Consumer<JmxAnnotationEndpointDiscoverer> consumer) {
|
||||
load(configuration, (id) -> null, consumer);
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Function<String, CachingConfiguration> cachingConfigurationFactory,
|
||||
Consumer<JmxAnnotationEndpointDiscoverer> consumer) {
|
||||
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configuration)) {
|
||||
consumer.accept(new JmxAnnotationEndpointDiscoverer(context,
|
||||
new ConversionServiceOperationParameterMapper(
|
||||
DefaultConversionService.getSharedInstance()),
|
||||
cachingConfigurationFactory));
|
||||
}
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
private static class TestEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public String getSomething(TimeUnit timeUnit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void update(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "jmx", types = EndpointType.JMX)
|
||||
private static class TestJmxEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = TestEndpoint.class)
|
||||
private static class TestJmxEndpointExtension {
|
||||
|
||||
@ManagedOperation(description = "Get all the things")
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
@ManagedOperation(description = "Get something based on a timeUnit")
|
||||
@ManagedOperationParameters({
|
||||
@ManagedOperationParameter(name = "unitMs", description = "Number of milliseconds") })
|
||||
public String getSomething(Long timeUnit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
@ManagedOperation(description = "Update something based on bar")
|
||||
@ManagedOperationParameters({
|
||||
@ManagedOperationParameter(name = "foo", description = "Foo identifier"),
|
||||
@ManagedOperationParameter(name = "bar", description = "Bar value") })
|
||||
public void update(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = TestEndpoint.class)
|
||||
private static class AdditionalOperationJmxEndpointExtension {
|
||||
|
||||
@ManagedOperation(description = "Get another thing")
|
||||
@ReadOperation
|
||||
public Object getAnother() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
static class ClashingOperationsEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = TestEndpoint.class)
|
||||
static class ClashingOperationsJmxEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll(String param) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "nonjmx", types = EndpointType.WEB)
|
||||
private static class NonJmxEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@JmxEndpointExtension(endpoint = NonJmxEndpoint.class)
|
||||
private static class NonJmxJmxEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getSomething() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class MultipleEndpointsConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpoint testJmxEndpoint() {
|
||||
return new TestJmxEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NonJmxEndpoint nonJmxEndpoint() {
|
||||
return new NonJmxEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class OverriddenOperationJmxEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpointExtension testJmxEndpointExtension() {
|
||||
return new TestJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AdditionalOperationJmxEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AdditionalOperationJmxEndpointExtension additionalOperationJmxEndpointExtension() {
|
||||
return new AdditionalOperationJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class AdditionalClashingOperationsConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClashingOperationsJmxEndpointExtension clashingOperationsJmxEndpointExtension() {
|
||||
return new ClashingOperationsJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingJmxEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpointExtension testExtensionOne() {
|
||||
return new TestJmxEndpointExtension();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestJmxEndpointExtension testExtensionTwo() {
|
||||
return new TestJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingStandardEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointTwo() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointOne() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class InvalidJmxExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public NonJmxEndpoint nonJmxEndpoint() {
|
||||
return new NonJmxEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NonJmxJmxEndpointExtension nonJmxJmxEndpointExtension() {
|
||||
return new NonJmxJmxEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,481 @@
|
|||
/*
|
||||
* 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 java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.endpoint.CachingConfiguration;
|
||||
import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
import org.springframework.boot.endpoint.OperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.ReadOperation;
|
||||
import org.springframework.boot.endpoint.Selector;
|
||||
import org.springframework.boot.endpoint.WriteOperation;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Abstract base class for web endpoint integration tests.
|
||||
*
|
||||
* @param <T> the type of application context used by the tests
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class AbstractWebEndpointIntegrationTests<T extends ConfigurableApplicationContext> {
|
||||
|
||||
private final Class<?> exporterConfiguration;
|
||||
|
||||
protected AbstractWebEndpointIntegrationTests(Class<?> exporterConfiguration) {
|
||||
this.exporterConfiguration = exporterConfiguration;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperation() {
|
||||
load(TestEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/test").accept(MediaType.APPLICATION_JSON)
|
||||
.exchange().expectStatus().isOk().expectBody().jsonPath("All")
|
||||
.isEqualTo(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithSelector() {
|
||||
load(TestEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/test/one")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isOk().expectBody().jsonPath("part").isEqualTo("one"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithSelectorContainingADot() {
|
||||
load(TestEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/test/foo.bar")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isOk().expectBody().jsonPath("part").isEqualTo("foo.bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void linksToOtherEndpointsAreProvided() {
|
||||
load(TestEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("").accept(MediaType.APPLICATION_JSON)
|
||||
.exchange().expectStatus().isOk().expectBody()
|
||||
.jsonPath("_links.length()").isEqualTo(3)
|
||||
.jsonPath("_links.self.href").isNotEmpty()
|
||||
.jsonPath("_links.self.templated").isEqualTo(false)
|
||||
.jsonPath("_links.test.href").isNotEmpty()
|
||||
.jsonPath("_links.test.templated").isEqualTo(false)
|
||||
.jsonPath("_links.test-part.href").isNotEmpty()
|
||||
.jsonPath("_links.test-part.templated").isEqualTo(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithSingleQueryParameters() {
|
||||
load(QueryEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/query?one=1&two=2")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isOk().expectBody().jsonPath("query").isEqualTo("1 2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithSingleQueryParametersAndMultipleValues() {
|
||||
load(QueryEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/query?one=1&one=1&two=2")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isOk().expectBody().jsonPath("query").isEqualTo("1,1 2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithListQueryParameterAndSingleValue() {
|
||||
load(QueryWithListEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/query?one=1&two=2")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isOk().expectBody().jsonPath("query").isEqualTo("1 [2]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithListQueryParameterAndMultipleValues() {
|
||||
load(QueryWithListEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/query?one=1&two=2&two=2")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isOk().expectBody().jsonPath("query").isEqualTo("1 [2, 2]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithMappingFailureProducesBadRequestResponse() {
|
||||
load(QueryEndpointConfiguration.class,
|
||||
(client) -> client.get().uri("/query?two=two")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeOperation() {
|
||||
load(TestEndpointConfiguration.class, (client) -> {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("foo", "one");
|
||||
body.put("bar", "two");
|
||||
client.post().uri("/test").syncBody(body).accept(MediaType.APPLICATION_JSON)
|
||||
.exchange().expectStatus().isNoContent().expectBody().isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void writeOperationWithVoidResponse() {
|
||||
load(VoidWriteResponseEndpointConfiguration.class, (context, client) -> {
|
||||
client.post().uri("/voidwrite").accept(MediaType.APPLICATION_JSON).exchange()
|
||||
.expectStatus().isNoContent().expectBody().isEmpty();
|
||||
verify(context.getBean(EndpointDelegate.class)).write();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullIsPassedToTheOperationWhenArgumentIsNotFoundInPostRequestBody() {
|
||||
load(TestEndpointConfiguration.class, (context, client) -> {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("foo", "one");
|
||||
client.post().uri("/test").syncBody(body).accept(MediaType.APPLICATION_JSON)
|
||||
.exchange().expectStatus().isNoContent().expectBody().isEmpty();
|
||||
verify(context.getBean(EndpointDelegate.class)).write("one", null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullsArePassedToTheOperationWhenPostRequestHasNoBody() {
|
||||
load(TestEndpointConfiguration.class, (context, client) -> {
|
||||
client.post().uri("/test").contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isNoContent().expectBody().isEmpty();
|
||||
verify(context.getBean(EndpointDelegate.class)).write(null, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullResponseFromReadOperationResultsInNotFoundResponseStatus() {
|
||||
load(NullReadResponseEndpointConfiguration.class,
|
||||
(context, client) -> client.get().uri("/nullread")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullResponseFromWriteOperationResultsInNoContentResponseStatus() {
|
||||
load(NullWriteResponseEndpointConfiguration.class,
|
||||
(context, client) -> client.post().uri("/nullwrite")
|
||||
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
|
||||
.isNoContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithResourceResponse() {
|
||||
load(ResourceEndpointConfiguration.class, (context, client) -> {
|
||||
byte[] responseBody = client.get().uri("/resource").exchange().expectStatus()
|
||||
.isOk().expectHeader().contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.returnResult(byte[].class).getResponseBodyContent();
|
||||
assertThat(responseBody).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readOperationWithResourceWebOperationResponse() {
|
||||
load(ResourceWebEndpointResponseEndpointConfiguration.class,
|
||||
(context, client) -> {
|
||||
byte[] responseBody = client.get().uri("/resource").exchange()
|
||||
.expectStatus().isOk().expectHeader()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.returnResult(byte[].class).getResponseBodyContent();
|
||||
assertThat(responseBody).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 8,
|
||||
9);
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract T createApplicationContext(Class<?>... config);
|
||||
|
||||
protected abstract int getPort(T context);
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
BiConsumer<ApplicationContext, WebTestClient> consumer) {
|
||||
T context = createApplicationContext(configuration, this.exporterConfiguration);
|
||||
try {
|
||||
consumer.accept(context,
|
||||
WebTestClient.bindToServer()
|
||||
.baseUrl(
|
||||
"http://localhost:" + getPort(context) + "/endpoints")
|
||||
.build());
|
||||
}
|
||||
finally {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected void load(Class<?> configuration, Consumer<WebTestClient> clientConsumer) {
|
||||
load(configuration, (context, client) -> clientConsumer.accept(client));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class BaseConfiguration {
|
||||
|
||||
@Bean
|
||||
public EndpointDelegate endpointDelegate() {
|
||||
return mock(EndpointDelegate.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebAnnotationEndpointDiscoverer webEndpointDiscoverer(
|
||||
ApplicationContext applicationContext) {
|
||||
OperationParameterMapper parameterMapper = new ConversionServiceOperationParameterMapper(
|
||||
DefaultConversionService.getSharedInstance());
|
||||
return new WebAnnotationEndpointDiscoverer(applicationContext,
|
||||
parameterMapper, (id) -> new CachingConfiguration(0),
|
||||
Collections.singletonList("application/json"),
|
||||
Collections.singletonList("application/json"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
protected static class TestEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint(EndpointDelegate endpointDelegate) {
|
||||
return new TestEndpoint(endpointDelegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class QueryEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public QueryEndpoint queryEndpoint() {
|
||||
return new QueryEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class QueryWithListEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public QueryWithListEndpoint queryEndpoint() {
|
||||
return new QueryWithListEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class VoidWriteResponseEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public VoidWriteResponseEndpoint voidWriteResponseEndpoint(
|
||||
EndpointDelegate delegate) {
|
||||
return new VoidWriteResponseEndpoint(delegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class NullWriteResponseEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public NullWriteResponseEndpoint nullWriteResponseEndpoint(
|
||||
EndpointDelegate delegate) {
|
||||
return new NullWriteResponseEndpoint(delegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class NullReadResponseEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public NullReadResponseEndpoint nullResponseEndpoint() {
|
||||
return new NullReadResponseEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class ResourceEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public ResourceEndpoint resourceEndpoint() {
|
||||
return new ResourceEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class ResourceWebEndpointResponseEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public ResourceWebEndpointResponseEndpoint resourceEndpoint() {
|
||||
return new ResourceWebEndpointResponseEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
static class TestEndpoint {
|
||||
|
||||
private final EndpointDelegate endpointDelegate;
|
||||
|
||||
TestEndpoint(EndpointDelegate endpointDelegate) {
|
||||
this.endpointDelegate = endpointDelegate;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Map<String, Object> readAll() {
|
||||
return Collections.singletonMap("All", true);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Map<String, Object> readPart(@Selector String part) {
|
||||
return Collections.singletonMap("part", part);
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void write(String foo, String bar) {
|
||||
this.endpointDelegate.write(foo, bar);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "query")
|
||||
static class QueryEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Map<String, String> query(String one, Integer two) {
|
||||
return Collections.singletonMap("query", one + " " + two);
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Map<String, String> queryWithParameterList(@Selector String list,
|
||||
String one, List<String> two) {
|
||||
return Collections.singletonMap("query", list + " " + one + " " + two);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "query")
|
||||
static class QueryWithListEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Map<String, String> queryWithParameterList(String one, List<String> two) {
|
||||
return Collections.singletonMap("query", one + " " + two);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "voidwrite")
|
||||
static class VoidWriteResponseEndpoint {
|
||||
|
||||
private final EndpointDelegate delegate;
|
||||
|
||||
VoidWriteResponseEndpoint(EndpointDelegate delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void write() {
|
||||
this.delegate.write();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "nullwrite")
|
||||
static class NullWriteResponseEndpoint {
|
||||
|
||||
private final EndpointDelegate delegate;
|
||||
|
||||
NullWriteResponseEndpoint(EndpointDelegate delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public Object write() {
|
||||
this.delegate.write();
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "nullread")
|
||||
static class NullReadResponseEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public String readReturningNull() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "resource")
|
||||
static class ResourceEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Resource read() {
|
||||
return new ByteArrayResource(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "resource")
|
||||
static class ResourceWebEndpointResponseEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public WebEndpointResponse<Resource> read() {
|
||||
return new WebEndpointResponse<Resource>(
|
||||
new ByteArrayResource(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }),
|
||||
200);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface EndpointDelegate {
|
||||
|
||||
void write();
|
||||
|
||||
void write(String foo, String bar);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,628 @@
|
|||
/*
|
||||
* 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.ArrayList;
|
||||
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 java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.boot.endpoint.CachingConfiguration;
|
||||
import org.springframework.boot.endpoint.CachingOperationInvoker;
|
||||
import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper;
|
||||
import org.springframework.boot.endpoint.Endpoint;
|
||||
import org.springframework.boot.endpoint.EndpointInfo;
|
||||
import org.springframework.boot.endpoint.EndpointType;
|
||||
import org.springframework.boot.endpoint.OperationInvoker;
|
||||
import org.springframework.boot.endpoint.ReadOperation;
|
||||
import org.springframework.boot.endpoint.Selector;
|
||||
import org.springframework.boot.endpoint.WriteOperation;
|
||||
import org.springframework.boot.endpoint.web.AbstractWebEndpointIntegrationTests.BaseConfiguration;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebAnnotationEndpointDiscoverer}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class WebAnnotationEndpointDiscovererTests {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void discoveryWorksWhenThereAreNoEndpoints() {
|
||||
load(EmptyConfiguration.class,
|
||||
(discoverer) -> assertThat(discoverer.discoverEndpoints()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void webExtensionMustHaveEndpoint() {
|
||||
load(TestWebEndpointExtensionConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid extension");
|
||||
this.thrown.expectMessage(TestWebEndpointExtension.class.getName());
|
||||
this.thrown.expectMessage("no endpoint found");
|
||||
this.thrown.expectMessage(TestEndpoint.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onlyWebEndpointsAreDiscovered() {
|
||||
load(MultipleEndpointsConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
|
||||
discoverer.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oneOperationIsDiscoveredWhenExtensionOverridesOperation() {
|
||||
load(OverriddenOperationWebEndpointExtensionConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
|
||||
discoverer.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
EndpointInfo<WebEndpointOperation> endpoint = endpoints.get("test");
|
||||
assertThat(requestPredicates(endpoint)).has(
|
||||
requestPredicates(path("test").httpMethod(WebEndpointHttpMethod.GET)
|
||||
.consumes().produces("application/json")));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoOperationsAreDiscoveredWhenExtensionAddsOperation() {
|
||||
load(AdditionalOperationWebEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
|
||||
discoverer.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
EndpointInfo<WebEndpointOperation> endpoint = endpoints.get("test");
|
||||
assertThat(requestPredicates(endpoint)).has(requestPredicates(
|
||||
path("test").httpMethod(WebEndpointHttpMethod.GET).consumes()
|
||||
.produces("application/json"),
|
||||
path("test/{id}").httpMethod(WebEndpointHttpMethod.GET).consumes()
|
||||
.produces("application/json")));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void predicateForWriteOperationThatReturnsVoidHasNoProducedMediaTypes() {
|
||||
load(VoidWriteOperationEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
|
||||
discoverer.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("voidwrite");
|
||||
EndpointInfo<WebEndpointOperation> endpoint = endpoints.get("voidwrite");
|
||||
assertThat(requestPredicates(endpoint)).has(requestPredicates(
|
||||
path("voidwrite").httpMethod(WebEndpointHttpMethod.POST).produces()
|
||||
.consumes("application/json")));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoExtensionsHaveTheSameEndpointType() {
|
||||
load(ClashingWebEndpointConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two extensions for the same endpoint");
|
||||
this.thrown.expectMessage(TestEndpoint.class.getName());
|
||||
this.thrown.expectMessage(TestWebEndpointExtension.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenTwoStandardEndpointsHaveTheSameId() {
|
||||
load(ClashingStandardEndpointConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Found two endpoints with the id 'test': ");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenEndpointHasClashingOperations() {
|
||||
load(ClashingOperationsEndpointConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage(
|
||||
"Found multiple web operations with matching request predicates:");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void discoveryFailsWhenExtensionIsNotCompatibleWithTheEndpointType() {
|
||||
load(InvalidWebExtensionConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage("Invalid extension");
|
||||
this.thrown.expectMessage(NonWebWebEndpointExtension.class.getName());
|
||||
this.thrown.expectMessage(NonWebEndpoint.class.getName());
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoOperationsOnSameEndpointClashWhenSelectorsHaveDifferentNames() {
|
||||
load(ClashingSelectorsWebEndpointExtensionConfiguration.class, (discoverer) -> {
|
||||
this.thrown.expect(IllegalStateException.class);
|
||||
this.thrown.expectMessage(
|
||||
"Found multiple web operations with matching request predicates:");
|
||||
discoverer.discoverEndpoints();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void endpointMainReadOperationIsCachedWithMatchingId() {
|
||||
load((id) -> new CachingConfiguration(500), TestEndpointConfiguration.class,
|
||||
(discoverer) -> {
|
||||
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
|
||||
discoverer.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
EndpointInfo<WebEndpointOperation> endpoint = endpoints.get("test");
|
||||
assertThat(endpoint.getOperations()).hasSize(1);
|
||||
OperationInvoker operationInvoker = endpoint.getOperations()
|
||||
.iterator().next().getOperationInvoker();
|
||||
assertThat(operationInvoker)
|
||||
.isInstanceOf(CachingOperationInvoker.class);
|
||||
assertThat(
|
||||
((CachingOperationInvoker) operationInvoker).getTimeToLive())
|
||||
.isEqualTo(500);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void operationsThatReturnResourceProduceApplicationOctetStream() {
|
||||
load(ResourceEndpointConfiguration.class, (discoverer) -> {
|
||||
Map<String, EndpointInfo<WebEndpointOperation>> endpoints = mapEndpoints(
|
||||
discoverer.discoverEndpoints());
|
||||
assertThat(endpoints).containsOnlyKeys("resource");
|
||||
EndpointInfo<WebEndpointOperation> endpoint = endpoints.get("resource");
|
||||
assertThat(requestPredicates(endpoint)).has(requestPredicates(
|
||||
path("resource").httpMethod(WebEndpointHttpMethod.GET).consumes()
|
||||
.produces("application/octet-stream")));
|
||||
});
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration,
|
||||
Consumer<WebAnnotationEndpointDiscoverer> consumer) {
|
||||
this.load((id) -> null, configuration, consumer);
|
||||
}
|
||||
|
||||
private void load(Function<String, CachingConfiguration> cachingConfigurationFactory,
|
||||
Class<?> configuration, Consumer<WebAnnotationEndpointDiscoverer> consumer) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
configuration);
|
||||
try {
|
||||
consumer.accept(new WebAnnotationEndpointDiscoverer(context,
|
||||
new ConversionServiceOperationParameterMapper(
|
||||
DefaultConversionService.getSharedInstance()),
|
||||
cachingConfigurationFactory,
|
||||
Collections.singletonList("application/json"),
|
||||
Collections.singletonList("application/json")));
|
||||
}
|
||||
finally {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, EndpointInfo<WebEndpointOperation>> mapEndpoints(
|
||||
Collection<EndpointInfo<WebEndpointOperation>> endpoints) {
|
||||
Map<String, EndpointInfo<WebEndpointOperation>> endpointById = new HashMap<>();
|
||||
endpoints.forEach((endpoint) -> endpointById.put(endpoint.getId(), endpoint));
|
||||
return endpointById;
|
||||
}
|
||||
|
||||
private List<OperationRequestPredicate> requestPredicates(
|
||||
EndpointInfo<WebEndpointOperation> endpoint) {
|
||||
return endpoint.getOperations().stream()
|
||||
.map(WebEndpointOperation::getRequestPredicate)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Condition<List<? extends OperationRequestPredicate>> requestPredicates(
|
||||
RequestPredicateMatcher... matchers) {
|
||||
return new Condition<>((predicates) -> {
|
||||
if (predicates.size() != matchers.length) {
|
||||
return false;
|
||||
}
|
||||
Map<OperationRequestPredicate, Long> matchCounts = new HashMap<>();
|
||||
for (OperationRequestPredicate predicate : predicates) {
|
||||
matchCounts.put(predicate, Stream.of(matchers)
|
||||
.filter(matcher -> matcher.matches(predicate)).count());
|
||||
}
|
||||
return matchCounts.values().stream().noneMatch(count -> count != 1);
|
||||
}, Arrays.toString(matchers));
|
||||
}
|
||||
|
||||
private RequestPredicateMatcher path(String path) {
|
||||
return new RequestPredicateMatcher(path);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class EmptyConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@WebEndpointExtension(endpoint = TestEndpoint.class)
|
||||
static class TestWebEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getOne(@Selector String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@WriteOperation
|
||||
public void update(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
public void someOtherMethod() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
static class TestEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@WebEndpointExtension(endpoint = TestEndpoint.class)
|
||||
static class OverriddenOperationWebEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@WebEndpointExtension(endpoint = TestEndpoint.class)
|
||||
static class AdditionalOperationWebEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getOne(@Selector String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "test")
|
||||
static class ClashingOperationsEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getAgain() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@WebEndpointExtension(endpoint = TestEndpoint.class)
|
||||
static class ClashingOperationsWebEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getAll() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object getAgain() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@WebEndpointExtension(endpoint = TestEndpoint.class)
|
||||
static class ClashingSelectorsWebEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object readOne(@Selector String oneA, @Selector String oneB) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ReadOperation
|
||||
public Object readTwo(@Selector String twoA, @Selector String twoB) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "nonweb", types = EndpointType.JMX)
|
||||
static class NonWebEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Object getData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@WebEndpointExtension(endpoint = NonWebEndpoint.class)
|
||||
static class NonWebWebEndpointExtension {
|
||||
|
||||
@ReadOperation
|
||||
public Object getSomething(@Selector String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "voidwrite")
|
||||
static class VoidWriteOperationEndpoint {
|
||||
|
||||
@WriteOperation
|
||||
public void write(String foo, String bar) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "resource")
|
||||
static class ResourceEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Resource read() {
|
||||
return new ByteArrayResource(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class MultipleEndpointsConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NonWebEndpoint nonWebEndpoint() {
|
||||
return new NonWebEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestWebEndpointExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestWebEndpointExtension endpointExtension() {
|
||||
return new TestWebEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingOperationsEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public ClashingOperationsEndpoint clashingOperationsEndpoint() {
|
||||
return new ClashingOperationsEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingOperationsWebEndpointExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public ClashingOperationsWebEndpointExtension clashingOperationsExtension() {
|
||||
return new ClashingOperationsWebEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(TestEndpointConfiguration.class)
|
||||
static class OverriddenOperationWebEndpointExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public OverriddenOperationWebEndpointExtension overriddenOperationExtension() {
|
||||
return new OverriddenOperationWebEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(TestEndpointConfiguration.class)
|
||||
static class AdditionalOperationWebEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public AdditionalOperationWebEndpointExtension additionalOperationExtension() {
|
||||
return new AdditionalOperationWebEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingWebEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestWebEndpointExtension testExtensionOne() {
|
||||
return new TestWebEndpointExtension();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestWebEndpointExtension testExtensionTwo() {
|
||||
return new TestWebEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingStandardEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointTwo() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpointOne() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ClashingSelectorsWebEndpointExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestEndpoint testEndpoint() {
|
||||
return new TestEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClashingSelectorsWebEndpointExtension clashingSelectorsExtension() {
|
||||
return new ClashingSelectorsWebEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class InvalidWebExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public NonWebEndpoint nonWebEndpoint() {
|
||||
return new NonWebEndpoint();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NonWebWebEndpointExtension nonWebWebEndpointExtension() {
|
||||
return new NonWebWebEndpointExtension();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class VoidWriteOperationEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public VoidWriteOperationEndpoint voidWriteOperationEndpoint() {
|
||||
return new VoidWriteOperationEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class ResourceEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public ResourceEndpoint resourceEndpoint() {
|
||||
return new ResourceEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class RequestPredicateMatcher {
|
||||
|
||||
private final String path;
|
||||
|
||||
private List<String> produces;
|
||||
|
||||
private List<String> consumes;
|
||||
|
||||
private WebEndpointHttpMethod httpMethod;
|
||||
|
||||
private RequestPredicateMatcher(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public RequestPredicateMatcher produces(String... mediaTypes) {
|
||||
this.produces = Arrays.asList(mediaTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RequestPredicateMatcher consumes(String... mediaTypes) {
|
||||
this.consumes = Arrays.asList(mediaTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
private RequestPredicateMatcher httpMethod(WebEndpointHttpMethod httpMethod) {
|
||||
this.httpMethod = httpMethod;
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean matches(OperationRequestPredicate predicate) {
|
||||
return (this.path == null || this.path.equals(predicate.getPath()))
|
||||
&& (this.httpMethod == null
|
||||
|| this.httpMethod == predicate.getHttpMethod())
|
||||
&& (this.produces == null || this.produces
|
||||
.equals(new ArrayList<>(predicate.getProduces())))
|
||||
&& (this.consumes == null || this.consumes
|
||||
.equals(new ArrayList<>(predicate.getConsumes())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Request predicate with path = '" + this.path + "', httpMethod = '"
|
||||
+ this.httpMethod + "', produces = '" + this.produces + "'";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue