com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter Maven / Gradle / Ivy
Show all versions of jsonrpc4j Show documentation
package com.googlecode.jsonrpc4j.spring;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.jsonrpc4j.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static org.springframework.util.ClassUtils.forName;
import static org.springframework.util.ClassUtils.getAllInterfacesForClass;
/**
*
* This class can be instantiated in a spring context in order to simplify the configuration of JSON-RPC
* services afforded by beans in the same context. The services to be configured are identified
* by the annotation {@link AutoJsonRpcServiceImpl} on the implementation of the service. Such
* implementation beans must also have the {@link JsonRpcService} annotation associated with them; either
* on the implementation class itself or, preferably, on an interface that the implementation implements.
*
* The path for exposing the service is obtained from {@link JsonRpcService#value()}, but it is also
* possible to define additional paths on {@link AutoJsonRpcServiceImpl#additionalPaths()}.
* Below is an example of spring context XML snippet that illustrates typical usage;
*
* <bean class="com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter"/>
* <bean class="MyServiceBean"/>
*
* Note that this class replaces {@link AutoJsonRpcServiceExporter}. See that class' javadoc
* for details.
*/
@SuppressWarnings("unused")
public class AutoJsonRpcServiceImplExporter implements BeanFactoryPostProcessor {
private static final Logger logger = LoggerFactory.getLogger(AutoJsonRpcServiceImplExporter.class);
private static final String PATH_PREFIX = "/";
private static final Pattern PATTERN_JSONRPC_PATH = Pattern.compile("^/?[A-Za-z0-9._~-]+(/[A-Za-z0-9._~-]+)*$");
private ObjectMapper objectMapper;
private ErrorResolver errorResolver = null;
private Boolean registerTraceInterceptor;
private boolean backwardsCompatible = true;
private boolean rethrowExceptions = false;
private boolean allowExtraParams = false;
private boolean allowLessParams = false;
private boolean shouldLogInvocationErrors = true;
private InvocationListener invocationListener = null;
private HttpStatusCodeProvider httpStatusCodeProvider = null;
private ConvertedParameterTransformer convertedParameterTransformer = null;
private String contentType = null;
private List interceptorList = null;
private ExecutorService batchExecutorService = null;
private long parallelBatchProcessingTimeout;
/**
* Finds the beans to expose.
*
* Searches parent factories as well.
*/
private static Map findServiceBeanDefinitions(ConfigurableListableBeanFactory beanFactory) {
final Map serviceBeanNames = new HashMap<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
AutoJsonRpcServiceImpl autoJsonRpcServiceImplAnnotation = beanFactory.findAnnotationOnBean(beanName, AutoJsonRpcServiceImpl.class);
JsonRpcService jsonRpcServiceAnnotation = beanFactory.findAnnotationOnBean(beanName, JsonRpcService.class);
if (null != autoJsonRpcServiceImplAnnotation) {
if (null == jsonRpcServiceAnnotation) {
throw new IllegalStateException("on the bean [" + beanName + "], @" +
AutoJsonRpcServiceImpl.class.getSimpleName() + " was found, but not @" +
JsonRpcService.class.getSimpleName() + " -- both are required");
}
List paths = new ArrayList<>();
Collections.addAll(paths, autoJsonRpcServiceImplAnnotation.additionalPaths());
paths.add(jsonRpcServiceAnnotation.value());
for (String path : paths) {
if (!PATTERN_JSONRPC_PATH.matcher(path).matches()) {
throw new RuntimeException("the path [" + path + "] for the bean [" + beanName + "] is not valid");
}
logger.info("exporting bean [{}] ---> [{}]", beanName, path);
if (isNotDuplicateService(serviceBeanNames, beanName, path)) {
serviceBeanNames.put(path, beanName);
}
}
}
}
collectFromParentBeans(beanFactory, serviceBeanNames);
return serviceBeanNames;
}
@SuppressWarnings("Convert2streamapi")
private static void collectFromParentBeans(ConfigurableListableBeanFactory beanFactory, Map serviceBeanNames) {
BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory();
if (parentBeanFactory != null && ConfigurableListableBeanFactory.class.isInstance(parentBeanFactory)) {
for (Entry entry : findServiceBeanDefinitions((ConfigurableListableBeanFactory) parentBeanFactory).entrySet()) {
if (isNotDuplicateService(serviceBeanNames, entry.getKey(), entry.getValue())) {
serviceBeanNames.put(entry.getKey(), entry.getValue());
}
}
}
}
private static boolean isNotDuplicateService(Map serviceBeanNames, String beanName, String pathValue) {
if (serviceBeanNames.containsKey(pathValue)) {
String otherBeanName = serviceBeanNames.get(pathValue);
logger.debug("Duplicate JSON-RPC path specification: found {} on both [{}] and [{}].", pathValue, beanName, otherBeanName);
return false;
}
return true;
}
private static boolean hasServiceAnnotation(JsonRpcService jsonRpcPath) {
return jsonRpcPath != null;
}
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
Map servicePathToBeanName = findServiceBeanDefinitions(defaultListableBeanFactory);
for (Entry entry : servicePathToBeanName.entrySet()) {
registerServiceProxy(defaultListableBeanFactory, makeUrlPath(entry.getKey()), entry.getValue());
}
}
/**
* To make the
* {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping}
* export a bean automatically, the name should start with a '/'.
*/
private String makeUrlPath(String servicePath) {
if (null == servicePath || 0 == servicePath.length()) {
throw new IllegalArgumentException("the service path must be provided");
}
if ('/' == servicePath.charAt(0)) {
return servicePath;
}
return PATH_PREFIX.concat(servicePath);
}
/**
* Registers the new beans with the bean factory.
*/
private void registerServiceProxy(DefaultListableBeanFactory defaultListableBeanFactory, String servicePath, String serviceBeanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(JsonServiceExporter.class).addPropertyReference("service", serviceBeanName);
BeanDefinition serviceBeanDefinition = findBeanDefinition(defaultListableBeanFactory, serviceBeanName);
for (Class> currentInterface : getBeanInterfaces(serviceBeanDefinition, defaultListableBeanFactory.getBeanClassLoader())) {
if (currentInterface.isAnnotationPresent(JsonRpcService.class)) {
String serviceInterface = currentInterface.getName();
logger.debug("Registering interface '{}' for JSON-RPC bean [{}].", serviceInterface, serviceBeanName);
builder.addPropertyValue("serviceInterface", serviceInterface);
break;
}
}
if (objectMapper != null) {
builder.addPropertyValue("objectMapper", objectMapper);
}
if (errorResolver != null) {
builder.addPropertyValue("errorResolver", errorResolver);
}
if (invocationListener != null) {
builder.addPropertyValue("invocationListener", invocationListener);
}
if (registerTraceInterceptor != null) {
builder.addPropertyValue("registerTraceInterceptor", registerTraceInterceptor);
}
if (httpStatusCodeProvider != null) {
builder.addPropertyValue("httpStatusCodeProvider", httpStatusCodeProvider);
}
if (convertedParameterTransformer != null) {
builder.addPropertyValue("convertedParameterTransformer", convertedParameterTransformer);
}
if (contentType != null) {
builder.addPropertyValue("contentType", contentType);
}
if (interceptorList != null) {
builder.addPropertyValue("interceptorList", interceptorList);
}
if (batchExecutorService != null) {
builder.addPropertyValue("batchExecutorService", batchExecutorService);
}
builder.addPropertyValue("backwardsCompatible", backwardsCompatible);
builder.addPropertyValue("rethrowExceptions", rethrowExceptions);
builder.addPropertyValue("allowExtraParams", allowExtraParams);
builder.addPropertyValue("allowLessParams", allowLessParams);
builder.addPropertyValue("shouldLogInvocationErrors", shouldLogInvocationErrors);
defaultListableBeanFactory.registerBeanDefinition(servicePath, builder.getBeanDefinition());
}
/**
* Find a {@link BeanDefinition} in the {@link BeanFactory} or it's parents.
*/
private BeanDefinition findBeanDefinition(ConfigurableListableBeanFactory beanFactory, String serviceBeanName) {
if (beanFactory.containsLocalBean(serviceBeanName)) {
return beanFactory.getBeanDefinition(serviceBeanName);
}
BeanFactory parentBeanFactory = beanFactory.getParentBeanFactory();
if (parentBeanFactory != null && ConfigurableListableBeanFactory.class.isInstance(parentBeanFactory)) {
return findBeanDefinition((ConfigurableListableBeanFactory) parentBeanFactory, serviceBeanName);
}
throw new NoSuchBeanDefinitionException(serviceBeanName);
}
private Class>[] getBeanInterfaces(BeanDefinition serviceBeanDefinition, ClassLoader beanClassLoader) {
String beanClassName = serviceBeanDefinition.getBeanClassName();
try {
Class> beanClass = forName(beanClassName, beanClassLoader);
return getAllInterfacesForClass(beanClass, beanClassLoader);
} catch (ClassNotFoundException | LinkageError e) {
throw new IllegalStateException(format("Cannot find bean class '%s'.", beanClassName), e);
}
}
/**
* @param objectMapper the objectMapper to set
*/
public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* @param errorResolver the errorResolver to set
*/
public void setErrorResolver(ErrorResolver errorResolver) {
this.errorResolver = errorResolver;
}
/**
* @param backwardsCompatible the backwardsCompatible to set
*/
public void setBackwardsCompatible(boolean backwardsCompatible) {
this.backwardsCompatible = backwardsCompatible;
}
/**
* @param rethrowExceptions the rethrowExceptions to set
*/
public void setRethrowExceptions(boolean rethrowExceptions) {
this.rethrowExceptions = rethrowExceptions;
}
/**
* @param allowExtraParams the allowExtraParams to set
*/
public void setAllowExtraParams(boolean allowExtraParams) {
this.allowExtraParams = allowExtraParams;
}
/**
* @param allowLessParams the allowLessParams to set
*/
public void setAllowLessParams(boolean allowLessParams) {
this.allowLessParams = allowLessParams;
}
/**
* See {@link org.springframework.remoting.support.RemoteExporter#setRegisterTraceInterceptor(boolean)}
*
* @param registerTraceInterceptor the registerTraceInterceptor value to set
*/
public void setRegisterTraceInterceptor(boolean registerTraceInterceptor) {
this.registerTraceInterceptor = registerTraceInterceptor;
}
/**
* @param invocationListener the invocationListener to set
*/
public void setInvocationListener(InvocationListener invocationListener) {
this.invocationListener = invocationListener;
}
/**
* @param httpStatusCodeProvider the HttpStatusCodeProvider to set
*/
public void setHttpStatusCodeProvider(HttpStatusCodeProvider httpStatusCodeProvider) {
this.httpStatusCodeProvider = httpStatusCodeProvider;
}
/**
* @param convertedParameterTransformer the convertedParameterTransformer to set
*/
public void setConvertedParameterTransformer(ConvertedParameterTransformer convertedParameterTransformer) {
this.convertedParameterTransformer = convertedParameterTransformer;
}
public void setShouldLogInvocationErrors(boolean shouldLogInvocationErrors) {
this.shouldLogInvocationErrors = shouldLogInvocationErrors;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public void setInterceptorList(List interceptorList) {
if (interceptorList == null || interceptorList.isEmpty()) {
throw new IllegalArgumentException("Interceptor list must be not null and not empty");
}
this.interceptorList = interceptorList;
}
/**
* @param batchExecutorService the {@link ExecutorService} to set
*/
public void setBatchExecutorService(ExecutorService batchExecutorService) {
this.batchExecutorService = batchExecutorService;
}
/**
* @param parallelBatchProcessingTimeout timeout used for parallel batch processing
*/
public void setParallelBatchProcessingTimeout(long parallelBatchProcessingTimeout) {
this.parallelBatchProcessingTimeout = parallelBatchProcessingTimeout;
}
}