Commit 4f45b2bb authored by Andy Wilkinson's avatar Andy Wilkinson

Merge branch 'endpoint-infrastructure'

parents 7bd285dd 9687a504
......@@ -10,6 +10,7 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes=
org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
......
......@@ -29,6 +29,22 @@
</subpackage>
</subpackage>
<!-- Endpoint infrastructure -->
<subpackage name="endpoint">
<disallow pkg="org.springframework.http" />
<disallow pkg="org.springframework.web" />
<subpackage name="web">
<allow pkg="org.springframework.http" />
<allow pkg="org.springframework.web" />
<subpackage name="mvc">
<disallow pkg="org.springframework.web.reactive" />
</subpackage>
<subpackage name="reactive">
<disallow pkg="org.springframework.web.servlet" />
</subpackage>
</subpackage>
</subpackage>
<!-- Logging -->
<subpackage name="logging">
<disallow pkg="org.springframework.context" />
......@@ -109,4 +125,4 @@
</subpackage>
</subpackage>
</import-control>
</import-control>
\ No newline at end of file
......@@ -159,6 +159,11 @@
<artifactId>jetty-webapp</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
......@@ -271,6 +276,11 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
......@@ -316,6 +326,16 @@
<artifactId>jaybird-jdk18</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
......@@ -352,4 +372,4 @@
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>
\ No newline at end of file
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
/**
* 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;
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
import java.util.Map;
import org.springframework.util.Assert;
/**
* An {@link OperationInvoker} that caches the response of an operation with a
* configurable time to live.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class CachingOperationInvoker implements OperationInvoker {
private final OperationInvoker target;
private final long timeToLive;
private volatile CachedResponse cachedResponse;
/**
* Create a new instance with the target {@link OperationInvoker} to use to compute
* the response and the time to live for the cache.
* @param target the {@link OperationInvoker} this instance wraps
* @param timeToLive the maximum time in milliseconds that a response can be cached
*/
public CachingOperationInvoker(OperationInvoker target, long timeToLive) {
Assert.state(timeToLive > 0, "TimeToLive must be strictly positive");
this.target = target;
this.timeToLive = timeToLive;
}
/**
* Return the maximum time in milliseconds that a response can be cached.
* @return the time to live of a response
*/
public long getTimeToLive() {
return this.timeToLive;
}
@Override
public Object invoke(Map<String, Object> arguments) {
long accessTime = System.currentTimeMillis();
CachedResponse cached = this.cachedResponse;
if (cached == null || cached.isStale(accessTime, this.timeToLive)) {
Object response = this.target.invoke(arguments);
this.cachedResponse = new CachedResponse(response, accessTime);
return response;
}
return cached.getResponse();
}
/**
* A cached response that encapsulates the response itself and the time at which it
* was created.
*/
static class CachedResponse {
private final Object response;
private final long creationTime;
CachedResponse(Object response, long creationTime) {
this.response = response;
this.creationTime = creationTime;
}
public boolean isStale(long accessTime, long timeToLive) {
return (accessTime - this.creationTime) >= timeToLive;
}
public Object getResponse() {
return this.response;
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
import org.springframework.boot.context.properties.bind.convert.BinderConversionService;
import org.springframework.core.convert.ConversionService;
/**
* {@link OperationParameterMapper} that uses a {@link ConversionService} to map parameter
* values if necessary.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public class ConversionServiceOperationParameterMapper
implements OperationParameterMapper {
private final ConversionService conversionService;
/**
* Create a new instance with the {@link ConversionService} to use.
* @param conversionService the conversion service
*/
public ConversionServiceOperationParameterMapper(
ConversionService conversionService) {
this.conversionService = new BinderConversionService(conversionService);
}
@Override
public <T> T mapParameter(Object input, Class<T> parameterType) {
if (input == null || parameterType.isAssignableFrom(input.getClass())) {
return parameterType.cast(input);
}
try {
return this.conversionService.convert(input, parameterType);
}
catch (Exception ex) {
throw new ParameterMappingException(input, parameterType, ex);
}
}
}
/*
* 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;
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
import java.util.Collection;
/**
* Discovers endpoints and provides an {@link EndpointInfo} for each of them.
*
* @param <T> the type of the operation
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface EndpointDiscoverer<T extends EndpointOperation> {
/**
* Perform endpoint discovery.
* @return the discovered endpoints
*/
Collection<EndpointInfo<T>> discoverEndpoints();
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
import java.util.Collection;
/**
* Information describing an endpoint.
*
* @param <T> the type of the endpoint's operations
* @author Andy Wilkinson
* @since 2.0.0
*/
public class EndpointInfo<T extends EndpointOperation> {
private final String id;
private final boolean enabledByDefault;
private final Collection<T> operations;
/**
* Creates a new {@code EndpointInfo} describing an endpoint with the given {@code id}
* and {@code operations}.
* @param id the id of the endpoint
* @param enabledByDefault whether or not the endpoint is enabled by default
* @param operations the operations of the endpoint
*/
public EndpointInfo(String id, boolean enabledByDefault, Collection<T> operations) {
this.id = id;
this.enabledByDefault = enabledByDefault;
this.operations = operations;
}
/**
* Returns the id of the endpoint.
* @return the id
*/
public String getId() {
return this.id;
}
/**
* Returns whether or not this endpoint is enabled by default.
* @return {@code true} if it is enabled by default, otherwise {@code false}
*/
public boolean isEnabledByDefault() {
return this.enabledByDefault;
}
/**
* Returns the operations of the endpoint.
* @return the operations
*/
public Collection<T> getOperations() {
return this.operations;
}
}
/*
* 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;
}
}
/*
* 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
}
/*
* 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
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
import java.util.Map;
/**
* An {@code OperationInvoker} is used to invoke an operation on an endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
@FunctionalInterface
public interface OperationInvoker {
/**
* Invoke the underlying operation using the given {@code arguments}.
* @param arguments the arguments to pass to the operation
* @return the result of the operation, may be {@code null}
*/
Object invoke(Map<String, Object> arguments);
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
/**
* An {@code OperationParameterMapper} is used to map parameters to the required type when
* invoking an endpoint.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
@FunctionalInterface
public interface OperationParameterMapper {
/**
* Map the specified {@code input} parameter to the given {@code parameterType}.
* @param input a parameter value
* @param parameterType the required type of the parameter
* @return a value suitable for that parameter
* @param <T> the actual type of the parameter
* @throws ParameterMappingException when a mapping failure occurs
*/
<T> T mapParameter(Object input, Class<T> parameterType);
}
/*
* 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;
}
}
/*
* 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 {
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.util.ReflectionUtils;
/**
* An {@code OperationInvoker} that invokes an operation using reflection.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class ReflectiveOperationInvoker implements OperationInvoker {
private final OperationParameterMapper parameterMapper;
private final Object target;
private final Method method;
/**
* Creates a new {code ReflectiveOperationInvoker} that will invoke the given
* {@code method} on the given {@code target}. The given {@code parameterMapper} will
* be used to map parameters to the required types.
*
* @param parameterMapper the parameter mapper
* @param target the target of the reflective call
* @param method the method to call
*/
public ReflectiveOperationInvoker(OperationParameterMapper parameterMapper,
Object target, Method method) {
this.parameterMapper = parameterMapper;
this.target = target;
ReflectionUtils.makeAccessible(method);
this.method = method;
}
@Override
public Object invoke(Map<String, Object> arguments) {
return ReflectionUtils.invokeMethod(this.method, this.target,
resolveArguments(arguments));
}
private Object[] resolveArguments(Map<String, Object> arguments) {
return Stream.of(this.method.getParameters())
.map((parameter) -> resolveArgument(parameter, arguments))
.collect(Collectors.collectingAndThen(Collectors.toList(),
(list) -> list.toArray(new Object[list.size()])));
}
private Object resolveArgument(Parameter parameter, Map<String, Object> arguments) {
Object resolved = arguments.get(parameter.getName());
return this.parameterMapper.mapParameter(resolved, parameter.getType());
}
}
/*
* 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 {
}
/*
* 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 {
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.jmx;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.ReflectionException;
import reactor.core.publisher.Mono;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.util.ClassUtils;
/**
* A {@link DynamicMBean} that invokes operations on an {@link EndpointInfo endpoint}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
* @see EndpointMBeanInfoAssembler
*/
public class EndpointMBean implements DynamicMBean {
private static final boolean REACTOR_PRESENT = ClassUtils.isPresent(
"reactor.core.publisher.Mono", EndpointMBean.class.getClassLoader());
private final Function<Object, Object> operationResponseConverter;
private final EndpointMBeanInfo endpointInfo;
EndpointMBean(Function<Object, Object> operationResponseConverter,
EndpointMBeanInfo endpointInfo) {
this.operationResponseConverter = operationResponseConverter;
this.endpointInfo = endpointInfo;
}
/**
* Return the id of the related endpoint.
* @return the endpoint id
*/
public String getEndpointId() {
return this.endpointInfo.getEndpointId();
}
@Override
public MBeanInfo getMBeanInfo() {
return this.endpointInfo.getMbeanInfo();
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature)
throws MBeanException, ReflectionException {
JmxEndpointOperation operationInfo = this.endpointInfo.getOperations()
.get(actionName);
if (operationInfo != null) {
Map<String, Object> arguments = new HashMap<>();
List<JmxEndpointOperationParameterInfo> parameters = operationInfo
.getParameters();
for (int i = 0; i < params.length; i++) {
arguments.put(parameters.get(i).getName(), params[i]);
}
Object result = operationInfo.getOperationInvoker().invoke(arguments);
if (REACTOR_PRESENT) {
result = ReactiveHandler.handle(result);
}
return this.operationResponseConverter.apply(result);
}
throw new ReflectionException(new IllegalArgumentException(
String.format("Endpoint with id '%s' has no operation named %s",
this.endpointInfo.getEndpointId(), actionName)));
}
@Override
public Object getAttribute(String attribute)
throws AttributeNotFoundException, MBeanException, ReflectionException {
throw new AttributeNotFoundException();
}
@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
InvalidAttributeValueException, MBeanException, ReflectionException {
throw new AttributeNotFoundException();
}
@Override
public AttributeList getAttributes(String[] attributes) {
return new AttributeList();
}
@Override
public AttributeList setAttributes(AttributeList attributes) {
return new AttributeList();
}
private static class ReactiveHandler {
public static Object handle(Object result) {
if (result instanceof Mono) {
return ((Mono<?>) result).block();
}
return result;
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.jmx;
import java.util.Map;
import javax.management.MBeanInfo;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperation;
/**
* The {@link MBeanInfo} for a particular {@link EndpointInfo endpoint}. Maps operation
* names to an {@link EndpointOperation}.
*
* @author Stephane Nicoll
* @since 2.0.0
*/
public final class EndpointMBeanInfo {
private final String endpointId;
private final MBeanInfo mBeanInfo;
private final Map<String, JmxEndpointOperation> operations;
public EndpointMBeanInfo(String endpointId, MBeanInfo mBeanInfo,
Map<String, JmxEndpointOperation> operations) {
this.endpointId = endpointId;
this.mBeanInfo = mBeanInfo;
this.operations = operations;
}
public String getEndpointId() {
return this.endpointId;
}
public MBeanInfo getMbeanInfo() {
return this.mBeanInfo;
}
public Map<String, JmxEndpointOperation> getOperations() {
return this.operations;
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.jmx;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanConstructorInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanNotificationInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
/**
* Gathers the management operations of a particular {@link EndpointInfo endpoint}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
*/
class EndpointMBeanInfoAssembler {
private final JmxOperationResponseMapper responseMapper;
EndpointMBeanInfoAssembler(JmxOperationResponseMapper responseMapper) {
this.responseMapper = responseMapper;
}
/**
* Creates the {@link EndpointMBeanInfo} for the specified {@link EndpointInfo
* endpoint}.
* @param endpointInfo the endpoint to handle
* @return the mbean info for the endpoint
*/
EndpointMBeanInfo createEndpointMBeanInfo(
EndpointInfo<JmxEndpointOperation> endpointInfo) {
Map<String, OperationInfos> operationsMapping = getOperationInfo(endpointInfo);
ModelMBeanOperationInfo[] operationsMBeanInfo = operationsMapping.values()
.stream().map((t) -> t.mBeanOperationInfo).collect(Collectors.toList())
.toArray(new ModelMBeanOperationInfo[] {});
Map<String, JmxEndpointOperation> operationsInfo = new LinkedHashMap<>();
operationsMapping.forEach((name, t) -> operationsInfo.put(name, t.operation));
MBeanInfo info = new ModelMBeanInfoSupport(EndpointMBean.class.getName(),
getDescription(endpointInfo), new ModelMBeanAttributeInfo[0],
new ModelMBeanConstructorInfo[0], operationsMBeanInfo,
new ModelMBeanNotificationInfo[0]);
return new EndpointMBeanInfo(endpointInfo.getId(), info, operationsInfo);
}
private String getDescription(EndpointInfo<?> endpointInfo) {
return "MBean operations for endpoint " + endpointInfo.getId();
}
private Map<String, OperationInfos> getOperationInfo(
EndpointInfo<JmxEndpointOperation> endpointInfo) {
Map<String, OperationInfos> operationInfos = new HashMap<>();
endpointInfo.getOperations().forEach((operationInfo) -> {
String name = operationInfo.getOperationName();
ModelMBeanOperationInfo mBeanOperationInfo = new ModelMBeanOperationInfo(
operationInfo.getOperationName(), operationInfo.getDescription(),
getMBeanParameterInfos(operationInfo), this.responseMapper
.mapResponseType(operationInfo.getOutputType()).getName(),
mapOperationType(operationInfo.getType()));
operationInfos.put(name,
new OperationInfos(mBeanOperationInfo, operationInfo));
});
return operationInfos;
}
private MBeanParameterInfo[] getMBeanParameterInfos(JmxEndpointOperation operation) {
return operation.getParameters().stream()
.map((operationParameter) -> new MBeanParameterInfo(
operationParameter.getName(),
operationParameter.getType().getName(),
operationParameter.getDescription()))
.collect(Collectors.collectingAndThen(Collectors.toList(),
(parameterInfos) -> parameterInfos
.toArray(new MBeanParameterInfo[parameterInfos.size()])));
}
private int mapOperationType(EndpointOperationType type) {
if (type == EndpointOperationType.READ) {
return MBeanOperationInfo.INFO;
}
if (type == EndpointOperationType.WRITE) {
return MBeanOperationInfo.ACTION;
}
return MBeanOperationInfo.UNKNOWN;
}
private static class OperationInfos {
private final ModelMBeanOperationInfo mBeanOperationInfo;
private final JmxEndpointOperation operation;
OperationInfos(ModelMBeanOperationInfo mBeanOperationInfo,
JmxEndpointOperation operation) {
this.mBeanOperationInfo = mBeanOperationInfo;
this.operation = operation;
}
}
}
/*
* 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);
}
}
}
/*
* 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;
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.jmx;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.boot.endpoint.AnnotationEndpointDiscoverer;
import org.springframework.boot.endpoint.CachingConfiguration;
import org.springframework.boot.endpoint.CachingOperationInvoker;
import org.springframework.boot.endpoint.Endpoint;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
import org.springframework.boot.endpoint.EndpointType;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.OperationParameterMapper;
import org.springframework.boot.endpoint.ReflectiveOperationInvoker;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.metadata.ManagedOperation;
import org.springframework.jmx.export.metadata.ManagedOperationParameter;
import org.springframework.util.StringUtils;
/**
* Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
* {@link JmxEndpointExtension JMX extensions} applied to them.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxAnnotationEndpointDiscoverer
extends AnnotationEndpointDiscoverer<JmxEndpointOperation, String> {
private static final AnnotationJmxAttributeSource jmxAttributeSource = new AnnotationJmxAttributeSource();
/**
* Creates a new {@link JmxAnnotationEndpointDiscoverer} that will discover
* {@link Endpoint endpoints} and {@link JmxEndpointExtension jmx extensions} using
* the given {@link ApplicationContext}.
*
* @param applicationContext the application context
* @param parameterMapper the {@link OperationParameterMapper} used to convert
* arguments when an operation is invoked
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
*/
public JmxAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
OperationParameterMapper parameterMapper,
Function<String, CachingConfiguration> cachingConfigurationFactory) {
super(applicationContext, new JmxEndpointOperationFactory(parameterMapper),
JmxEndpointOperation::getOperationName, cachingConfigurationFactory);
}
@Override
public Collection<EndpointInfo<JmxEndpointOperation>> discoverEndpoints() {
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors = discoverEndpointsWithExtension(
JmxEndpointExtension.class, EndpointType.JMX);
verifyThatOperationsHaveDistinctName(endpointDescriptors);
return endpointDescriptors.stream().map(EndpointInfoDescriptor::getEndpointInfo)
.collect(Collectors.toList());
}
private void verifyThatOperationsHaveDistinctName(
Collection<EndpointInfoDescriptor<JmxEndpointOperation, String>> endpointDescriptors) {
List<List<JmxEndpointOperation>> clashes = new ArrayList<>();
endpointDescriptors.forEach((descriptor) -> clashes
.addAll(descriptor.findDuplicateOperations().values()));
if (!clashes.isEmpty()) {
StringBuilder message = new StringBuilder();
message.append(
String.format("Found multiple JMX operations with the same name:%n"));
clashes.forEach((clash) -> {
message.append(" ").append(clash.get(0).getOperationName())
.append(String.format(":%n"));
clash.forEach((operation) -> message.append(" ")
.append(String.format("%s%n", operation)));
});
throw new IllegalStateException(message.toString());
}
}
private static class JmxEndpointOperationFactory
implements EndpointOperationFactory<JmxEndpointOperation> {
private final OperationParameterMapper parameterMapper;
JmxEndpointOperationFactory(OperationParameterMapper parameterMapper) {
this.parameterMapper = parameterMapper;
}
@Override
public JmxEndpointOperation createOperation(String endpointId,
AnnotationAttributes operationAttributes, Object target, Method method,
EndpointOperationType type, long timeToLive) {
String operationName = method.getName();
Class<?> outputType = mapParameterType(method.getReturnType());
String description = getDescription(method,
() -> "Invoke " + operationName + " for endpoint " + endpointId);
List<JmxEndpointOperationParameterInfo> parameters = getParameters(method);
OperationInvoker invoker = new ReflectiveOperationInvoker(
this.parameterMapper, target, method);
if (timeToLive > 0) {
invoker = new CachingOperationInvoker(invoker, timeToLive);
}
return new JmxEndpointOperation(type, invoker, operationName, outputType,
description, parameters);
}
private String getDescription(Method method, Supplier<String> fallback) {
ManagedOperation managedOperation = jmxAttributeSource
.getManagedOperation(method);
if (managedOperation != null
&& StringUtils.hasText(managedOperation.getDescription())) {
return managedOperation.getDescription();
}
return fallback.get();
}
private List<JmxEndpointOperationParameterInfo> getParameters(Method method) {
List<JmxEndpointOperationParameterInfo> parameters = new ArrayList<>();
Parameter[] methodParameters = method.getParameters();
if (methodParameters.length == 0) {
return parameters;
}
ManagedOperationParameter[] managedOperationParameters = jmxAttributeSource
.getManagedOperationParameters(method);
if (managedOperationParameters.length > 0) {
for (int i = 0; i < managedOperationParameters.length; i++) {
ManagedOperationParameter mBeanParameter = managedOperationParameters[i];
Parameter methodParameter = methodParameters[i];
parameters.add(new JmxEndpointOperationParameterInfo(
mBeanParameter.getName(),
mapParameterType(methodParameter.getType()),
mBeanParameter.getDescription()));
}
}
else {
for (Parameter parameter : methodParameters) {
parameters.add(
new JmxEndpointOperationParameterInfo(parameter.getName(),
mapParameterType(parameter.getType()), null));
}
}
return parameters;
}
private Class<?> mapParameterType(Class<?> parameter) {
if (parameter.isEnum()) {
return String.class;
}
if (Date.class.isAssignableFrom(parameter)) {
return String.class;
}
if (parameter.getName().startsWith("java.")) {
return parameter;
}
if (parameter.equals(Void.TYPE)) {
return parameter;
}
return Object.class;
}
}
}
/*
* 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();
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.jmx;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.boot.endpoint.EndpointInfo;
/**
* A factory for creating JMX MBeans for endpoint operations.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxEndpointMBeanFactory {
private final EndpointMBeanInfoAssembler assembler;
private final JmxOperationResponseMapper resultMapper;
/**
* Create a new {@link JmxEndpointMBeanFactory} instance that will use the given
* {@code responseMapper} to convert an operation's response to a JMX-friendly form.
* @param responseMapper the response mapper
*/
public JmxEndpointMBeanFactory(JmxOperationResponseMapper responseMapper) {
this.assembler = new EndpointMBeanInfoAssembler(responseMapper);
this.resultMapper = responseMapper;
}
/**
* Creates MBeans for the given {@code endpoints}.
* @param endpoints the endpoints
* @return the MBeans
*/
public Collection<EndpointMBean> createMBeans(
Collection<EndpointInfo<JmxEndpointOperation>> endpoints) {
return endpoints.stream().map((endpointInfo) -> {
EndpointMBeanInfo endpointMBeanInfo = this.assembler
.createEndpointMBeanInfo(endpointInfo);
return new EndpointMBean(this.resultMapper::mapResponse, endpointMBeanInfo);
}).collect(Collectors.toList());
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.jmx;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.endpoint.EndpointOperation;
import org.springframework.boot.endpoint.EndpointOperationType;
import org.springframework.boot.endpoint.OperationInvoker;
/**
* An operation on a JMX endpoint.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JmxEndpointOperation extends EndpointOperation {
private final String operationName;
private final Class<?> outputType;
private final String description;
private final List<JmxEndpointOperationParameterInfo> parameters;
/**
* Creates a new {@code JmxEndpointOperation} for an operation of the given
* {@code type}. The operation can be performed using the given {@code invoker}.
* @param type the type of the operation
* @param invoker used to perform the operation
* @param operationName the name of the operation
* @param outputType the type of the output of the operation
* @param description the description of the operation
* @param parameters the parameters of the operation
*/
public JmxEndpointOperation(EndpointOperationType type, OperationInvoker invoker,
String operationName, Class<?> outputType, String description,
List<JmxEndpointOperationParameterInfo> parameters) {
super(type, invoker, true);
this.operationName = operationName;
this.outputType = outputType;
this.description = description;
this.parameters = parameters;
}
/**
* Returns the name of the operation.
* @return the operation name
*/
public String getOperationName() {
return this.operationName;
}
/**
* Returns the type of the output of the operation.
* @return the output type
*/
public Class<?> getOutputType() {
return this.outputType;
}
/**
* Returns the description of the operation.
* @return the operation description
*/
public String getDescription() {
return this.description;
}
/**
* Returns the parameters of the operation.
* @return the operation parameters
*/
public List<JmxEndpointOperationParameterInfo> getParameters() {
return Collections.unmodifiableList(this.parameters);
}
}
/*
* 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;
}
}
/*
* 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);
}
/*
* 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;
/*
* 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;
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.endpoint.EndpointInfo;
/**
* A resolver for {@link Link links} to web endpoints.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class EndpointLinksResolver {
/**
* Resolves links to the operations of the given {code webEndpoints} based on a
* request with the given {@code requestUrl}.
*
* @param webEndpoints the web endpoints
* @param requestUrl the url of the request for the endpoint links
* @return the links
*/
public Map<String, Link> resolveLinks(
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
String requestUrl) {
String normalizedUrl = normalizeRequestUrl(requestUrl);
Map<String, Link> links = new LinkedHashMap<String, Link>();
links.put("self", new Link(normalizedUrl));
for (EndpointInfo<WebEndpointOperation> endpoint : webEndpoints) {
for (WebEndpointOperation operation : endpoint.getOperations()) {
webEndpoints.stream().map(EndpointInfo::getId).forEach((id) -> links
.put(operation.getId(), createLink(normalizedUrl, operation)));
}
}
return links;
}
private String normalizeRequestUrl(String requestUrl) {
if (requestUrl.endsWith("/")) {
return requestUrl.substring(0, requestUrl.length() - 1);
}
return requestUrl;
}
private Link createLink(String requestUrl, WebEndpointOperation operation) {
String path = operation.getRequestPredicate().getPath();
return new Link(requestUrl + (path.startsWith("/") ? path : "/" + path));
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
import org.springframework.core.style.ToStringCreator;
/**
* Details for a link in a
* <a href="https://tools.ietf.org/html/draft-kelly-json-hal-08">HAL</a>-formatted
* response.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class Link {
private final String href;
private final boolean templated;
/**
* Creates a new {@link Link} with the given {@code href}.
* @param href the href
*/
public Link(String href) {
this.href = href;
this.templated = href.contains("{");
}
/**
* Returns the href of the link.
* @return the href
*/
public String getHref() {
return this.href;
}
/**
* Returns whether or not the {@link #getHref() href} is templated.
* @return {@code true} if the href is templated, otherwise {@code false}
*/
public boolean isTemplated() {
return this.templated;
}
@Override
public String toString() {
return new ToStringCreator(this).append("href", this.href).toString();
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
import java.util.Collection;
import java.util.Collections;
import org.springframework.core.style.ToStringCreator;
/**
* A predicate for a request to an operation on a web endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class OperationRequestPredicate {
private final String path;
private final String canonicalPath;
private final WebEndpointHttpMethod httpMethod;
private final Collection<String> consumes;
private final Collection<String> produces;
/**
* Creates a new {@code WebEndpointRequestPredict}.
*
* @param path the path for the operation
* @param httpMethod the HTTP method that the operation supports
* @param produces the media types that the operation produces
* @param consumes the media types that the operation consumes
*/
public OperationRequestPredicate(String path, WebEndpointHttpMethod httpMethod,
Collection<String> consumes, Collection<String> produces) {
this.path = path;
this.canonicalPath = path.replaceAll("\\{.*?}", "{*}");
this.httpMethod = httpMethod;
this.consumes = consumes;
this.produces = produces;
}
/**
* Returns the path for the operation.
* @return the path
*/
public String getPath() {
return this.path;
}
/**
* Returns the HTTP method for the operation.
* @return the HTTP method
*/
public WebEndpointHttpMethod getHttpMethod() {
return this.httpMethod;
}
/**
* Returns the media types that the operation consumes.
* @return the consumed media types
*/
public Collection<String> getConsumes() {
return Collections.unmodifiableCollection(this.consumes);
}
/**
* Returns the media types that the operation produces.
* @return the produced media types
*/
public Collection<String> getProduces() {
return Collections.unmodifiableCollection(this.produces);
}
@Override
public String toString() {
return new ToStringCreator(this).append("httpMethod", this.httpMethod)
.append("path", this.path).append("consumes", this.consumes)
.append("produces", this.produces).toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.consumes.hashCode();
result = prime * result + this.httpMethod.hashCode();
result = prime * result + this.canonicalPath.hashCode();
result = prime * result + this.produces.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
OperationRequestPredicate other = (OperationRequestPredicate) obj;
if (!this.consumes.equals(other.consumes)) {
return false;
}
if (this.httpMethod != other.httpMethod) {
return false;
}
if (!this.canonicalPath.equals(other.canonicalPath)) {
return false;
}
if (!this.produces.equals(other.produces)) {
return false;
}
return true;
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.reactivestreams.Publisher;
import org.springframework.boot.endpoint.AnnotationEndpointDiscoverer;
import org.springframework.boot.endpoint.CachingConfiguration;
import org.springframework.boot.endpoint.CachingOperationInvoker;
import org.springframework.boot.endpoint.Endpoint;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
import org.springframework.boot.endpoint.EndpointType;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.OperationParameterMapper;
import org.springframework.boot.endpoint.ReflectiveOperationInvoker;
import org.springframework.boot.endpoint.Selector;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
/**
* Discovers the {@link Endpoint endpoints} in an {@link ApplicationContext} with
* {@link WebEndpointExtension web extensions} applied to them.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
*/
public class WebAnnotationEndpointDiscoverer extends
AnnotationEndpointDiscoverer<WebEndpointOperation, OperationRequestPredicate> {
/**
* Creates a new {@link WebAnnotationEndpointDiscoverer} that will discover
* {@link Endpoint endpoints} and {@link WebEndpointExtension web extensions} using
* the given {@link ApplicationContext}.
* @param applicationContext the application context
* @param operationParameterMapper the {@link OperationParameterMapper} used to
* convert arguments when an operation is invoked
* @param cachingConfigurationFactory the {@link CachingConfiguration} factory to use
* @param consumedMediaTypes the media types consumed by web endpoint operations
* @param producedMediaTypes the media types produced by web endpoint operations
*/
public WebAnnotationEndpointDiscoverer(ApplicationContext applicationContext,
OperationParameterMapper operationParameterMapper,
Function<String, CachingConfiguration> cachingConfigurationFactory,
Collection<String> consumedMediaTypes,
Collection<String> producedMediaTypes) {
super(applicationContext,
new WebEndpointOperationFactory(operationParameterMapper,
consumedMediaTypes, producedMediaTypes),
WebEndpointOperation::getRequestPredicate, cachingConfigurationFactory);
}
@Override
public Collection<EndpointInfo<WebEndpointOperation>> discoverEndpoints() {
Collection<EndpointInfoDescriptor<WebEndpointOperation, OperationRequestPredicate>> endpoints = discoverEndpointsWithExtension(
WebEndpointExtension.class, EndpointType.WEB);
verifyThatOperationsHaveDistinctPredicates(endpoints);
return endpoints.stream().map(EndpointInfoDescriptor::getEndpointInfo)
.collect(Collectors.toList());
}
private void verifyThatOperationsHaveDistinctPredicates(
Collection<EndpointInfoDescriptor<WebEndpointOperation, OperationRequestPredicate>> endpointDescriptors) {
List<List<WebEndpointOperation>> clashes = new ArrayList<>();
endpointDescriptors.forEach((descriptor) -> clashes
.addAll(descriptor.findDuplicateOperations().values()));
if (!clashes.isEmpty()) {
StringBuilder message = new StringBuilder();
message.append(String.format(
"Found multiple web operations with matching request predicates:%n"));
clashes.forEach((clash) -> {
message.append(" ").append(clash.get(0).getRequestPredicate())
.append(String.format(":%n"));
clash.forEach((operation) -> message.append(" ")
.append(String.format("%s%n", operation)));
});
throw new IllegalStateException(message.toString());
}
}
private static final class WebEndpointOperationFactory
implements EndpointOperationFactory<WebEndpointOperation> {
private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent(
"org.reactivestreams.Publisher",
WebEndpointOperationFactory.class.getClassLoader());
private final OperationParameterMapper parameterMapper;
private final Collection<String> consumedMediaTypes;
private final Collection<String> producedMediaTypes;
private WebEndpointOperationFactory(OperationParameterMapper parameterMapper,
Collection<String> consumedMediaTypes,
Collection<String> producedMediaTypes) {
this.parameterMapper = parameterMapper;
this.consumedMediaTypes = consumedMediaTypes;
this.producedMediaTypes = producedMediaTypes;
}
@Override
public WebEndpointOperation createOperation(String endpointId,
AnnotationAttributes operationAttributes, Object target, Method method,
EndpointOperationType type, long timeToLive) {
WebEndpointHttpMethod httpMethod = determineHttpMethod(type);
OperationRequestPredicate requestPredicate = new OperationRequestPredicate(
determinePath(endpointId, method), httpMethod,
determineConsumedMediaTypes(httpMethod, method),
determineProducedMediaTypes(method));
OperationInvoker invoker = new ReflectiveOperationInvoker(
this.parameterMapper, target, method);
if (timeToLive > 0) {
invoker = new CachingOperationInvoker(invoker, timeToLive);
}
return new WebEndpointOperation(type, invoker, determineBlocking(method),
requestPredicate, determineId(endpointId, method));
}
private String determinePath(String endpointId, Method operationMethod) {
StringBuilder path = new StringBuilder(endpointId);
Stream.of(operationMethod.getParameters())
.filter((
parameter) -> parameter.getAnnotation(Selector.class) != null)
.map((parameter) -> "/{" + parameter.getName() + "}")
.forEach(path::append);
return path.toString();
}
private String determineId(String endpointId, Method operationMethod) {
StringBuilder path = new StringBuilder(endpointId);
Stream.of(operationMethod.getParameters())
.filter((
parameter) -> parameter.getAnnotation(Selector.class) != null)
.map((parameter) -> "-" + parameter.getName()).forEach(path::append);
return path.toString();
}
private Collection<String> determineConsumedMediaTypes(
WebEndpointHttpMethod httpMethod, Method method) {
if (WebEndpointHttpMethod.POST == httpMethod && consumesRequestBody(method)) {
return this.consumedMediaTypes;
}
return Collections.emptyList();
}
private Collection<String> determineProducedMediaTypes(Method method) {
if (Void.class.equals(method.getReturnType())
|| void.class.equals(method.getReturnType())) {
return Collections.emptyList();
}
if (producesResourceResponseBody(method)) {
return Collections.singletonList("application/octet-stream");
}
return this.producedMediaTypes;
}
private boolean producesResourceResponseBody(Method method) {
if (Resource.class.equals(method.getReturnType())) {
return true;
}
if (WebEndpointResponse.class.isAssignableFrom(method.getReturnType())) {
ResolvableType returnType = ResolvableType.forMethodReturnType(method);
if (ResolvableType.forClass(Resource.class)
.isAssignableFrom(returnType.getGeneric(0))) {
return true;
}
}
return false;
}
private boolean consumesRequestBody(Method method) {
return Stream.of(method.getParameters()).anyMatch(
(parameter) -> parameter.getAnnotation(Selector.class) == null);
}
private WebEndpointHttpMethod determineHttpMethod(
EndpointOperationType operationType) {
if (operationType == EndpointOperationType.WRITE) {
return WebEndpointHttpMethod.POST;
}
return WebEndpointHttpMethod.GET;
}
private boolean determineBlocking(Method method) {
return !REACTIVE_STREAMS_PRESENT
|| !Publisher.class.isAssignableFrom(method.getReturnType());
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.endpoint.Endpoint;
/**
* Identifies a type as being a Web-specific extension of an {@link Endpoint}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 2.0.0
* @see Endpoint
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebEndpointExtension {
/**
* The {@link Endpoint endpoint} class to which this Web extension relates.
* @return the endpoint class
*/
Class<?> endpoint();
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
/**
* An enumeration of HTTP methods supported by web endpoint operations.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public enum WebEndpointHttpMethod {
/**
* An HTTP GET request.
*/
GET,
/**
* An HTTP POST request.
*/
POST
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
import org.springframework.boot.endpoint.EndpointOperation;
import org.springframework.boot.endpoint.EndpointOperationType;
import org.springframework.boot.endpoint.OperationInvoker;
/**
* An operation on a web endpoint.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebEndpointOperation extends EndpointOperation {
private final OperationRequestPredicate requestPredicate;
private final String id;
/**
* Creates a new {@code WebEndpointOperation} with the given {@code type}. The
* operation can be performed using the given {@code operationInvoker}. The operation
* can handle requests that match the given {@code requestPredicate}.
* @param type the type of the operation
* @param operationInvoker used to perform the operation
* @param blocking whether or not this is a blocking operation
* @param requestPredicate the predicate for requests that can be handled by the
* @param id the id of the operation, unique within its endpoint operation
*/
public WebEndpointOperation(EndpointOperationType type,
OperationInvoker operationInvoker, boolean blocking,
OperationRequestPredicate requestPredicate, String id) {
super(type, operationInvoker, blocking);
this.requestPredicate = requestPredicate;
this.id = id;
}
/**
* Returns the predicate for requests that can be handled by this operation.
* @return the predicate
*/
public OperationRequestPredicate getRequestPredicate() {
return this.requestPredicate;
}
/**
* Returns the ID of the operation that uniquely identifies it within its endpoint.
* @return the ID
*/
public String getId() {
return this.id;
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
/**
* A {@code WebEndpointResponse} can be returned by an operation on a
* {@link WebEndpointExtension} to provide additional, web-specific information such as
* the HTTP status code.
*
* @param <T> the type of the response body
* @author Stephane Nicoll
* @author Andy Wilkinson
* @since 2.0.0
*/
public final class WebEndpointResponse<T> {
private final T body;
private final int status;
/**
* Creates a new {@code WebEndpointResponse} with no body and a 200 (OK) status.
*/
public WebEndpointResponse() {
this(null);
}
/**
* Creates a new {@code WebEndpointResponse} with no body and the given
* {@code status}.
* @param status the HTTP status
*/
public WebEndpointResponse(int status) {
this(null, status);
}
/**
* Creates a new {@code WebEndpointResponse} with then given body and a 200 (OK)
* status.
* @param body the body
*/
public WebEndpointResponse(T body) {
this(body, 200);
}
/**
* Creates a new {@code WebEndpointResponse} with then given body and status.
* @param body the body
* @param status the HTTP status
*/
public WebEndpointResponse(T body, int status) {
this.body = body;
this.status = status;
}
/**
* Returns the body for the response.
* @return the body
*/
public T getBody() {
return this.body;
}
/**
* Returns the status for the response.
* @return the status
*/
public int getStatus() {
return this.status;
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web.jersey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.Resource.Builder;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.ParameterMappingException;
import org.springframework.boot.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.endpoint.web.Link;
import org.springframework.boot.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.WebEndpointResponse;
import org.springframework.util.CollectionUtils;
/**
* A factory for creating Jersey {@link Resource Resources} for web endpoint operations.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class JerseyEndpointResourceFactory {
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
/**
* Creates {@link Resource Resources} for the operations of the given
* {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param webEndpoints the web endpoints
* @return the resources for the operations
*/
public Collection<Resource> createEndpointResources(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints) {
List<Resource> resources = new ArrayList<>();
webEndpoints.stream()
.flatMap((endpointInfo) -> endpointInfo.getOperations().stream())
.map((operation) -> createResource(endpointPath, operation))
.forEach(resources::add);
resources.add(createEndpointLinksResource(endpointPath, webEndpoints));
return resources;
}
private Resource createResource(String endpointPath, WebEndpointOperation operation) {
OperationRequestPredicate requestPredicate = operation.getRequestPredicate();
Builder resourceBuilder = Resource.builder()
.path(endpointPath + "/" + requestPredicate.getPath());
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
.consumes(toStringArray(requestPredicate.getConsumes()))
.produces(toStringArray(requestPredicate.getProduces()))
.handledBy(new EndpointInvokingInflector(operation.getOperationInvoker(),
!requestPredicate.getConsumes().isEmpty()));
return resourceBuilder.build();
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
private Resource createEndpointLinksResource(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints) {
Builder resourceBuilder = Resource.builder().path(endpointPath);
resourceBuilder.addMethod("GET").handledBy(
new EndpointLinksInflector(webEndpoints, this.endpointLinksResolver));
return resourceBuilder.build();
}
private static final class EndpointInvokingInflector
implements Inflector<ContainerRequestContext, Object> {
private final OperationInvoker operationInvoker;
private final boolean readBody;
private EndpointInvokingInflector(OperationInvoker operationInvoker,
boolean readBody) {
this.operationInvoker = operationInvoker;
this.readBody = readBody;
}
@SuppressWarnings("unchecked")
@Override
public Response apply(ContainerRequestContext data) {
Map<String, Object> arguments = new HashMap<>();
if (this.readBody) {
Map<String, Object> body = ((ContainerRequest) data)
.readEntity(Map.class);
if (body != null) {
arguments.putAll(body);
}
}
arguments.putAll(extractPathParmeters(data));
arguments.putAll(extractQueryParmeters(data));
try {
return convertToJaxRsResponse(this.operationInvoker.invoke(arguments),
data.getRequest().getMethod());
}
catch (ParameterMappingException ex) {
return Response.status(Status.BAD_REQUEST).build();
}
}
private Map<String, Object> extractPathParmeters(
ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getPathParameters());
}
private Map<String, Object> extractQueryParmeters(
ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getQueryParameters());
}
private Map<String, Object> extract(
MultivaluedMap<String, String> multivaluedMap) {
Map<String, Object> result = new HashMap<>();
multivaluedMap.forEach((name, values) -> {
if (!CollectionUtils.isEmpty(values)) {
result.put(name, values.size() == 1 ? values.get(0) : values);
}
});
return result;
}
private Response convertToJaxRsResponse(Object response, String httpMethod) {
if (response == null) {
return Response.status(HttpMethod.GET.equals(httpMethod)
? Status.NOT_FOUND : Status.NO_CONTENT).build();
}
try {
if (!(response instanceof WebEndpointResponse)) {
return Response.status(Status.OK).entity(convertIfNecessary(response))
.build();
}
WebEndpointResponse<?> webEndpointResponse = (WebEndpointResponse<?>) response;
return Response.status(webEndpointResponse.getStatus())
.entity(convertIfNecessary(webEndpointResponse.getBody()))
.build();
}
catch (IOException ex) {
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
}
}
private Object convertIfNecessary(Object body) throws IOException {
if (body instanceof org.springframework.core.io.Resource) {
return ((org.springframework.core.io.Resource) body).getInputStream();
}
return body;
}
}
private static final class EndpointLinksInflector
implements Inflector<ContainerRequestContext, Response> {
private final Collection<EndpointInfo<WebEndpointOperation>> endpoints;
private final EndpointLinksResolver linksResolver;
private EndpointLinksInflector(
Collection<EndpointInfo<WebEndpointOperation>> endpoints,
EndpointLinksResolver linksResolver) {
this.endpoints = endpoints;
this.linksResolver = linksResolver;
}
@Override
public Response apply(ContainerRequestContext request) {
Map<String, Link> links = this.linksResolver.resolveLinks(this.endpoints,
request.getUriInfo().getAbsolutePath().toString());
return Response.ok(Collections.singletonMap("_links", links)).build();
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Jersey web endpoint support.
*/
package org.springframework.boot.endpoint.web.jersey;
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web.mvc;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.ParameterMappingException;
import org.springframework.boot.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.endpoint.web.Link;
import org.springframework.boot.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.WebEndpointResponse;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
/**
* A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available
* over HTTP using Spring MVC.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebEndpointServletHandlerMapping extends RequestMappingInfoHandlerMapping
implements InitializingBean {
private final Method handle = ReflectionUtils.findMethod(OperationHandler.class,
"handle", HttpServletRequest.class, Map.class);
private final Method links = ReflectionUtils.findMethod(
WebEndpointServletHandlerMapping.class, "links", HttpServletRequest.class);
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final String endpointPath;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
private final CorsConfiguration corsConfiguration;
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param collection the web endpoints operations
*/
public WebEndpointServletHandlerMapping(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointPath, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuraton for the endpoints
*/
public WebEndpointServletHandlerMapping(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath;
this.webEndpoints = webEndpoints;
this.corsConfiguration = corsConfiguration;
setOrder(-100);
}
@Override
protected void initHandlerMethods() {
this.webEndpoints.stream()
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
registerMapping(new RequestMappingInfo(patternsRequestConditionForPattern(""),
new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null,
null, null), this, this.links);
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
private void registerMappingForOperation(WebEndpointOperation operation) {
registerMapping(createRequestMappingInfo(operation),
new OperationHandler(operation.getOperationInvoker()), this.handle);
}
private RequestMappingInfo createRequestMappingInfo(
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
return new RequestMappingInfo(null,
patternsRequestConditionForPattern(requestPredicate.getPath()),
new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name())),
null, null,
new ConsumesRequestCondition(
toStringArray(requestPredicate.getConsumes())),
new ProducesRequestCondition(
toStringArray(requestPredicate.getProduces())),
null);
}
private PatternsRequestCondition patternsRequestConditionForPattern(String path) {
return new PatternsRequestCondition(
new String[] { this.endpointPath
+ (StringUtils.hasText(path) ? "/" + path : "") },
null, null, false, false);
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method,
Class<?> handlerType) {
return null;
}
@Override
protected void extendInterceptors(List<Object> interceptors) {
interceptors.add(new SkipPathExtensionContentNegotiation());
}
@ResponseBody
private Map<String, Map<String, Link>> links(HttpServletRequest request) {
return Collections.singletonMap("_links", this.endpointLinksResolver
.resolveLinks(this.webEndpoints, request.getRequestURL().toString()));
}
/**
* A handler for an endpoint operation.
*/
final class OperationHandler {
private final OperationInvoker operationInvoker;
OperationHandler(OperationInvoker operationInvoker) {
this.operationInvoker = operationInvoker;
}
@SuppressWarnings("unchecked")
@ResponseBody
public Object handle(HttpServletRequest request,
@RequestBody(required = false) Map<String, String> body) {
Map<String, Object> arguments = new HashMap<>((Map<String, String>) request
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());
if (body != null && HttpMethod.POST == httpMethod) {
arguments.putAll(body);
}
request.getParameterMap().forEach((name, values) -> arguments.put(name,
values.length == 1 ? values[0] : Arrays.asList(values)));
try {
return handleResult(this.operationInvoker.invoke(arguments), httpMethod);
}
catch (ParameterMappingException ex) {
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
}
}
private Object handleResult(Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT);
}
if (!(result instanceof WebEndpointResponse)) {
return result;
}
WebEndpointResponse<?> response = (WebEndpointResponse<?>) result;
return new ResponseEntity<Object>(response.getBody(),
HttpStatus.valueOf(response.getStatus()));
}
}
/**
* {@link HandlerInterceptorAdapter} to ensure that
* {@link PathExtensionContentNegotiationStrategy} is skipped for web endpoints.
*/
private static final class SkipPathExtensionContentNegotiation
extends HandlerInterceptorAdapter {
private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
.getName() + ".SKIP";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(SKIP_ATTRIBUTE, Boolean.TRUE);
return true;
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Spring MVC web endpoint support.
*/
package org.springframework.boot.endpoint.web.mvc;
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Web endpoint support.
*/
package org.springframework.boot.endpoint.web;
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web.reactive;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
import org.springframework.boot.endpoint.OperationInvoker;
import org.springframework.boot.endpoint.ParameterMappingException;
import org.springframework.boot.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.endpoint.web.Link;
import org.springframework.boot.endpoint.web.OperationRequestPredicate;
import org.springframework.boot.endpoint.web.WebEndpointOperation;
import org.springframework.boot.endpoint.web.WebEndpointResponse;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.ConsumesRequestCondition;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.condition.ProducesRequestCondition;
import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* A custom {@link RequestMappingInfoHandlerMapping} that makes web endpoints available
* over HTTP using Spring WebFlux.
*
* @author Andy Wilkinson
* @since 2.0.0
*/
public class WebEndpointReactiveHandlerMapping extends RequestMappingInfoHandlerMapping
implements InitializingBean {
private static final PathPatternParser pathPatternParser = new PathPatternParser();
private final Method handleRead = ReflectionUtils
.findMethod(ReadOperationHandler.class, "handle", ServerWebExchange.class);
private final Method handleWrite = ReflectionUtils.findMethod(
WriteOperationHandler.class, "handle", ServerWebExchange.class, Map.class);
private final Method links = ReflectionUtils.findMethod(getClass(), "links",
ServerHttpRequest.class);
private final EndpointLinksResolver endpointLinksResolver = new EndpointLinksResolver();
private final String endpointPath;
private final Collection<EndpointInfo<WebEndpointOperation>> webEndpoints;
private final CorsConfiguration corsConfiguration;
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param collection the web endpoints
*/
public WebEndpointReactiveHandlerMapping(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> collection) {
this(endpointPath, collection, null);
}
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointPath the path beneath which all endpoints should be mapped
* @param webEndpoints the web endpoints
* @param corsConfiguration the CORS configuraton for the endpoints
*/
public WebEndpointReactiveHandlerMapping(String endpointPath,
Collection<EndpointInfo<WebEndpointOperation>> webEndpoints,
CorsConfiguration corsConfiguration) {
this.endpointPath = (endpointPath.startsWith("/") ? "" : "/") + endpointPath;
this.webEndpoints = webEndpoints;
this.corsConfiguration = corsConfiguration;
setOrder(-100);
}
@Override
protected void initHandlerMethods() {
this.webEndpoints.stream()
.flatMap((webEndpoint) -> webEndpoint.getOperations().stream())
.forEach(this::registerMappingForOperation);
registerMapping(new RequestMappingInfo(
new PatternsRequestCondition(pathPatternParser.parse(this.endpointPath)),
new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null,
null, null), this, this.links);
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
private void registerMappingForOperation(WebEndpointOperation operation) {
EndpointOperationType operationType = operation.getType();
registerMapping(createRequestMappingInfo(operation),
operationType == EndpointOperationType.WRITE
? new WriteOperationHandler(operation.getOperationInvoker())
: new ReadOperationHandler(operation.getOperationInvoker()),
operationType == EndpointOperationType.WRITE ? this.handleWrite
: this.handleRead);
}
private RequestMappingInfo createRequestMappingInfo(
WebEndpointOperation operationInfo) {
OperationRequestPredicate requestPredicate = operationInfo.getRequestPredicate();
return new RequestMappingInfo(null,
new PatternsRequestCondition(pathPatternParser
.parse(this.endpointPath + "/" + requestPredicate.getPath())),
new RequestMethodsRequestCondition(
RequestMethod.valueOf(requestPredicate.getHttpMethod().name())),
null, null,
new ConsumesRequestCondition(
toStringArray(requestPredicate.getConsumes())),
new ProducesRequestCondition(
toStringArray(requestPredicate.getProduces())),
null);
}
private String[] toStringArray(Collection<String> collection) {
return collection.toArray(new String[collection.size()]);
}
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method,
Class<?> handlerType) {
return null;
}
@ResponseBody
private Map<String, Map<String, Link>> links(ServerHttpRequest request) {
return Collections.singletonMap("_links",
this.endpointLinksResolver.resolveLinks(this.webEndpoints,
UriComponentsBuilder.fromUri(request.getURI()).replaceQuery(null)
.toUriString()));
}
/**
* Base class for handlers for endpoint operations.
*/
abstract class AbstractOperationHandler {
private final OperationInvoker operationInvoker;
AbstractOperationHandler(OperationInvoker operationInvoker) {
this.operationInvoker = operationInvoker;
}
@SuppressWarnings("unchecked")
ResponseEntity<?> doHandle(ServerWebExchange exchange, Map<String, String> body) {
Map<String, Object> arguments = new HashMap<>((Map<String, String>) exchange
.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE));
if (body != null) {
arguments.putAll(body);
}
exchange.getRequest().getQueryParams().forEach((name, values) -> arguments
.put(name, values.size() == 1 ? values.get(0) : values));
try {
return handleResult(this.operationInvoker.invoke(arguments),
exchange.getRequest().getMethod());
}
catch (ParameterMappingException ex) {
return new ResponseEntity<Void>(HttpStatus.BAD_REQUEST);
}
}
private ResponseEntity<?> handleResult(Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(httpMethod == HttpMethod.GET
? HttpStatus.NOT_FOUND : HttpStatus.NO_CONTENT);
}
if (!(result instanceof WebEndpointResponse)) {
return new ResponseEntity<>(result, HttpStatus.OK);
}
WebEndpointResponse<?> response = (WebEndpointResponse<?>) result;
return new ResponseEntity<Object>(response.getBody(),
HttpStatus.valueOf(response.getStatus()));
}
}
/**
* A handler for an endpoint write operation.
*/
final class WriteOperationHandler extends AbstractOperationHandler {
WriteOperationHandler(OperationInvoker operationInvoker) {
super(operationInvoker);
}
@ResponseBody
public ResponseEntity<?> handle(ServerWebExchange exchange,
@RequestBody(required = false) Map<String, String> body) {
return doHandle(exchange, body);
}
}
/**
* A handler for an endpoint write operation.
*/
final class ReadOperationHandler extends AbstractOperationHandler {
ReadOperationHandler(OperationInvoker operationInvoker) {
super(operationInvoker);
}
@ResponseBody
public ResponseEntity<?> handle(ServerWebExchange exchange) {
return doHandle(exchange, null);
}
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Reactive web endpoint support.
*/
package org.springframework.boot.endpoint.web.reactive;
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link CachingOperationInvoker}.
*
* @author Stephane Nicoll
*/
public class CachingOperationInvokerTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void createInstanceWithTllSetToZero() {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("TimeToLive");
new CachingOperationInvoker(mock(OperationInvoker.class), 0);
}
@Test
public void cacheInTtlRange() {
Object expected = new Object();
OperationInvoker target = mock(OperationInvoker.class);
Map<String, Object> parameters = new HashMap<>();
given(target.invoke(parameters)).willReturn(expected);
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 500L);
Object response = invoker.invoke(parameters);
assertThat(response).isSameAs(expected);
verify(target, times(1)).invoke(parameters);
Object cachedResponse = invoker.invoke(parameters);
assertThat(cachedResponse).isSameAs(response);
verifyNoMoreInteractions(target);
}
@Test
public void targetInvokedWhenCacheExpires() throws InterruptedException {
OperationInvoker target = mock(OperationInvoker.class);
Map<String, Object> parameters = new HashMap<>();
given(target.invoke(parameters)).willReturn(new Object());
CachingOperationInvoker invoker = new CachingOperationInvoker(target, 50L);
invoker.invoke(parameters);
Thread.sleep(55);
invoker.invoke(parameters);
verify(target, times(2)).invoke(parameters);
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.jmx;
import java.util.Collections;
import java.util.Map;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import org.junit.Test;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
import org.springframework.boot.endpoint.OperationInvoker;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link EndpointMBeanInfoAssembler}.
*
* @author Stephane Nicoll
*/
public class EndpointMBeanInfoAssemblerTests {
private final EndpointMBeanInfoAssembler mBeanInfoAssembler = new EndpointMBeanInfoAssembler(
new DummyOperationResponseMapper());
@Test
public void exposeSimpleReadOperation() {
JmxEndpointOperation operation = new JmxEndpointOperation(
EndpointOperationType.READ, new DummyOperationInvoker(), "getAll",
Object.class, "Test operation", Collections.emptyList());
EndpointInfo<JmxEndpointOperation> endpoint = new EndpointInfo<>("test", true,
Collections.singletonList(operation));
EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler
.createEndpointMBeanInfo(endpoint);
assertThat(endpointMBeanInfo).isNotNull();
assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("test");
assertThat(endpointMBeanInfo.getOperations())
.containsOnly(entry("getAll", operation));
MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo();
assertThat(mbeanInfo).isNotNull();
assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName());
assertThat(mbeanInfo.getDescription())
.isEqualTo("MBean operations for endpoint test");
assertThat(mbeanInfo.getAttributes()).isEmpty();
assertThat(mbeanInfo.getNotifications()).isEmpty();
assertThat(mbeanInfo.getConstructors()).isEmpty();
assertThat(mbeanInfo.getOperations()).hasSize(1);
MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0];
assertThat(mBeanOperationInfo.getName()).isEqualTo("getAll");
assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName());
assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.INFO);
assertThat(mBeanOperationInfo.getSignature()).hasSize(0);
}
@Test
public void exposeSimpleWriteOperation() {
JmxEndpointOperation operation = new JmxEndpointOperation(
EndpointOperationType.WRITE, new DummyOperationInvoker(), "update",
Object.class, "Update operation",
Collections.singletonList(new JmxEndpointOperationParameterInfo("test",
String.class, "Test argument")));
EndpointInfo<JmxEndpointOperation> endpoint = new EndpointInfo<>("another", true,
Collections.singletonList(operation));
EndpointMBeanInfo endpointMBeanInfo = this.mBeanInfoAssembler
.createEndpointMBeanInfo(endpoint);
assertThat(endpointMBeanInfo).isNotNull();
assertThat(endpointMBeanInfo.getEndpointId()).isEqualTo("another");
assertThat(endpointMBeanInfo.getOperations())
.containsOnly(entry("update", operation));
MBeanInfo mbeanInfo = endpointMBeanInfo.getMbeanInfo();
assertThat(mbeanInfo).isNotNull();
assertThat(mbeanInfo.getClassName()).isEqualTo(EndpointMBean.class.getName());
assertThat(mbeanInfo.getDescription())
.isEqualTo("MBean operations for endpoint another");
assertThat(mbeanInfo.getAttributes()).isEmpty();
assertThat(mbeanInfo.getNotifications()).isEmpty();
assertThat(mbeanInfo.getConstructors()).isEmpty();
assertThat(mbeanInfo.getOperations()).hasSize(1);
MBeanOperationInfo mBeanOperationInfo = mbeanInfo.getOperations()[0];
assertThat(mBeanOperationInfo.getName()).isEqualTo("update");
assertThat(mBeanOperationInfo.getReturnType()).isEqualTo(Object.class.getName());
assertThat(mBeanOperationInfo.getImpact()).isEqualTo(MBeanOperationInfo.ACTION);
assertThat(mBeanOperationInfo.getSignature()).hasSize(1);
MBeanParameterInfo mBeanParameterInfo = mBeanOperationInfo.getSignature()[0];
assertThat(mBeanParameterInfo.getName()).isEqualTo("test");
assertThat(mBeanParameterInfo.getType()).isEqualTo(String.class.getName());
assertThat(mBeanParameterInfo.getDescription()).isEqualTo("Test argument");
}
private static class DummyOperationInvoker implements OperationInvoker {
@Override
public Object invoke(Map<String, Object> arguments) {
return null;
}
}
private static class DummyOperationResponseMapper
implements JmxOperationResponseMapper {
@Override
public Object mapResponse(Object response) {
return response;
}
@Override
public Class<?> mapResponseType(Class<?> responseType) {
return responseType;
}
}
}
/*
* 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);
}
}
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.endpoint.web;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import org.assertj.core.api.Condition;
import org.junit.Test;
import org.springframework.boot.endpoint.EndpointInfo;
import org.springframework.boot.endpoint.EndpointOperationType;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link EndpointLinksResolver}.
*
* @author Andy Wilkinson
*/
public class EndpointLinksResolverTests {
private final EndpointLinksResolver linksResolver = new EndpointLinksResolver();
@Test
public void linkResolutionWithTrailingSlashStripsSlashOnSelfLink() {
Map<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(),
"https://api.example.com/application/");
assertThat(links).hasSize(1);
assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/application"));
}
@Test
public void linkResolutionWithoutTrailingSlash() {
Map<String, Link> links = this.linksResolver.resolveLinks(Collections.emptyList(),
"https://api.example.com/application");
assertThat(links).hasSize(1);
assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/application"));
}
@Test
public void resolvedLinksContainsALinkForEachEndpointOperation() {
Map<String, Link> links = this.linksResolver
.resolveLinks(
Arrays.asList(new EndpointInfo<>("alpha", true,
Arrays.asList(operationWithPath("/alpha", "alpha"),
operationWithPath("/alpha/{name}",
"alpha-name")))),
"https://api.example.com/application");
assertThat(links).hasSize(3);
assertThat(links).hasEntrySatisfying("self",
linkWithHref("https://api.example.com/application"));
assertThat(links).hasEntrySatisfying("alpha",
linkWithHref("https://api.example.com/application/alpha"));
assertThat(links).hasEntrySatisfying("alpha-name",
linkWithHref("https://api.example.com/application/alpha/{name}"));
}
private WebEndpointOperation operationWithPath(String path, String id) {
return new WebEndpointOperation(EndpointOperationType.READ, null, false,
new OperationRequestPredicate(path, WebEndpointHttpMethod.GET,
Collections.emptyList(), Collections.emptyList()),
id);
}
private Condition<Link> linkWithHref(String href) {
return new Condition<>((link) -> href.equals(link.getHref()),
"Link with href '%s'", href);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment