/**
* Copyright 2005-2014 Restlet
*
* The contents of this file are subject to the terms of one of the following
* open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can
* select the license that you prefer but you may not use this file except in
* compliance with one of these Licenses.
*
* You can obtain a copy of the Apache 2.0 license at
* http://www.opensource.org/licenses/apache-2.0
*
* You can obtain a copy of the EPL 1.0 license at
* http://www.opensource.org/licenses/eclipse-1.0
*
* See the Licenses for the specific language governing permissions and
* limitations under the Licenses.
*
* Alternatively, you can obtain a royalty free commercial license with less
* limitations, transferable or non-transferable, directly at
* http://restlet.com/products/restlet-framework
*
* Restlet is a registered trademark of Restlet S.A.S.
*/
package org.restlet.ext.jaxrs.internal.client;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.logging.Level;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import org.apache.commons.lang.ClassUtils;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Uniform;
import org.restlet.data.ClientInfo;
import org.restlet.data.Cookie;
import org.restlet.data.Form;
import org.restlet.data.Parameter;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.engine.application.StatusInfo;
import org.restlet.engine.resource.ClientInvocationHandler;
import org.restlet.engine.resource.MethodAnnotationInfo;
import org.restlet.engine.resource.ThrowableAnnotationInfo;
import org.restlet.engine.util.StringUtils;
import org.restlet.ext.jaxrs.JaxRsClientResource;
import org.restlet.ext.jaxrs.internal.exceptions.IllegalMethodParamTypeException;
import org.restlet.ext.jaxrs.internal.util.Util;
import org.restlet.representation.Representation;
import org.restlet.representation.Variant;
import org.restlet.resource.ClientProxy;
import org.restlet.resource.ClientResource;
import org.restlet.resource.ResourceException;
import org.restlet.resource.Result;
import org.restlet.util.Series;
/**
* Reflection proxy invocation handler created for the
* {@link JaxRsClientResource#wrap(Class)} and related methods.
*
* @see JaxRsClientResource
* @see ClientInvocationHandler
*
* @author Shaun Elliott
*
* @param <T>
* The annotated resource interface.
*/
public class JaxRsClientInvocationHandler<T> extends ClientInvocationHandler<T> {
private ClientResource clientResource;
/**
* Constructor.
*
* @param clientResource
* The client resource.
* @param resourceInterface
* The annotated resource interface.
*/
public JaxRsClientInvocationHandler(ClientResource clientResource,
Class<? extends T> resourceInterface) {
super(clientResource, resourceInterface, JaxRsAnnotationUtils
.getInstance());
this.clientResource = clientResource;
}
private void addCookieParam(Request request, String representationAsText,
Annotation annotation) {
Series<Cookie> cookies = request.getCookies();
if (cookies == null) {
cookies = new Series<Cookie>(Cookie.class);
}
cookies.add(new Cookie(((CookieParam) annotation).value(),
representationAsText));
request.setCookies(cookies);
}
private void addFormParam(Form form, String representationAsText,
Annotation annotation) {
form.add(new Parameter(((FormParam) annotation).value(),
representationAsText));
}
private void addHeaderParam(Request request, String representationAsText,
Annotation annotation) {
Util.getHttpHeaders(request).add(((HeaderParam) annotation).value(),
representationAsText);
}
private void addPathParam(Request request, String representationAsText,
Annotation annotation) {
String paramName = ((PathParam) annotation).value();
String existingPath = Reference.decode(request.getResourceRef()
.getPath());
String simplePathParam = String.format("{%s}", paramName);
if (existingPath.contains(simplePathParam)) {
existingPath = existingPath.replace(simplePathParam,
Reference.encode(representationAsText));
}
// TODO - allow regex path params - this code *mostly* works, but not
// quite
// String regexPathParam = String.format(".*\\{%s:(.+)\\}.*",
// paramName);
// try {
// if (existingPath.matches(regexPathParam)) {
// Matcher matcher = Pattern.compile(regexPathParam).matcher(
// existingPath);
// String pattern = matcher.group(1);
//
// /* I'm not sure how much sense it makes to match on the
// * textual form of the representation, unless it is a String...
// */
// if (representationAsText.matches(pattern)) {
// existingPath = existingPath.replace(regexPathParam,
// Reference.encode(representationAsText));
// }
// }
// } catch (PatternSyntaxException pse) {
// // something is not right in the param definition, skip it
// pse.printStackTrace();
// return;
// }
request.getResourceRef().setPath(existingPath);
}
private void addQueryParam(Request request, String representationAsText,
Annotation annotation) {
request.getResourceRef().addQueryParameter(
new Parameter(((QueryParam) annotation).value(),
representationAsText));
}
private String getRepresentationAsText(Object value) {
Class<? extends Object> clazz = value.getClass();
boolean isPrimitiveOrWrapped = clazz.isPrimitive()
|| ClassUtils.wrapperToPrimitive(clazz) != null;
if (isPrimitiveOrWrapped || clazz == String.class) {
return String.valueOf(value);
}
String representationAsText = null;
try {
Representation representation = clientResource.getApplication()
.getConverterService().toRepresentation(value);
representationAsText = representation.getText();
} catch (IOException e) {
throw new WebApplicationException(e);
}
return representationAsText;
}
@Override
protected Request getRequest(Method javaMethod, Object[] args)
throws Throwable {
Request request = super.getRequest(javaMethod, args);
setRequestPathToAnnotationPath(javaMethod, request);
return request;
}
@SuppressWarnings("rawtypes")
private void handleJavaMethodParameter(Request request, Object value,
Type genericParameterType, Annotation[] annotations, Form form) throws IOException {
if (value == null) {
// Set the entity
request.setEntity(null);
} else if (Result.class.isAssignableFrom(value.getClass())) {
// Asynchronous mode where a callback object is to
// be called.
// Get the kind of result expected.
final Result rCallback = (Result) value;
ParameterizedType parameterizedType = (genericParameterType instanceof java.lang.reflect.ParameterizedType) ? (java.lang.reflect.ParameterizedType) genericParameterType
: null;
final Class<?> actualType = (parameterizedType
.getActualTypeArguments()[0] instanceof Class<?>) ? (Class<?>) parameterizedType
.getActualTypeArguments()[0] : null;
// Define the callback
Uniform callback = new Uniform() {
@SuppressWarnings("unchecked")
public void handle(Request request,
Response response) {
if (response.getStatus().isError()) {
rCallback.onFailure(new ResourceException(
response.getStatus()));
} else {
if (actualType != null) {
Object result = null;
boolean serializationError = false;
try {
result = getClientResource()
.toObject(
response.getEntity(),
actualType);
} catch (Exception e) {
serializationError = true;
rCallback
.onFailure(new ResourceException(
e));
}
if (!serializationError) {
rCallback.onSuccess(result);
}
} else {
rCallback.onSuccess(null);
}
}
}
};
getClientResource().setOnResponse(callback);
} else if (annotations != null && annotations.length > 0) {
String representationAsText = getRepresentationAsText(value);
if (representationAsText != null) {
for (Annotation annotation : annotations) {
if (annotation instanceof HeaderParam) {
addHeaderParam(request, representationAsText,
annotation);
} else if (annotation instanceof QueryParam) {
addQueryParam(request, representationAsText, annotation);
} else if (annotation instanceof FormParam) {
addFormParam(form, representationAsText, annotation);
} else if (annotation instanceof CookieParam) {
addCookieParam(request, representationAsText,
annotation);
} else if (annotation instanceof MatrixParam) {
// TODO
} else if (annotation instanceof PathParam) {
addPathParam(request, representationAsText, annotation);
}
}
}
} else {
// Set the entity
request.setEntity(getClientResource().toRepresentation(
value));
}
}
@Override
public Object invoke(Object proxy, Method javaMethod, Object[] args)
throws Throwable {
Object result = null;
if (javaMethod.equals(Object.class.getMethod("toString"))) {
// Help debug
result = "ClientProxy for resource: " + clientResource;
} else if (javaMethod.equals(ClientProxy.class
.getMethod("getClientResource"))) {
result = clientResource;
} else {
MethodAnnotationInfo annotationInfo = getAnnotationUtils()
.getMethodAnnotation(getAnnotations(), javaMethod);
if (annotationInfo == null) {
return result;
}
// Clone the prototype request
Request request = getRequest(javaMethod, args);
if ((args != null) && args.length > 0) {
Form form = new Form();
Annotation[][] parameterAnnotations = javaMethod.getParameterAnnotations();
Type[] genericParameterTypes = javaMethod.getGenericParameterTypes();
// Checks if the user has defined its own callback.
for (int i = 0; i < args.length; i++) {
Object o = args[i];
Type genericParameterType = genericParameterTypes[i];
handleJavaMethodParameter(request, o, genericParameterType, parameterAnnotations[i], form);
}
if (!form.isEmpty()) {
request.setEntity(form.getWebRepresentation());
}
}
// The Java method was annotated
request.setMethod(annotationInfo.getRestletMethod());
// Add the mandatory query parameters
String query = annotationInfo.getQuery();
if (query != null) {
Form queryParams = new Form(annotationInfo.getQuery());
request.getResourceRef().addQueryParameters(queryParams);
}
// Updates the client preferences if they weren't changed
if ((request.getClientInfo().getAcceptedCharacterSets().isEmpty())
&& (request.getClientInfo().getAcceptedEncodings()
.isEmpty())
&& (request.getClientInfo().getAcceptedLanguages()
.isEmpty())
&& (request.getClientInfo().getAcceptedMediaTypes()
.isEmpty())) {
List<Variant> responseVariants = annotationInfo
.getResponseVariants(getClientResource()
.getMetadataService(), getClientResource()
.getConverterService());
if (responseVariants != null) {
request.setClientInfo(new ClientInfo(responseVariants));
}
}
// Effectively handle the call
Response response = getClientResource().handleOutbound(request);
// Handle the response
if (getClientResource().getOnResponse() == null) {
if ((response != null) && response.getStatus().isError()) {
ThrowableAnnotationInfo tai = getAnnotationUtils()
.getThrowableAnnotationInfo(javaMethod,
response.getStatus().getCode());
if (tai != null) {
Class<?> throwableClazz = tai.getJavaClass();
Throwable t = null;
if (tai.isSerializable()
&& response.isEntityAvailable()) {
t = (Throwable) getClientResource().toObject(
response.getEntity(), throwableClazz);
} else {
try {
t = (Throwable) throwableClazz.newInstance();
} catch (Exception e) {
Context.getCurrentLogger()
.log(Level.FINE,
"Unable to instantiate the client-side exception using the default constructor.");
}
if (response.isEntityAvailable()) {
StatusInfo si = getClientResource().toObject(
response.getEntity(), StatusInfo.class);
if (si != null) {
response.setStatus(new Status(si.getCode(),
si.getReasonPhrase(), si
.getDescription()));
}
}
}
if (t != null) {
throw t;
}
// TODO cf issues 1004 and 1018.
// this code has been commented as the automatic
// deserialization is problematic. We may rethink a
// way to recover the status info.
// } else if (response.isEntityAvailable()) {
// StatusInfo si = getClientResource().toObject(
// response.getEntity(), StatusInfo.class);
//
// if (si != null) {
// response.setStatus(new Status(si.getCode(), si
// .getReasonPhrase(), si.getDescription()));
// }
}
getClientResource().doError(response.getStatus());
} else if (!annotationInfo.getJavaOutputType().equals(
void.class)) {
result = getClientResource().toObject(
(response == null ? null : response.getEntity()),
annotationInfo.getJavaOutputType());
}
}
}
return result;
}
private void setRequestPathToAnnotationPath(Method javaMethod,
Request request) {
Path methodPathAnnotation = javaMethod.getAnnotation(Path.class);
if (methodPathAnnotation != null) {
String methodPath = methodPathAnnotation.value();
if (!StringUtils.isNullOrEmpty(methodPath)) {
String fullUriFromPath = request.getResourceRef().getPath();
if (fullUriFromPath.endsWith("/")) {
if (methodPath.startsWith("/")) {
fullUriFromPath += methodPath.substring(1);
} else {
fullUriFromPath += methodPath;
}
} else {
if (methodPath.startsWith("/")) {
fullUriFromPath += methodPath;
} else {
fullUriFromPath += "/" + methodPath;
}
}
request.getResourceRef().setPath(fullUriFromPath);
}
}
}
}