/*
* Copyright 2008-2011 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 com.nominanuda.hyperapi;
import static com.nominanuda.web.http.HttpCoreHelper.HTTP;
import static com.nominanuda.zen.obj.JsonPath.JPATH;
import static com.nominanuda.zen.obj.wrap.Wrap.WF;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicHttpResponse;
import com.nominanuda.web.http.Http500Exception;
import com.nominanuda.web.http.HttpCoreHelper;
import com.nominanuda.web.mvc.ObjURISpec;
import com.nominanuda.web.mvc.WebService;
import com.nominanuda.zen.common.Check;
import com.nominanuda.zen.common.Tuple2;
import com.nominanuda.zen.obj.Arr;
import com.nominanuda.zen.obj.Obj;
import com.nominanuda.zen.obj.wrap.ObjWrapper;
public class HyperApiWsSkelton implements WebService {
private final EntityCodec entityCodec = EntityCodec.createBasic();
private final HyperApiIntrospector apiIntrospector = new HyperApiIntrospector();
private Class<?> api;
private String requestUriPrefix = "";
private String jsonDurationProperty;
private Object service;
public HttpResponse handle(HttpRequest request) throws Exception {
long start = System.currentTimeMillis();
try {
Tuple2<Object, AnnotatedType> result = handleCall(request);
Object handlerResult = result.get0();
if (jsonDurationProperty != null && handlerResult instanceof Obj) {
((Obj)handlerResult).put(jsonDurationProperty, System.currentTimeMillis() - start);
}
return response(handlerResult, result.get1());
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause != null && cause instanceof Exception) {
throw (Exception) cause;
} else {
throw new Http500Exception(e);
}
}
}
protected Tuple2<Object, AnnotatedType> handleCall(HttpRequest request) throws Exception, IllegalArgumentException/*method not found*/ {
String requestUri = request.getRequestLine().getUri();
Check.illegalargument.assertTrue(requestUri.startsWith(requestUriPrefix));
String apiRequestUri = requestUri.substring(requestUriPrefix.length());
for (Method m : api.getMethods()) { // better than getDeclaredMethods(), as we use interfaces and they could extend one another
Path pathAnno = apiIntrospector.findPathAnno(m);
if (pathAnno != null) {
ObjURISpec spec = new ObjURISpec(pathAnno.value());
Obj uriParams = spec.match(apiRequestUri);
if (uriParams != null) {
if (supportsHttpMethod(m, request.getRequestLine().getMethod())) {
Object[] args = createArgs(uriParams, new HttpCoreHelper().getEntity(request), api, m);
Object result = invokeMethod(m, args);
return new Tuple2<Object, AnnotatedType>(result, new AnnotatedType(m.getReturnType(), m.getAnnotations()));
}
}
}
}
throw new IllegalArgumentException("could not find any suitable method to call " + "for api request: " + apiRequestUri);
}
protected Object invokeMethod(Method m, Object[] args) throws IllegalAccessException, InvocationTargetException {
Object result = m.invoke(service, args);
return result;
}
private boolean supportsHttpMethod(Method method, String httpMethod) {
for (Annotation a : method.getAnnotations()) {
if (a instanceof GET
|| a instanceof POST
|| a instanceof PUT
|| a instanceof DELETE) {
if (a.annotationType().getSimpleName().equals(httpMethod)) {
return true;
}
}
}
return false;
}
private Object[] createArgs(Obj uriParams, HttpEntity entity, Class<?> api2, Method method) throws IOException {
List<NameValuePair> formParams = Collections.emptyList();
if (entity != null) {
formParams = HTTP.parseEntityWithDefaultUtf8(entity);
}
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
Annotation[] annotations = parameterAnnotations[i];
AnnotatedType p = new AnnotatedType(parameterType, annotations);
boolean annotationFound = false;
for (Annotation annotation : annotations){
if (annotation instanceof HeaderParam) {
// TODO
} else if (annotation instanceof PathParam) {
annotationFound = true;
Object o = JPATH.getPathSafe(uriParams, ((PathParam) annotation).value());
args[i] = decast(o, parameterType);
break;
} else if (annotation instanceof QueryParam) {
annotationFound = true;
Object o = JPATH.getPathSafe(uriParams, ((QueryParam) annotation).value());
args[i] = decast(o, parameterType);
break;
} else if (annotation instanceof FormParam) {
annotationFound = true;
if (Obj.class.equals(parameterType)) {
args[i] = HTTP.toStru(formParams).asObj();
} else if (parameterType.isInterface() && ObjWrapper.class.isAssignableFrom(parameterType)) {
args[i] = WF.wrap(HTTP.toStru(formParams).asObj(), parameterType);
} else {
Object o = getFormParams(formParams, ((FormParam) annotation).value());
args[i] = decast(o, parameterType);
}
break;
}
}
if(! annotationFound) {
if(entity == null) {
args[i] = null;
}/* else if(parameterType.isInterface() && ObjWrapper.class.isAssignableFrom(parameterType)) {
args[i] = WF.wrap((ObjWrapper)entityCodec.decode(entity, p), parameterType);
}*/ else {
args[i] = entityCodec.decode(entity, p);
}
}
}
return args;
}
private Object getFormParams(List<NameValuePair> formParams, String name) {
List<Object> params = new ArrayList<>();
for (NameValuePair pair : formParams) {
if (name.equals(pair.getName())) {
params.add(pair.getValue());
}
}
switch (params.size()) {
case 0:
return null;
case 1:
return params.get(0);
}
return params;
}
private Object decast(Object val, Class<?> targetType) {
if (val != null) {
if (Collection.class.isAssignableFrom(targetType)) {
if (val instanceof Collection) {
return val;
}
if (val instanceof Arr) {
return val;//
}
Collection<String> result = new ArrayList<String>();
result.add((String) decast(val, String.class));
return result;
}
if (Arr.class.equals(targetType)) {
if (val instanceof Arr) {
return val;
}
//Andreas if (val instanceof Collection) {
// STRUCT.fromMapsAndCollections((Collection<?>) val);
// }
return Arr.make(decast(val, String.class));
}
String sval = (String) val;
if (String.class.equals(targetType)) {
return sval;
} else if (Integer.class.equals(targetType) || int.class.equals(targetType) || "int".equals(targetType.getSimpleName())) {
return Integer.parseInt(sval);
} else if (Long.class.equals(targetType) || long.class.equals(targetType) || "long".equals(targetType.getSimpleName())) {
return Long.parseLong(sval);
} else if (Double.class.equals(targetType) || double.class.equals(targetType) || "double".equals(targetType.getSimpleName())) {
return Double.parseDouble(sval);
} else if (Boolean.class.equals(targetType) || boolean.class.equals(targetType) || "boolean".equals(targetType.getSimpleName())) {
return Boolean.parseBoolean(sval);
}
}
return null;
}
protected HttpResponse response(Object result, AnnotatedType ap) {
if(result instanceof HttpResponse) {
return (HttpResponse)result;
} else {
BasicHttpResponse resp = new BasicHttpResponse(HTTP.statusLine(200));
HttpEntity entity = null;
if(result instanceof HttpEntity) {
entity = (HttpEntity)result;
} else if(result != null) {
entity = entityCodec.encode(result, ap);
}
if (entity != null) {
resp.setEntity(entity);
}
return resp;
}
}
/* proxy magic */
private void evCreateProxy() {
if (api != null && service != null) {
if (!api.isInstance(service)) {
final Object origService = service;
service = Proxy.newProxyInstance(api.getClassLoader(), new java.lang.Class[] { api }, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Method m = origService.getClass().getMethod(method.getName(), method.getParameterTypes()); // "same" method in different objs hierarchy
return m.invoke(origService, args);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause != null && cause instanceof Exception) {
throw (Exception) cause;
} else {
throw new Http500Exception(e);
}
}
}
});
}
}
}
/* setters */
public void setApi(Class<?> api) {
this.api = api;
evCreateProxy();
}
public void setService(Object service) {
this.service = service;
evCreateProxy();
}
public void setRequestUriPrefix(String requestUriPrefix) {
this.requestUriPrefix = requestUriPrefix;
}
public void setJsonDurationProperty(String jsonDurationProperty) {
this.jsonDurationProperty = jsonDurationProperty;
}
}