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.staticFinalFieldPrefixes=
|
||||||
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
|
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
|
||||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
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.targetPlatform=1.6
|
||||||
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||||
org.eclipse.jdt.core.compiler.compliance=1.6
|
org.eclipse.jdt.core.compiler.compliance=1.6
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,22 @@
|
||||||
</subpackage>
|
</subpackage>
|
||||||
</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 -->
|
<!-- Logging -->
|
||||||
<subpackage name="logging">
|
<subpackage name="logging">
|
||||||
<disallow pkg="org.springframework.context" />
|
<disallow pkg="org.springframework.context" />
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,11 @@
|
||||||
<artifactId>jetty-webapp</artifactId>
|
<artifactId>jetty-webapp</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.core</groupId>
|
||||||
|
<artifactId>jersey-server</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hamcrest</groupId>
|
<groupId>org.hamcrest</groupId>
|
||||||
<artifactId>hamcrest-library</artifactId>
|
<artifactId>hamcrest-library</artifactId>
|
||||||
|
|
@ -271,6 +276,11 @@
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.jayway.jsonpath</groupId>
|
||||||
|
<artifactId>json-path</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.sqlserver</groupId>
|
<groupId>com.microsoft.sqlserver</groupId>
|
||||||
<artifactId>mssql-jdbc</artifactId>
|
<artifactId>mssql-jdbc</artifactId>
|
||||||
|
|
@ -316,6 +326,16 @@
|
||||||
<artifactId>jaybird-jdk18</artifactId>
|
<artifactId>jaybird-jdk18</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</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>
|
<dependency>
|
||||||
<groupId>org.hsqldb</groupId>
|
<groupId>org.hsqldb</groupId>
|
||||||
<artifactId>hsqldb</artifactId>
|
<artifactId>hsqldb</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -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