diff --git a/eclipse/org.eclipse.jdt.core.prefs b/eclipse/org.eclipse.jdt.core.prefs
index e44b6fc3667..3ec70345dfe 100644
--- a/eclipse/org.eclipse.jdt.core.prefs
+++ b/eclipse/org.eclipse.jdt.core.prefs
@@ -10,6 +10,7 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
diff --git a/spring-boot-parent/src/checkstyle/import-control.xml b/spring-boot-parent/src/checkstyle/import-control.xml
index b73b7cbd407..cc52288841b 100644
--- a/spring-boot-parent/src/checkstyle/import-control.xml
+++ b/spring-boot-parent/src/checkstyle/import-control.xml
@@ -29,6 +29,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -109,4 +125,4 @@
-
+
\ No newline at end of file
diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml
index 56f060e6201..21d22b2c0e7 100644
--- a/spring-boot/pom.xml
+++ b/spring-boot/pom.xml
@@ -159,6 +159,11 @@
jetty-webapp
true
+
+ org.glassfish.jersey.core
+ jersey-server
+ true
+
org.hamcrest
hamcrest-library
@@ -271,6 +276,11 @@
h2
test
+
+ com.jayway.jsonpath
+ json-path
+ test
+
com.microsoft.sqlserver
mssql-jdbc
@@ -316,6 +326,16 @@
jaybird-jdk18
test
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet-core
+ test
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-jackson
+ test
+
org.hsqldb
hsqldb
@@ -352,4 +372,4 @@
test
-
+
\ No newline at end of file
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/AnnotationEndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/AnnotationEndpointDiscoverer.java
new file mode 100644
index 00000000000..94eb2d9554b
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/AnnotationEndpointDiscoverer.java
@@ -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 the type of the operation
+ * @param the type of the operation key
+ * @author Andy Wilkinson
+ * @author Stephane Nicoll
+ * @since 2.0.0
+ */
+public abstract class AnnotationEndpointDiscoverer
+ implements EndpointDiscoverer {
+
+ private final ApplicationContext applicationContext;
+
+ private final EndpointOperationFactory operationFactory;
+
+ private final Function operationKeyFactory;
+
+ private final Function cachingConfigurationFactory;
+
+ protected AnnotationEndpointDiscoverer(ApplicationContext applicationContext,
+ EndpointOperationFactory operationFactory,
+ Function operationKeyFactory,
+ Function 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> discoverEndpointsWithExtension(
+ Class extends Annotation> extensionType, EndpointType endpointType) {
+ Map, EndpointInfo> endpoints = discoverGenericEndpoints(endpointType);
+ Map, EndpointExtensionInfo> extensions = discoverExtensions(endpoints,
+ extensionType, endpointType);
+ Collection> result = new ArrayList<>();
+ endpoints.forEach((endpointClass, endpointInfo) -> {
+ EndpointExtensionInfo extension = extensions.remove(endpointClass);
+ result.add(createDescriptor(endpointClass, endpointInfo, extension));
+ });
+ return result;
+ }
+
+ private EndpointInfoDescriptor createDescriptor(Class> endpointType,
+ EndpointInfo endpoint, EndpointExtensionInfo extension) {
+ Map, List> 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 mergeEndpoint(EndpointInfo endpoint,
+ EndpointExtensionInfo extension) {
+ Map operations = new HashMap<>();
+ Consumer 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, List> indexOperations(String endpointId,
+ Class> target, Collection operations) {
+ LinkedMultiValueMap, T> operationByKey = new LinkedMultiValueMap<>();
+ operations
+ .forEach(
+ (operation) -> operationByKey.add(
+ new OperationKey<>(endpointId, target,
+ this.operationKeyFactory.apply(operation)),
+ operation));
+ return operationByKey;
+ }
+
+ private Map, EndpointInfo> discoverGenericEndpoints(
+ EndpointType endpointType) {
+ String[] endpointBeanNames = this.applicationContext
+ .getBeanNamesForAnnotation(Endpoint.class);
+ Map> endpointsById = new HashMap<>();
+ Map, EndpointInfo> 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 endpointInfo = createEndpointInfo(endpointsById, beanName,
+ beanType, endpointAttributes, endpointId);
+ endpointsByClass.put(beanType, endpointInfo);
+ }
+ }
+ return endpointsByClass;
+ }
+
+ private EndpointInfo createEndpointInfo(Map> endpointsById,
+ String beanName, Class> beanType, AnnotationAttributes endpointAttributes,
+ String endpointId) {
+ Map operationMethods = discoverOperations(endpointId, beanName,
+ beanType);
+ EndpointInfo endpointInfo = new EndpointInfo<>(endpointId,
+ endpointAttributes.getBoolean("enabledByDefault"),
+ operationMethods.values());
+ EndpointInfo 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, EndpointExtensionInfo> discoverExtensions(
+ Map, EndpointInfo> endpoints,
+ Class extends Annotation> extensionType, EndpointType endpointType) {
+ if (extensionType == null) {
+ return Collections.emptyMap();
+ }
+ String[] extensionBeanNames = this.applicationContext
+ .getBeanNamesForAnnotation(extensionType);
+ Map, EndpointExtensionInfo> 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 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 operationMethods = discoverOperations(endpoint.getId(),
+ beanName, beanType);
+ EndpointExtensionInfo extension = new EndpointExtensionInfo<>(beanType,
+ operationMethods.values());
+ EndpointExtensionInfo 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 discoverOperations(String endpointId, String beanName,
+ Class> beanType) {
+ return MethodIntrospector.selectMethods(beanType,
+ (MethodIntrospector.MetadataLookup) (
+ 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 the {@link EndpointOperation} type
+ */
+ @FunctionalInterface
+ protected interface EndpointOperationFactory {
+
+ /**
+ * 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 the type of the operation
+ */
+ private static final class EndpointExtensionInfo {
+
+ private final Class> endpointExtensionType;
+
+ private final Collection operations;
+
+ private EndpointExtensionInfo(Class> endpointExtensionType,
+ Collection operations) {
+ this.endpointExtensionType = endpointExtensionType;
+ this.operations = operations;
+ }
+
+ private Class> getEndpointExtensionType() {
+ return this.endpointExtensionType;
+ }
+
+ private Collection getOperations() {
+ return this.operations;
+ }
+
+ }
+
+ /**
+ * Describes an {@link EndpointInfo endpoint} and whether or not it is valid.
+ *
+ * @param the type of the operation
+ * @param the type of the operation key
+ */
+ protected static class EndpointInfoDescriptor {
+
+ private final EndpointInfo endpointInfo;
+
+ private final Map, List> operations;
+
+ protected EndpointInfoDescriptor(EndpointInfo endpointInfo,
+ Map, List> operations) {
+ this.endpointInfo = endpointInfo;
+ this.operations = operations;
+ }
+
+ public EndpointInfo getEndpointInfo() {
+ return this.endpointInfo;
+ }
+
+ public Map, List> findDuplicateOperations() {
+ Map, List> 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 the type of the key
+ */
+ protected static final class OperationKey {
+
+ 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;
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/CachingConfiguration.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/CachingConfiguration.java
new file mode 100644
index 00000000000..06d5ae264e7
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/CachingConfiguration.java
@@ -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;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/CachingOperationInvoker.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/CachingOperationInvoker.java
new file mode 100644
index 00000000000..2a80c6cbc09
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/CachingOperationInvoker.java
@@ -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 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;
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/ConversionServiceOperationParameterMapper.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/ConversionServiceOperationParameterMapper.java
new file mode 100644
index 00000000000..064e40436a5
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/ConversionServiceOperationParameterMapper.java
@@ -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 mapParameter(Object input, Class 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);
+ }
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/Endpoint.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/Endpoint.java
new file mode 100644
index 00000000000..5483ecadf17
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/Endpoint.java
@@ -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;
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointDiscoverer.java
new file mode 100644
index 00000000000..0013da9a028
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointDiscoverer.java
@@ -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 the type of the operation
+ * @author Andy Wilkinson
+ * @author Stephane Nicoll
+ * @since 2.0.0
+ */
+@FunctionalInterface
+public interface EndpointDiscoverer {
+
+ /**
+ * Perform endpoint discovery.
+ * @return the discovered endpoints
+ */
+ Collection> discoverEndpoints();
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointInfo.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointInfo.java
new file mode 100644
index 00000000000..50a852bb6f4
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointInfo.java
@@ -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 the type of the endpoint's operations
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class EndpointInfo {
+
+ private final String id;
+
+ private final boolean enabledByDefault;
+
+ private final Collection 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 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 getOperations() {
+ return this.operations;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointOperation.java
new file mode 100644
index 00000000000..f4583e05daf
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointOperation.java
@@ -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;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointOperationType.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointOperationType.java
new file mode 100644
index 00000000000..1cfabdfcacb
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointOperationType.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint;
+
+/**
+ * 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
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointType.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointType.java
new file mode 100644
index 00000000000..657a3357a49
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/EndpointType.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint;
+
+/**
+ * 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
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationInvoker.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationInvoker.java
new file mode 100644
index 00000000000..6de256a8fc5
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationInvoker.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint;
+
+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 arguments);
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationParameterMapper.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationParameterMapper.java
new file mode 100644
index 00000000000..ef8d0fa3e79
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationParameterMapper.java
@@ -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 the actual type of the parameter
+ * @throws ParameterMappingException when a mapping failure occurs
+ */
+ T mapParameter(Object input, Class parameterType);
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/ParameterMappingException.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/ParameterMappingException.java
new file mode 100644
index 00000000000..c7371217555
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/ParameterMappingException.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint;
+
+/**
+ * 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;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/ReadOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/ReadOperation.java
new file mode 100644
index 00000000000..c68397f8005
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/ReadOperation.java
@@ -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 {
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/ReflectiveOperationInvoker.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/ReflectiveOperationInvoker.java
new file mode 100644
index 00000000000..ded4079bc8e
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/ReflectiveOperationInvoker.java
@@ -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 arguments) {
+ return ReflectionUtils.invokeMethod(this.method, this.target,
+ resolveArguments(arguments));
+ }
+
+ private Object[] resolveArguments(Map 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 arguments) {
+ Object resolved = arguments.get(parameter.getName());
+ return this.parameterMapper.mapParameter(resolved, parameter.getType());
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/Selector.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/Selector.java
new file mode 100644
index 00000000000..215459e86be
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/Selector.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint;
+
+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 {
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/WriteOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/WriteOperation.java
new file mode 100644
index 00000000000..eab3b30eef0
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/WriteOperation.java
@@ -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 {
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBean.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBean.java
new file mode 100644
index 00000000000..bedb1e63ca2
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBean.java
@@ -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 operationResponseConverter;
+
+ private final EndpointMBeanInfo endpointInfo;
+
+ EndpointMBean(Function 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 arguments = new HashMap<>();
+ List 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;
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfo.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfo.java
new file mode 100644
index 00000000000..d457dbd2e44
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfo.java
@@ -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 operations;
+
+ public EndpointMBeanInfo(String endpointId, MBeanInfo mBeanInfo,
+ Map operations) {
+ this.endpointId = endpointId;
+ this.mBeanInfo = mBeanInfo;
+ this.operations = operations;
+ }
+
+ public String getEndpointId() {
+ return this.endpointId;
+ }
+
+ public MBeanInfo getMbeanInfo() {
+ return this.mBeanInfo;
+ }
+
+ public Map getOperations() {
+ return this.operations;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssembler.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssembler.java
new file mode 100644
index 00000000000..237e97641c4
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssembler.java
@@ -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 endpointInfo) {
+ Map operationsMapping = getOperationInfo(endpointInfo);
+ ModelMBeanOperationInfo[] operationsMBeanInfo = operationsMapping.values()
+ .stream().map((t) -> t.mBeanOperationInfo).collect(Collectors.toList())
+ .toArray(new ModelMBeanOperationInfo[] {});
+ Map 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 getOperationInfo(
+ EndpointInfo endpointInfo) {
+ Map 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;
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanRegistrar.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanRegistrar.java
new file mode 100644
index 00000000000..887ee965068
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanRegistrar.java
@@ -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);
+ }
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointObjectNameFactory.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointObjectNameFactory.java
new file mode 100644
index 00000000000..2647e827081
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointObjectNameFactory.java
@@ -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;
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscoverer.java
new file mode 100644
index 00000000000..11b0980a83d
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscoverer.java
@@ -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 {
+
+ 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 cachingConfigurationFactory) {
+ super(applicationContext, new JmxEndpointOperationFactory(parameterMapper),
+ JmxEndpointOperation::getOperationName, cachingConfigurationFactory);
+ }
+
+ @Override
+ public Collection> discoverEndpoints() {
+ Collection> endpointDescriptors = discoverEndpointsWithExtension(
+ JmxEndpointExtension.class, EndpointType.JMX);
+ verifyThatOperationsHaveDistinctName(endpointDescriptors);
+ return endpointDescriptors.stream().map(EndpointInfoDescriptor::getEndpointInfo)
+ .collect(Collectors.toList());
+ }
+
+ private void verifyThatOperationsHaveDistinctName(
+ Collection> endpointDescriptors) {
+ List> clashes = new ArrayList<>();
+ endpointDescriptors.forEach((descriptor) -> clashes
+ .addAll(descriptor.findDuplicateOperations().values()));
+ if (!clashes.isEmpty()) {
+ StringBuilder message = new StringBuilder();
+ message.append(
+ String.format("Found multiple 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 {
+
+ 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 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 fallback) {
+ ManagedOperation managedOperation = jmxAttributeSource
+ .getManagedOperation(method);
+ if (managedOperation != null
+ && StringUtils.hasText(managedOperation.getDescription())) {
+ return managedOperation.getDescription();
+ }
+ return fallback.get();
+ }
+
+ private List getParameters(Method method) {
+ List 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;
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointExtension.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointExtension.java
new file mode 100644
index 00000000000..aaacf887150
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointExtension.java
@@ -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();
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointMBeanFactory.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointMBeanFactory.java
new file mode 100644
index 00000000000..abfa353a7c8
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointMBeanFactory.java
@@ -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 createMBeans(
+ Collection> endpoints) {
+ return endpoints.stream().map((endpointInfo) -> {
+ EndpointMBeanInfo endpointMBeanInfo = this.assembler
+ .createEndpointMBeanInfo(endpointInfo);
+ return new EndpointMBean(this.resultMapper::mapResponse, endpointMBeanInfo);
+ }).collect(Collectors.toList());
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointOperation.java
new file mode 100644
index 00000000000..e8dc4f51883
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointOperation.java
@@ -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 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 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 getParameters() {
+ return Collections.unmodifiableList(this.parameters);
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointOperationParameterInfo.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointOperationParameterInfo.java
new file mode 100644
index 00000000000..5da2b862c4d
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxEndpointOperationParameterInfo.java
@@ -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;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxOperationResponseMapper.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxOperationResponseMapper.java
new file mode 100644
index 00000000000..74695742c6d
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/JmxOperationResponseMapper.java
@@ -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);
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/package-info.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/package-info.java
new file mode 100644
index 00000000000..ecfde0e231d
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * JMX endpoint support.
+ */
+package org.springframework.boot.endpoint.jmx;
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/package-info.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/package-info.java
new file mode 100644
index 00000000000..ae47aa6e4e6
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Endpoint support.
+ */
+package org.springframework.boot.endpoint;
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointLinksResolver.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointLinksResolver.java
new file mode 100644
index 00000000000..647a59c69fd
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/EndpointLinksResolver.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.springframework.boot.endpoint.EndpointInfo;
+
+/**
+ * A resolver for {@link Link links} to web endpoints.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class EndpointLinksResolver {
+
+ /**
+ * Resolves links to the operations of the given {code webEndpoints} based on a
+ * request with the given {@code requestUrl}.
+ *
+ * @param webEndpoints the web endpoints
+ * @param requestUrl the url of the request for the endpoint links
+ * @return the links
+ */
+ public Map resolveLinks(
+ Collection> webEndpoints,
+ String requestUrl) {
+ String normalizedUrl = normalizeRequestUrl(requestUrl);
+ Map links = new LinkedHashMap();
+ links.put("self", new Link(normalizedUrl));
+ for (EndpointInfo endpoint : webEndpoints) {
+ for (WebEndpointOperation operation : endpoint.getOperations()) {
+ webEndpoints.stream().map(EndpointInfo::getId).forEach((id) -> links
+ .put(operation.getId(), createLink(normalizedUrl, operation)));
+ }
+ }
+ return links;
+ }
+
+ private String normalizeRequestUrl(String requestUrl) {
+ if (requestUrl.endsWith("/")) {
+ return requestUrl.substring(0, requestUrl.length() - 1);
+ }
+ return requestUrl;
+ }
+
+ private Link createLink(String requestUrl, WebEndpointOperation operation) {
+ String path = operation.getRequestPredicate().getPath();
+ return new Link(requestUrl + (path.startsWith("/") ? path : "/" + path));
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/Link.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/Link.java
new file mode 100644
index 00000000000..9b2ecec8b70
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/Link.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+import org.springframework.core.style.ToStringCreator;
+
+/**
+ * Details for a link in a
+ * HAL -formatted
+ * response.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class Link {
+
+ private final String href;
+
+ private final boolean templated;
+
+ /**
+ * Creates a new {@link Link} with the given {@code href}.
+ * @param href the href
+ */
+ public Link(String href) {
+ this.href = href;
+ this.templated = href.contains("{");
+
+ }
+
+ /**
+ * Returns the href of the link.
+ * @return the href
+ */
+ public String getHref() {
+ return this.href;
+ }
+
+ /**
+ * Returns whether or not the {@link #getHref() href} is templated.
+ * @return {@code true} if the href is templated, otherwise {@code false}
+ */
+ public boolean isTemplated() {
+ return this.templated;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("href", this.href).toString();
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/OperationRequestPredicate.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/OperationRequestPredicate.java
new file mode 100644
index 00000000000..269877976bf
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/OperationRequestPredicate.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.springframework.core.style.ToStringCreator;
+
+/**
+ * A predicate for a request to an operation on a web endpoint.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class OperationRequestPredicate {
+
+ private final String path;
+
+ private final String canonicalPath;
+
+ private final WebEndpointHttpMethod httpMethod;
+
+ private final Collection consumes;
+
+ private final Collection produces;
+
+ /**
+ * Creates a new {@code WebEndpointRequestPredict}.
+ *
+ * @param path the path for the operation
+ * @param httpMethod the HTTP method that the operation supports
+ * @param produces the media types that the operation produces
+ * @param consumes the media types that the operation consumes
+ */
+ public OperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod,
+ Collection consumes, Collection produces) {
+ this.path = path;
+ this.canonicalPath = path.replaceAll("\\{.*?}", "{*}");
+ this.httpMethod = httpMethod;
+ this.consumes = consumes;
+ this.produces = produces;
+ }
+
+ /**
+ * Returns the path for the operation.
+ * @return the path
+ */
+ public String getPath() {
+ return this.path;
+ }
+
+ /**
+ * Returns the HTTP method for the operation.
+ * @return the HTTP method
+ */
+ public WebEndpointHttpMethod getHttpMethod() {
+ return this.httpMethod;
+ }
+
+ /**
+ * Returns the media types that the operation consumes.
+ * @return the consumed media types
+ */
+ public Collection getConsumes() {
+ return Collections.unmodifiableCollection(this.consumes);
+ }
+
+ /**
+ * Returns the media types that the operation produces.
+ * @return the produced media types
+ */
+ public Collection getProduces() {
+ return Collections.unmodifiableCollection(this.produces);
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("httpMethod", this.httpMethod)
+ .append("path", this.path).append("consumes", this.consumes)
+ .append("produces", this.produces).toString();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + this.consumes.hashCode();
+ result = prime * result + this.httpMethod.hashCode();
+ result = prime * result + this.canonicalPath.hashCode();
+ result = prime * result + this.produces.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ OperationRequestPredicate other = (OperationRequestPredicate) obj;
+ if (!this.consumes.equals(other.consumes)) {
+ return false;
+ }
+ if (this.httpMethod != other.httpMethod) {
+ return false;
+ }
+ if (!this.canonicalPath.equals(other.canonicalPath)) {
+ return false;
+ }
+ if (!this.produces.equals(other.produces)) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java
new file mode 100644
index 00000000000..35390beb12d
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.reactivestreams.Publisher;
+
+import org.springframework.boot.endpoint.AnnotationEndpointDiscoverer;
+import org.springframework.boot.endpoint.CachingConfiguration;
+import org.springframework.boot.endpoint.CachingOperationInvoker;
+import org.springframework.boot.endpoint.Endpoint;
+import org.springframework.boot.endpoint.EndpointInfo;
+import org.springframework.boot.endpoint.EndpointOperationType;
+import org.springframework.boot.endpoint.EndpointType;
+import org.springframework.boot.endpoint.OperationInvoker;
+import org.springframework.boot.endpoint.OperationParameterMapper;
+import org.springframework.boot.endpoint.ReflectiveOperationInvoker;
+import org.springframework.boot.endpoint.Selector;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.io.Resource;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
+ * {@link WebEndpointExtension web extensions} applied to them.
+ *
+ * @author Andy Wilkinson
+ * @author Stephane Nicoll
+ * @since 2.0.0
+ */
+public class WebAnnotationEndpointDiscoverer extends
+ AnnotationEndpointDiscoverer {
+
+ /**
+ * Creates a new {@link WebAnnotationEndpointDiscoverer} that will discover
+ * {@link Endpoint endpoints} and {@link WebEndpointExtension web extensions} using
+ * the given {@link ApplicationContext}.
+ * @param applicationContext the application context
+ * @param operationParameterMapper the {@link OperationParameterMapper} used to
+ * convert arguments when an operation is invoked
+ * @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
+ * @param consumedMediaTypes the media types consumed by web endpoint operations
+ * @param producedMediaTypes the media types produced by web endpoint operations
+ */
+ public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
+ OperationParameterMapper operationParameterMapper,
+ Function cachingConfigurationFactory,
+ Collection consumedMediaTypes,
+ Collection producedMediaTypes) {
+ super(applicationContext,
+ new WebEndpointOperationFactory(operationParameterMapper,
+ consumedMediaTypes, producedMediaTypes),
+ WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory);
+ }
+
+ @Override
+ public Collection> discoverEndpoints() {
+ Collection> endpoints = discoverEndpointsWithExtension(
+ WebEndpointExtension.class, EndpointType.WEB);
+ verifyThatOperationsHaveDistinctPredicates(endpoints);
+ return endpoints.stream().map(EndpointInfoDescriptor::getEndpointInfo)
+ .collect(Collectors.toList());
+ }
+
+ private void verifyThatOperationsHaveDistinctPredicates(
+ Collection> endpointDescriptors) {
+ List> clashes = new ArrayList<>();
+ endpointDescriptors.forEach((descriptor) -> clashes
+ .addAll(descriptor.findDuplicateOperations().values()));
+ if (!clashes.isEmpty()) {
+ StringBuilder message = new StringBuilder();
+ message.append(String.format(
+ "Found multiple web operations with matching request predicates:%n"));
+ clashes.forEach((clash) -> {
+ message.append(" ").append(clash.get(0).getRequestPredicate())
+ .append(String.format(":%n"));
+ clash.forEach((operation) -> message.append(" ")
+ .append(String.format("%s%n", operation)));
+ });
+ throw new IllegalStateException(message.toString());
+ }
+ }
+
+ private static final class WebEndpointOperationFactory
+ implements EndpointOperationFactory {
+
+ private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent(
+ "org.reactivestreams.Publisher",
+ WebEndpointOperationFactory.class.getClassLoader());
+
+ private final OperationParameterMapper parameterMapper;
+
+ private final Collection consumedMediaTypes;
+
+ private final Collection producedMediaTypes;
+
+ private WebEndpointOperationFactory(OperationParameterMapper parameterMapper,
+ Collection consumedMediaTypes,
+ Collection producedMediaTypes) {
+ this.parameterMapper = parameterMapper;
+ this.consumedMediaTypes = consumedMediaTypes;
+ this.producedMediaTypes = producedMediaTypes;
+ }
+
+ @Override
+ public WebEndpointOperation createOperation(String endpointId,
+ AnnotationAttributes operationAttributes, Object target, Method method,
+ EndpointOperationType type, long timeToLive) {
+ WebEndpointHttpMethod httpMethod = determineHttpMethod(type);
+ OperationRequestPredicate requestPredicate = new OperationRequestPredicate(
+ determinePath(endpointId, method), httpMethod,
+ determineConsumedMediaTypes(httpMethod, method),
+ determineProducedMediaTypes(method));
+ OperationInvoker invoker = new ReflectiveOperationInvoker(
+ this.parameterMapper, target, method);
+ if (timeToLive > 0) {
+ invoker = new CachingOperationInvoker(invoker, timeToLive);
+ }
+ return new WebEndpointOperation(type, invoker, determineBlocking(method),
+ requestPredicate, determineId(endpointId, method));
+ }
+
+ private String determinePath(String endpointId, Method operationMethod) {
+ StringBuilder path = new StringBuilder(endpointId);
+ Stream.of(operationMethod.getParameters())
+ .filter((
+ parameter) -> parameter.getAnnotation(Selector.class) != null)
+ .map((parameter) -> "/{" + parameter.getName() + "}")
+ .forEach(path::append);
+ return path.toString();
+ }
+
+ private String determineId(String endpointId, Method operationMethod) {
+ StringBuilder path = new StringBuilder(endpointId);
+ Stream.of(operationMethod.getParameters())
+ .filter((
+ parameter) -> parameter.getAnnotation(Selector.class) != null)
+ .map((parameter) -> "-" + parameter.getName()).forEach(path::append);
+ return path.toString();
+ }
+
+ private Collection determineConsumedMediaTypes(
+ WebEndpointHttpMethod httpMethod, Method method) {
+ if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) {
+ return this.consumedMediaTypes;
+ }
+ return Collections.emptyList();
+ }
+
+ private Collection determineProducedMediaTypes(Method method) {
+ if (Void.class.equals(method.getReturnType())
+ || void.class.equals(method.getReturnType())) {
+ return Collections.emptyList();
+ }
+ if (producesResourceResponseBody(method)) {
+ return Collections.singletonList("application/octet-stream");
+ }
+ return this.producedMediaTypes;
+ }
+
+ private boolean producesResourceResponseBody(Method method) {
+ if (Resource.class.equals(method.getReturnType())) {
+ return true;
+ }
+ if (WebEndpointResponse.class.isAssignableFrom(method.getReturnType())) {
+ ResolvableType returnType = ResolvableType.forMethodReturnType(method);
+ if (ResolvableType.forClass(Resource.class)
+ .isAssignableFrom(returnType.getGeneric(0))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean consumesRequestBody(Method method) {
+ return Stream.of(method.getParameters()).anyMatch(
+ (parameter) -> parameter.getAnnotation(Selector.class) == null);
+ }
+
+ private WebEndpointHttpMethod determineHttpMethod(
+ EndpointOperationType operationType) {
+ if (operationType == EndpointOperationType.WRITE) {
+ return WebEndpointHttpMethod.POST;
+ }
+ return WebEndpointHttpMethod.GET;
+ }
+
+ private boolean determineBlocking(Method method) {
+ return !REACTIVE_STREAMS_PRESENT
+ || !Publisher.class.isAssignableFrom(method.getReturnType());
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointExtension.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointExtension.java
new file mode 100644
index 00000000000..f89416fb23a
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointExtension.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.boot.endpoint.Endpoint;
+
+/**
+ * Identifies a type as being a Web-specific extension of an {@link Endpoint}.
+ *
+ * @author Andy Wilkinson
+ * @author Stephane Nicoll
+ * @since 2.0.0
+ * @see Endpoint
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface WebEndpointExtension {
+
+ /**
+ * The {@link Endpoint endpoint} class to which this Web extension relates.
+ * @return the endpoint class
+ */
+ Class> endpoint();
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java
new file mode 100644
index 00000000000..d43d41b872a
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+/**
+ * An enumeration of HTTP methods supported by web endpoint operations.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public enum WebEndpointHttpMethod {
+
+ /**
+ * An HTTP GET request.
+ */
+ GET,
+
+ /**
+ * An HTTP POST request.
+ */
+ POST
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointOperation.java
new file mode 100644
index 00000000000..df87213729c
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointOperation.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+import org.springframework.boot.endpoint.EndpointOperation;
+import org.springframework.boot.endpoint.EndpointOperationType;
+import org.springframework.boot.endpoint.OperationInvoker;
+
+/**
+ * An operation on a web endpoint.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class WebEndpointOperation extends EndpointOperation {
+
+ private final OperationRequestPredicate requestPredicate;
+
+ private final String id;
+
+ /**
+ * Creates a new {@code WebEndpointOperation} with the given {@code type}. The
+ * operation can be performed using the given {@code operationInvoker}. The operation
+ * can handle requests that match the given {@code requestPredicate}.
+ * @param type the type of the operation
+ * @param operationInvoker used to perform the operation
+ * @param blocking whether or not this is a blocking operation
+ * @param requestPredicate the predicate for requests that can be handled by the
+ * @param id the id of the operation, unique within its endpoint operation
+ */
+ public WebEndpointOperation(EndpointOperationType type,
+ OperationInvoker operationInvoker, boolean blocking,
+ OperationRequestPredicate requestPredicate, String id) {
+ super(type, operationInvoker, blocking);
+ this.requestPredicate = requestPredicate;
+ this.id = id;
+ }
+
+ /**
+ * Returns the predicate for requests that can be handled by this operation.
+ * @return the predicate
+ */
+ public OperationRequestPredicate getRequestPredicate() {
+ return this.requestPredicate;
+ }
+
+ /**
+ * Returns the ID of the operation that uniquely identifies it within its endpoint.
+ * @return the ID
+ */
+ public String getId() {
+ return this.id;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointResponse.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointResponse.java
new file mode 100644
index 00000000000..ae50b1c14ea
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointResponse.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web;
+
+/**
+ * A {@code WebEndpointResponse} can be returned by an operation on a
+ * {@link WebEndpointExtension} to provide additional, web-specific information such as
+ * the HTTP status code.
+ *
+ * @param the type of the response body
+ * @author Stephane Nicoll
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public final class WebEndpointResponse {
+
+ private final T body;
+
+ private final int status;
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with no body and a 200 (OK) status.
+ */
+ public WebEndpointResponse() {
+ this(null);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with no body and the given
+ * {@code status}.
+ * @param status the HTTP status
+ */
+ public WebEndpointResponse(int status) {
+ this(null, status);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with then given body and a 200 (OK)
+ * status.
+ * @param body the body
+ */
+ public WebEndpointResponse(T body) {
+ this(body, 200);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointResponse} with then given body and status.
+ * @param body the body
+ * @param status the HTTP status
+ */
+ public WebEndpointResponse(T body, int status) {
+ this.body = body;
+ this.status = status;
+ }
+
+ /**
+ * Returns the body for the response.
+ * @return the body
+ */
+ public T getBody() {
+ return this.body;
+ }
+
+ /**
+ * Returns the status for the response.
+ * @return the status
+ */
+ public int getStatus() {
+ return this.status;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java
new file mode 100644
index 00000000000..eb49a5a58ca
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/JerseyEndpointResourceFactory.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web.jersey;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.glassfish.jersey.process.Inflector;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.server.model.Resource.Builder;
+
+import org.springframework.boot.endpoint.EndpointInfo;
+import org.springframework.boot.endpoint.OperationInvoker;
+import org.springframework.boot.endpoint.ParameterMappingException;
+import org.springframework.boot.endpoint.web.EndpointLinksResolver;
+import org.springframework.boot.endpoint.web.Link;
+import org.springframework.boot.endpoint.web.OperationRequestPredicate;
+import org.springframework.boot.endpoint.web.WebEndpointOperation;
+import org.springframework.boot.endpoint.web.WebEndpointResponse;
+import org.springframework.util.CollectionUtils;
+
+/**
+ * A factory for creating Jersey {@link Resource Resources} for web endpoint operations.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class JerseyEndpointResourceFactory {
+
+ private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
+
+ /**
+ * Creates {@link Resource Resources} for the operations of the given
+ * {@code webEndpoints}.
+ * @param endpointPath the path beneath which all endpoints should be mapped
+ * @param webEndpoints the web endpoints
+ * @return the resources for the operations
+ */
+ public Collection createEndpointResources(String endpointPath,
+ Collection> webEndpoints) {
+ List resources = new ArrayList<>();
+ webEndpoints.stream()
+ .flatMap((endpointInfo) -> endpointInfo.getOperations().stream())
+ .map((operation) -> createResource(endpointPath, operation))
+ .forEach(resources::add);
+ resources.add(createEndpointLinksResource(endpointPath, webEndpoints));
+ return resources;
+ }
+
+ private Resource createResource(String endpointPath, WebEndpointOperation operation) {
+ OperationRequestPredicate requestPredicate = operation.getRequestPredicate();
+ Builder resourceBuilder = Resource.builder()
+ .path(endpointPath + "/" + requestPredicate.getPath());
+ resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
+ .consumes(toStringArray(requestPredicate.getConsumes()))
+ .produces(toStringArray(requestPredicate.getProduces()))
+ .handledBy(new EndpointInvokingInflector(operation.getOperationInvoker(),
+ !requestPredicate.getConsumes().isEmpty()));
+ return resourceBuilder.build();
+ }
+
+ private String[] toStringArray(Collection collection) {
+ return collection.toArray(new String[collection.size()]);
+ }
+
+ private Resource createEndpointLinksResource(String endpointPath,
+ Collection> webEndpoints) {
+ Builder resourceBuilder = Resource.builder().path(endpointPath);
+ resourceBuilder.addMethod("GET").handledBy(
+ new EndpointLinksInflector(webEndpoints, this.endpointLinksResolver));
+ return resourceBuilder.build();
+ }
+
+ private static final class EndpointInvokingInflector
+ implements Inflector {
+
+ private final OperationInvoker operationInvoker;
+
+ private final boolean readBody;
+
+ private EndpointInvokingInflector(OperationInvoker operationInvoker,
+ boolean readBody) {
+ this.operationInvoker = operationInvoker;
+ this.readBody = readBody;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Response apply(ContainerRequestContext data) {
+ Map arguments = new HashMap<>();
+ if (this.readBody) {
+ Map body = ((ContainerRequest) data)
+ .readEntity(Map.class);
+ if (body != null) {
+ arguments.putAll(body);
+ }
+ }
+ arguments.putAll(extractPathParmeters(data));
+ arguments.putAll(extractQueryParmeters(data));
+ try {
+ return convertToJaxRsResponse(this.operationInvoker.invoke(arguments),
+ data.getRequest().getMethod());
+ }
+ catch (ParameterMappingException ex) {
+ return Response.status(Status.BAD_REQUEST).build();
+ }
+ }
+
+ private Map extractPathParmeters(
+ ContainerRequestContext requestContext) {
+ return extract(requestContext.getUriInfo().getPathParameters());
+ }
+
+ private Map extractQueryParmeters(
+ ContainerRequestContext requestContext) {
+ return extract(requestContext.getUriInfo().getQueryParameters());
+ }
+
+ private Map extract(
+ MultivaluedMap multivaluedMap) {
+ Map result = new HashMap<>();
+ multivaluedMap.forEach((name, values) -> {
+ if (!CollectionUtils.isEmpty(values)) {
+ result.put(name, values.size() == 1 ? values.get(0) : values);
+ }
+ });
+ return result;
+ }
+
+ private Response convertToJaxRsResponse(Object response, String httpMethod) {
+ if (response == null) {
+ return Response.status(HttpMethod.GET.equals(httpMethod)
+ ? Status.NOT_FOUND : Status.NO_CONTENT).build();
+ }
+ try {
+ if (!(response instanceof WebEndpointResponse)) {
+ return Response.status(Status.OK).entity(convertIfNecessary(response))
+ .build();
+ }
+ WebEndpointResponse> webEndpointResponse = (WebEndpointResponse>) response;
+ return Response.status(webEndpointResponse.getStatus())
+ .entity(convertIfNecessary(webEndpointResponse.getBody()))
+ .build();
+ }
+ catch (IOException ex) {
+ return Response.status(Status.INTERNAL_SERVER_ERROR).build();
+ }
+ }
+
+ private Object convertIfNecessary(Object body) throws IOException {
+ if (body instanceof org.springframework.core.io.Resource) {
+ return ((org.springframework.core.io.Resource) body).getInputStream();
+ }
+ return body;
+ }
+
+ }
+
+ private static final class EndpointLinksInflector
+ implements Inflector {
+
+ private final Collection> endpoints;
+
+ private final EndpointLinksResolver linksResolver;
+
+ private EndpointLinksInflector(
+ Collection> endpoints,
+ EndpointLinksResolver linksResolver) {
+ this.endpoints = endpoints;
+ this.linksResolver = linksResolver;
+ }
+
+ @Override
+ public Response apply(ContainerRequestContext request) {
+ Map links = this.linksResolver.resolveLinks(this.endpoints,
+ request.getUriInfo().getAbsolutePath().toString());
+ return Response.ok(Collections.singletonMap("_links", links)).build();
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/package-info.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/package-info.java
new file mode 100644
index 00000000000..65469b9973c
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/jersey/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Jersey web endpoint support.
+ */
+package org.springframework.boot.endpoint.web.jersey;
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java
new file mode 100644
index 00000000000..77065dd8c1c
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/WebEndpointServletHandlerMapping.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.endpoint.web.mvc;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.boot.endpoint.EndpointInfo;
+import org.springframework.boot.endpoint.OperationInvoker;
+import org.springframework.boot.endpoint.ParameterMappingException;
+import org.springframework.boot.endpoint.web.EndpointLinksResolver;
+import org.springframework.boot.endpoint.web.Link;
+import org.springframework.boot.endpoint.web.OperationRequestPredicate;
+import org.springframework.boot.endpoint.web.WebEndpointOperation;
+import org.springframework.boot.endpoint.web.WebEndpointResponse;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
+import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
+import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
+import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
+
+/**
+ * A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available
+ * over HTTP using Spring MVC.
+ *
+ * @author Andy Wilkinson
+ * @since 2.0.0
+ */
+public class WebEndpointServletHandlerMapping extends RequestMappingInfoHandlerMapping
+ implements InitializingBean {
+
+ private final Method handle = ReflectionUtils.findMethod(OperationHandler.class,
+ "handle", HttpServletRequest.class, Map.class);
+
+ private final Method links = ReflectionUtils.findMethod(
+ WebEndpointServletHandlerMapping.class, "links", HttpServletRequest.class);
+
+ private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
+
+ private final String endpointPath;
+
+ private final Collection> webEndpoints;
+
+ private final CorsConfiguration corsConfiguration;
+
+ /**
+ * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
+ * operations of the given {@code webEndpoints}.
+ * @param endpointPath the path beneath which all endpoints should be mapped
+ * @param collection the web endpoints operations
+ */
+ public WebEndpointServletHandlerMapping(String endpointPath,
+ Collection> collection) {
+ this(endpointPath, collection, null);
+ }
+
+ /**
+ * Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
+ * operations of the given {@code webEndpoints}.
+ * @param endpointPath the path beneath which all endpoints should be mapped
+ * @param webEndpoints the web endpoints
+ * @param corsConfiguration the CORS configuraton for the endpoints
+ */
+ public WebEndpointServletHandlerMapping(String endpointPath,
+ Collection> webEndpoints,
+ CorsConfiguration corsConfiguration) {
+ this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath;
+ this.webEndpoints = webEndpoints;
+ this.corsConfiguration = corsConfiguration;
+ setOrder(-100);
+ }
+
+ @Override
+ protected void initHandlerMethods() {
+ this.webEndpoints.stream()
+ .flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
+ .forEach(this::registerMappingForOperation);
+ registerMapping(new RequestMappingInfo(patternsRequestConditionForPattern(""),
+ new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null,
+ null, null), this, this.links);
+ }
+
+ @Override
+ protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
+ RequestMappingInfo mapping) {
+ return this.corsConfiguration;
+ }
+
+ private void registerMappingForOperation(WebEndpointOperation operation) {
+ registerMapping(createRequestMappingInfo(operation),
+ new OperationHandler(operation.getOperationInvoker()), this.handle);
+ }
+
+ private RequestMappingInfo createRequestMappingInfo(
+ WebEndpointOperation operationInfo) {
+ OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
+ return new RequestMappingInfo(null,
+ patternsRequestConditionForPattern(requestPredicate.getPath()),
+ new RequestMethodsRequestCondition(
+ RequestMethod.valueOf(requestPredicate.getHttpMethod().name())),
+ null, null,
+ new ConsumesRequestCondition(
+ toStringArray(requestPredicate.getConsumes())),
+ new ProducesRequestCondition(
+ toStringArray(requestPredicate.getProduces())),
+ null);
+ }
+
+ private PatternsRequestCondition patternsRequestConditionForPattern(String path) {
+ return new PatternsRequestCondition(
+ new String[] { this.endpointPath
+ + (StringUtils.hasText(path) ? "/" + path : "") },
+ null, null, false, false);
+ }
+
+ private String[] toStringArray(Collection collection) {
+ return collection.toArray(new String[collection.size()]);
+ }
+
+ @Override
+ protected boolean isHandler(Class> beanType) {
+ return false;
+ }
+
+ @Override
+ protected RequestMappingInfo getMappingForMethod(Method method,
+ Class> handlerType) {
+ return null;
+ }
+
+ @Override
+ protected void extendInterceptors(List interceptors) {
+ interceptors.add(new SkipPathExtensionContentNegotiation());
+ }
+
+ @ResponseBody
+ private Map> 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 body) {
+ Map arguments = new HashMap<>((Map) 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(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(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;
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/package-info.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/package-info.java
new file mode 100644
index 00000000000..f2874cd1ff6
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/mvc/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Spring MVC web endpoint support.
+ */
+package org.springframework.boot.endpoint.web.mvc;
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/package-info.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/package-info.java
new file mode 100644
index 00000000000..8c0c6c9d856
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Web endpoint support.
+ */
+package org.springframework.boot.endpoint.web;
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/WebEndpointReactiveHandlerMapping.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/WebEndpointReactiveHandlerMapping.java
new file mode 100644
index 00000000000..9db49432ad9
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/WebEndpointReactiveHandlerMapping.java
@@ -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> 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> 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> 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 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> 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 body) {
+ Map arguments = new HashMap<>((Map) 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(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(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 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);
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/package-info.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/package-info.java
new file mode 100644
index 00000000000..98c77dc6f97
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/reactive/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Reactive web endpoint support.
+ */
+package org.springframework.boot.endpoint.web.reactive;
diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/AnnotationEndpointDiscovererTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/AnnotationEndpointDiscovererTests.java
new file mode 100644
index 00000000000..0b680354fbd
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/AnnotationEndpointDiscovererTests.java
@@ -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> endpoints = mapEndpoints(
+ new TestAnnotationEndpointDiscoverer(context).discoverEndpoints());
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map 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> endpoints = mapEndpoints(
+ new TestAnnotationEndpointDiscoverer(context).discoverEndpoints());
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map 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> endpoints = mapEndpoints(
+ new TestAnnotationEndpointDiscoverer(context,
+ (endpointId) -> new CachingConfiguration(0))
+ .discoverEndpoints());
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map operations = mapOperations(
+ endpoints.get("test"));
+ assertThat(operations).hasSize(3);
+ operations.values()
+ .forEach(operation -> assertThat(operation.getOperationInvoker())
+ .isNotInstanceOf(CachingOperationInvoker.class));
+ });
+ }
+
+ @Test
+ public void endpointMainReadOperationIsNotCachedWithNonMatchingId() {
+ Function cachingConfigurationFactory = (
+ endpointId) -> (endpointId.equals("foo") ? new CachingConfiguration(500)
+ : new CachingConfiguration(0));
+ load(TestEndpointConfiguration.class, (context) -> {
+ Map> endpoints = mapEndpoints(
+ new TestAnnotationEndpointDiscoverer(context,
+ cachingConfigurationFactory).discoverEndpoints());
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map operations = mapOperations(
+ endpoints.get("test"));
+ assertThat(operations).hasSize(3);
+ operations.values()
+ .forEach(operation -> assertThat(operation.getOperationInvoker())
+ .isNotInstanceOf(CachingOperationInvoker.class));
+ });
+ }
+
+ @Test
+ public void endpointMainReadOperationIsCachedWithMatchingId() {
+ Function cachingConfigurationFactory = (
+ endpointId) -> (endpointId.equals("test") ? new CachingConfiguration(500)
+ : new CachingConfiguration(0));
+ load(TestEndpointConfiguration.class, (context) -> {
+ Map> endpoints = mapEndpoints(
+ new TestAnnotationEndpointDiscoverer(context,
+ cachingConfigurationFactory).discoverEndpoints());
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map 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> mapEndpoints(
+ Collection> endpoints) {
+ Map> endpointById = new LinkedHashMap<>();
+ endpoints.forEach((endpoint) -> {
+ EndpointInfo 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 mapOperations(
+ EndpointInfo endpoint) {
+ Map 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 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 {
+
+ TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
+ Function cachingConfigurationFactory) {
+ super(applicationContext, endpointOperationFactory(),
+ TestEndpointOperation::getOperationMethod,
+ cachingConfigurationFactory);
+ }
+
+ TestAnnotationEndpointDiscoverer(ApplicationContext applicationContext) {
+ this(applicationContext, (id) -> null);
+ }
+
+ @Override
+ public Collection> discoverEndpoints() {
+ return discoverEndpointsWithExtension(null, null).stream()
+ .map(EndpointInfoDescriptor::getEndpointInfo)
+ .collect(Collectors.toList());
+ }
+
+ private static EndpointOperationFactory endpointOperationFactory() {
+ return new EndpointOperationFactory() {
+
+ @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;
+ }
+ }
+ };
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/CachingOperationInvokerTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/CachingOperationInvokerTests.java
new file mode 100644
index 00000000000..ec0cca0319b
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/CachingOperationInvokerTests.java
@@ -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 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 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);
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssemblerTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssemblerTests.java
new file mode 100644
index 00000000000..34fc5caacb0
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssemblerTests.java
@@ -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 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 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 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;
+ }
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanRegistrarTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanRegistrarTests.java
new file mode 100644
index 00000000000..1e44c415a35
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanRegistrarTests.java
@@ -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);
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanTests.java
new file mode 100644
index 00000000000..d41d5906b64
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanTests.java
@@ -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 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 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 mapOperations(MBeanInfo info) {
+ Map operations = new HashMap<>();
+ for (MBeanOperationInfo mBeanOperationInfo : info.getOperations()) {
+ operations.put(mBeanOperationInfo.getName(), mBeanOperationInfo);
+ }
+ return operations;
+ }
+
+ private void load(Class> configuration,
+ Consumer 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 all = new LinkedHashMap<>();
+
+ FooEndpoint() {
+ this.all.put(FooName.ONE, new Foo("one"));
+ this.all.put(FooName.TWO, new Foo("two"));
+ }
+
+ @ReadOperation
+ public Collection 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 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;
+ }
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscovererTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscovererTests.java
new file mode 100644
index 00000000000..0090ac27e28
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscovererTests.java
@@ -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> endpoints = discover(
+ discoverer);
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map 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> 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> endpoints = discover(
+ discoverer);
+ assertThat(endpoints).containsOnlyKeys("test");
+ assertJmxTestEndpoint(endpoints.get("test"));
+ });
+ }
+
+ @Test
+ public void jmxEndpointAddsExtraOperation() {
+ load(AdditionalOperationJmxEndpointConfiguration.class, (discoverer) -> {
+ Map> endpoints = discover(
+ discoverer);
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map 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> endpoints = discover(
+ discoverer);
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map 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> endpoints = discover(
+ discoverer);
+ assertThat(endpoints).containsOnlyKeys("test");
+ Map 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 endpoint) {
+ Map 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> discover(
+ JmxAnnotationEndpointDiscoverer discoverer) {
+ Map> endpointsById = new HashMap<>();
+ discoverer.discoverEndpoints()
+ .forEach((endpoint) -> endpointsById.put(endpoint.getId(), endpoint));
+ return endpointsById;
+ }
+
+ private Map mapOperations(
+ Collection operations) {
+ Map operationByName = new HashMap<>();
+ operations.forEach((operation) -> operationByName
+ .put(operation.getOperationName(), operation));
+ return operationByName;
+ }
+
+ private void load(Class> configuration,
+ Consumer consumer) {
+ load(configuration, (id) -> null, consumer);
+ }
+
+ private void load(Class> configuration,
+ Function cachingConfigurationFactory,
+ Consumer 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();
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java
new file mode 100644
index 00000000000..0c4fe80aee1
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java
@@ -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 the type of application context used by the tests
+ * @author Andy Wilkinson
+ */
+public abstract class AbstractWebEndpointIntegrationTests {
+
+ 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 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 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 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