Merge branch 'endpoint-infrastructure'

This commit is contained in:
Andy Wilkinson 2017-08-03 18:44:19 +01:00
commit 4f45b2bb52
61 changed files with 6983 additions and 2 deletions

View File

@ -10,6 +10,7 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6

View File

@ -29,6 +29,22 @@
</subpackage>
</subpackage>
<!-- Endpoint infrastructure -->
<subpackage name="endpoint">
<disallow pkg="org.springframework.http" />
<disallow pkg="org.springframework.web" />
<subpackage name="web">
<allow pkg="org.springframework.http" />
<allow pkg="org.springframework.web" />
<subpackage name="mvc">
<disallow pkg="org.springframework.web.reactive" />
</subpackage>
<subpackage name="reactive">
<disallow pkg="org.springframework.web.servlet" />
</subpackage>
</subpackage>
</subpackage>
<!-- Logging -->
<subpackage name="logging">
<disallow pkg="org.springframework.context" />
@ -109,4 +125,4 @@
</subpackage>
</subpackage>
</import-control>
</import-control>

View File

@ -159,6 +159,11 @@
<artifactId>jetty-webapp</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
@ -271,6 +276,11 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
@ -316,6 +326,16 @@
<artifactId>jaybird-jdk18</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
@ -352,4 +372,4 @@
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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