package com.github.ggeorgovassilis.springjsonmapper;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import com.github.ggeorgovassilis.springjsonmapper.jaxrs.JaxRsInvokerProxyFactoryBean;
import com.github.ggeorgovassilis.springjsonmapper.model.MappingDeclarationException;
import com.github.ggeorgovassilis.springjsonmapper.model.MethodParameterDescriptor;
import com.github.ggeorgovassilis.springjsonmapper.model.MethodParameterDescriptor.Type;
import com.github.ggeorgovassilis.springjsonmapper.model.UrlMapping;
import com.github.ggeorgovassilis.springjsonmapper.spring.SpringRestInvokerProxyFactoryBean;
import com.github.ggeorgovassilis.springjsonmapper.utils.CglibProxyFactory;
import com.github.ggeorgovassilis.springjsonmapper.utils.DynamicJavaProxyFactory;
import com.github.ggeorgovassilis.springjsonmapper.utils.ProxyFactory;
/**
* Base component for proxy factories that bind java interfaces to a remote REST
* service. For more information look up
* {@link SpringRestInvokerProxyFactoryBean} and {@link JaxRsInvokerProxyFactoryBean}
*
* Will generate by default dynamic java proxies. Use {@link #setProxyTargetClass(ClassLoader, Class)} or {@link #setProxyTargetClass(Class)}
* in order to generate proxies extending a concrete class.
* @see JaxRsInvokerProxyFactoryBean
* @see SpringRestInvokerProxyFactoryBean
* @author george georgovassilis
* @author Maxime Guennec
*
*/
public abstract class BaseRestInvokerProxyFactoryBean implements
FactoryBean<Object>, InvocationHandler, EmbeddedValueResolverAware {
protected Class<?> remoteServiceInterfaceClass;
protected String remoteServiceInterfaceClassName;
protected Object remoteProxy;
protected String baseUrl;
protected RestOperations restTemplate;
protected MethodInspector methodInspector;
protected StringValueResolver expressionResolver;
protected ProxyFactory proxyFactory = new DynamicJavaProxyFactory();
/**
* Return an implementation of a {@link MethodInspector} which can look at
* methods and return a {@link RequestMapping} describing how that method is
* to be mapped to a URL of a remote REST service.
*
* @return
*/
protected abstract MethodInspector constructDefaultMethodInspector();
/**
* Implementations inspect a method and return the corresponding
* {@link RequestMapping}.
*
* @param method
* @param args
* method arguments
* @return Must always return a {@link RequestMapping}. If there's a problem
* then implementations must throw an exception instead of returning
* null.
*/
protected UrlMapping getRequestMapping(Method method, Object[] args) {
return methodInspector.inspect(method, args);
}
/**
* Specify the class to extend
*
* @param classLoader
* Classloader to use
* @param c
* Proxies will extend this base class
*/
public void setProxyTargetClass(ClassLoader classLoader, Class<?> c) {
proxyFactory = new CglibProxyFactory(classLoader, c);
}
/**
* Specify class to derive proxies from
*
* @param c
* Base class. Will use this class' classloader
*/
public void setProxyTargetClass(Class<?> c) {
setProxyTargetClass(c.getClassLoader(), c);
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.expressionResolver = resolver;
}
public MethodInspector getMethodInspector() {
return methodInspector;
}
/**
* Override the default method inspector provided by the extending
* implementation.
*
* @param methodInspector
*/
public void setMethodInspector(MethodInspector methodInspector) {
this.methodInspector = methodInspector;
}
protected RestOperations getRestTemplate() {
return restTemplate;
}
/**
* Optionally provide a {@link RestTemplate} if you need to handle http
* yourself (proxies? BASIC auth?)
*
* @param restTemplate
*/
public void setRestTemplate(RestOperations restTemplate) {
this.restTemplate = restTemplate;
}
public String getBaseUrl() {
return baseUrl;
}
/**
* Set the base URL of the remote REST service for HTTP requests. Further
* mappings specified on service interfaces are resolved relatively to this
* URL.
*
* @param baseUrl
*/
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public Class<?> getRemoteServiceInterfaceClass() {
return remoteServiceInterfaceClass;
}
/**
* Set the class of the remote service interface. Use either this setter or
* {@link #setRemoteServiceInterfaceClassName(String)}
*
* @param c
*/
public void setRemoteServiceInterfaceClass(Class<?> c) {
remoteServiceInterfaceClass = c;
}
/**
* Set the absolute class name of the remote service interface to map to the
* remote REST service. Use either this setter or
* {@link #setRemoteServiceInterfaceClass(Class)}
*
* @param className
*/
public void setRemoteServiceInterfaceClassName(String className) {
this.remoteServiceInterfaceClassName = className;
}
protected RestTemplate constructDefaultRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
/**
* If instantiating this object programmatically then, after setting any
* dependencies, call this method to finish object initialization. Spring
* will normally do that in an application context.
*/
@PostConstruct
public void initialize() {
if (remoteServiceInterfaceClass == null) {
if (remoteServiceInterfaceClassName == null)
throw new IllegalArgumentException(
"Must provide the remote service interface class or classname.");
try {
remoteServiceInterfaceClass = getClass().getClassLoader()
.loadClass(remoteServiceInterfaceClassName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
if (methodInspector == null)
methodInspector = constructDefaultMethodInspector();
if (restTemplate == null)
restTemplate = constructDefaultRestTemplate();
}
@Override
public synchronized Object getObject() throws Exception {
if (remoteProxy == null) {
remoteProxy = proxyFactory.createProxy(getClass().getClassLoader(),
new Class[] { getRemoteServiceInterfaceClass() }, this);
proxyFactory = null;
}
return remoteProxy;
}
@Override
public Class<?> getObjectType() {
return getRemoteServiceInterfaceClass();
}
@Override
public boolean isSingleton() {
return true;
}
protected HttpMethod mapStringToHttpMethod(String method) {
return HttpMethod.valueOf(method);
}
protected Object handleRemoteInvocation(Object proxy, Method method,
Object[] args, UrlMapping requestMapping) {
Object result = null;
Map<String, Object> parameters = new LinkedHashMap<>();
Map<String, Object> dataObjects = new LinkedHashMap<>();
MultiValueMap<String, String> headers = getHeaders(requestMapping);
Map<String, String> cookies = new LinkedHashMap<>();
MultiValueMap<String, Object> formObjects = new LinkedMultiValueMap<>();
RestOperations rest = getRestTemplate();
Class<?> returnType = method.getReturnType();
String url = baseUrl;
url += requestMapping.getUrl();
HttpMethod httpMethod = requestMapping.getHttpMethod();
UrlMapping urlMapping = methodInspector.inspect(method, args);
for (MethodParameterDescriptor descriptor : urlMapping.getParameters()) {
if (descriptor.getType().equals(Type.httpParameter)
&& !urlMapping.hasRequestBody(descriptor.getName())) {
if (parameters.containsKey(descriptor.getName()))
throw new MappingDeclarationException(
"Duplicate parameter " + descriptor.getName()
+ " on " + method, method, null, -1);
parameters.put(descriptor.getName(), descriptor.getValue());
url = appendDescriptorNameParameterToUrl(url, descriptor);
} else if (descriptor.getType().equals(Type.cookie)) {
cookies.put(descriptor.getName(),
(String) descriptor.getValue());
} else if (descriptor.getType().equals(Type.pathVariable)) {
url = replacePathVariableDescriptorInUrl(url, descriptor);
} else if (descriptor.getType().equals(Type.requestBody)) {
if (dataObjects.containsKey(descriptor.getName()))
throw new MappingDeclarationException(
String.format(
"Duplicate requestBody with name '%s' on method %s",
descriptor.getName(), method), method,
null, -1);
dataObjects.put(descriptor.getName(), descriptor.getValue());
} else if (descriptor.getType().equals(Type.requestPart)) {
formObjects.add(descriptor.getName(), descriptor.getValue());
}
}
result = executeRequest(dataObjects, method, rest, url, httpMethod,
returnType, parameters, formObjects, cookies, headers);
return result;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// no request mapping on method -> call method directly on object
UrlMapping requestMapping = getRequestMapping(method, args);
Object result = null;
if (requestMapping == null) {
result = handleSelfMethodInvocation(proxy, method, args);
} else {
result = handleRemoteInvocation(proxy, method, args, requestMapping);
}
return result;
}
protected Object executeRequest(Map<String, Object> dataObjects,
Method method, RestOperations rest, String url,
HttpMethod httpMethod, Class<?> returnType,
Map<String, Object> parameters,
MultiValueMap<String, Object> formObjects,
Map<String, String> cookies, MultiValueMap<String, String> headers) {
HttpEntity<?> requestEntity = null;
Object dataObject = dataObjects.get("");
if (dataObjects.size() > 1 && dataObject != null)
throw new MappingDeclarationException(
String.format(
"Found both named and anonymous arguments on method %s, that's ambiguous.",
method.toGenericString()), method, null, -1);
if (dataObject == null)
dataObject = formObjects.isEmpty() ? dataObjects : formObjects;
LinkedMultiValueMap<String, String> finalHeaders = new LinkedMultiValueMap<String, String>(
headers);
augmentHeadersWithCookies(finalHeaders, cookies);
boolean hasBody = !HttpMethod.GET.equals(httpMethod);
if (hasBody)
requestEntity = new HttpEntity<Object>(dataObject, finalHeaders);
else
requestEntity = new HttpEntity<Object>(headers);
ResponseEntity<?> responseEntity = rest.exchange(url, httpMethod,
requestEntity, returnType, parameters);
Object result = responseEntity.getBody();
return result;
}
protected void augmentHeadersWithCookies(
LinkedMultiValueMap<String, String> headers,
Map<String, String> cookies) {
if (cookies.isEmpty())
return;
String cookieHeader = "";
String prefix = "";
for (String cookieName : cookies.keySet()) {
cookieHeader = cookieHeader + prefix + cookieName + "="
+ cookies.get(cookieName);
prefix = "&";
}
headers.add("Cookie", cookieHeader);
}
protected String appendDescriptorNameParameterToUrl(String url,
MethodParameterDescriptor descriptor) {
if (url.contains("?"))
url += "&";
else
url += "?";
url += descriptor.getName() + "={" + descriptor.getName() + "}";
return url;
}
protected String replacePathVariableDescriptorInUrl(String url,
MethodParameterDescriptor descriptor) {
url = url.replaceAll("\\{" + descriptor.getName() + "\\}", ""
+ descriptor.getValue());
return url;
}
private MultiValueMap<String, String> getHeaders(UrlMapping requestMapping) {
MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>();
if (requestMapping.getHeaders() != null)
for (String header : requestMapping.getHeaders()) {
int index = header.indexOf("=");
if (index == -1)
throw new MappingDeclarationException(
"Missing equals sign in header annotation "
+ header + ": must be like KEY=VALUE",
null, null, -1);
String key = header.substring(0, index);
String value = header.substring(index + 1);
result.add(key, value);
}
if (requestMapping.getConsumes() != null)
for (String consume : requestMapping.getConsumes()) {
result.add("Content-Type", consume);
}
if (requestMapping.getProduces() != null)
for (String produce : requestMapping.getProduces()) {
result.add("Accept", produce);
}
for (MethodParameterDescriptor mpd : requestMapping.getParameters())
if (mpd.getType().equals(Type.httpHeader)) {
Object value = mpd.getValue();
String stringValue = value == null ? "" : value.toString();
result.add(mpd.getName(), stringValue);
}
return result;
}
/**
* Handles reflective method invocation, either invoking a method on the
* proxy (equals or hashcode) or directly on the target. Implementation
* copied from spring framework ServiceLocationInvocationHandler
*
* @param proxy
* @param method
* @param args
* @return
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
protected Object handleSelfMethodInvocation(Object proxy, Method method,
Object[] args) throws InvocationTargetException,
IllegalAccessException {
if (ReflectionUtils.isEqualsMethod(method)) {
// Only consider equal when proxies are identical.
return proxy == args[0];
} else if (ReflectionUtils.isHashCodeMethod(method)) {
// Use hashCode of service locator proxy.
return System.identityHashCode(proxy);
} else if (ReflectionUtils.isToStringMethod(method)) {
return remoteServiceInterfaceClass.getName() + "@"
+ System.identityHashCode(proxy);
} else {
return method.invoke(this, args);
}
}
}