package com.nominanuda.hyperapi; import com.nominanuda.urispec.StringMapURISpec; import com.nominanuda.web.http.Http400Exception; import com.nominanuda.web.http.Http401Exception; import com.nominanuda.web.http.Http403Exception; import com.nominanuda.web.http.Http404Exception; import com.nominanuda.web.http.Http4xxException; import com.nominanuda.web.http.Http5xxException; import com.nominanuda.zen.common.Check; import com.nominanuda.zen.obj.Obj; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.sql.Struct; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; 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 okhttp3.CacheControl; import okhttp3.FormBody; import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.internal.http.HttpMethod; import static com.nominanuda.hyperapi.EntityCodec.ENC; import static com.nominanuda.zen.io.Uris.URIS; /** * Created by azum on 20/03/17. */ public class HyperApiHttpInvocationHandler implements InvocationHandler { private final static CacheControl CACHE_CONTROL = new CacheControl.Builder().noCache().noStore().build(); private final static RequestBody EMPTY_REQUEST_BODY = RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), "".getBytes()); protected final OkHttpClient client; protected final String uriPrefix; protected HyperApiHttpInvocationHandler(OkHttpClient client, String uriPrefix) { this.client = client; this.uriPrefix = uriPrefix; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Request request = encode(method, args); Response response = client.newCall(request).execute(); int status = response.code(); if (status >= 400) { String message = response.message(); if (status < 500) { switch (status) { case 400: throw new Http400Exception(message); case 401: throw new Http401Exception(message); case 403: throw new Http403Exception(message); case 404: throw new Http404Exception(message); default: throw new Http4xxException(message, status); } } throw new Http5xxException(message, status); } return ENC.decode(response.body(), new AnnotatedType(method.getReturnType(), method.getAnnotations())); } protected Request encode(Method method, Object[] args) { RequestBody entity = null; Map<String, Object> uriParams = new HashMap<>(); Headers.Builder headersBuilder = new Headers.Builder(); FormBody.Builder formBodyBuilder = new FormBody.Builder(); Class<?>[] parameterTypes = method.getParameterTypes(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; Annotation[] annotations = parameterAnnotations[i]; Object arg = args[i]; boolean annotationFound = false; for (Annotation annotation : annotations) { if (annotation instanceof HeaderParam) { annotationFound = true; if (arg != null) { headersBuilder.add(((HeaderParam) annotation).value(), arg.toString()); } break; } else if (annotation instanceof PathParam) { annotationFound = true; if (arg != null) { uriParams.put(((PathParam) annotation).value(), arg.toString()); } break; } else if (annotation instanceof QueryParam) { annotationFound = true; if (arg != null) { uriParams.put(((QueryParam) annotation).value(), arg instanceof Collection ? toStringsList((Collection<?>) arg) : arg.toString() ); } break; } else if (annotation instanceof FormParam) { annotationFound = true; if (arg != null) { String name = ((FormParam) annotation).value(); if (arg instanceof Obj) { Check.unsupportedoperation.fail("TODO arg instanceof Obj"); // Map<String, Object> map = new HashMap<String, Object>(); // STRUCT.toFlatMap(STRUCT.buildObject(name, arg), map); // for (Map.Entry<String, Object> entry : map.entrySet()) { // if (entry.getValue() != null) { // formBodyBuilder.add(entry.getKey(), entry.getValue().toString()); // } // } } else if (arg instanceof Collection) { for (Object v : (Collection<?>) arg) { if (v != null) { formBodyBuilder.add(name, v.toString()); } } } else { formBodyBuilder.add(name, arg.toString()); } } break; } } if (!annotationFound) { Check.unsupportedoperation.assertNull(entity); entity = ENC.encode(arg, new AnnotatedType(parameterType, annotations)); } } String httpMethod = null; StringMapURISpec spec = null; for (Annotation a : method.getAnnotations()) { if (a instanceof POST) { httpMethod = "POST"; } else if (a instanceof GET) { httpMethod = "GET"; } else if (a instanceof PUT) { httpMethod = "PUT"; } else if (a instanceof DELETE) { httpMethod = "DELETE"; } else if (a instanceof Path) { spec = new StringMapURISpec(URIS.pathJoin(uriPrefix, ((Path) a).value())); } } FormBody formBody = formBodyBuilder.build(); if (formBody.size() > 0) { httpMethod = "POST"; entity = formBody; } return new Request.Builder() .cacheControl(CACHE_CONTROL) .url(spec.template(uriParams)) .headers(headersBuilder .add("Accept", Struct.class.isAssignableFrom(method.getReturnType()) ? "application/json" : "*/*") .add("User-Agent", "zen-android") .add("Cache-Control", "no-cache") .add("Connection", "close") // TODO remove? .add("Pragma", "no-cache").build()) .method(httpMethod, entity == null && HttpMethod.requiresRequestBody(httpMethod) ? EMPTY_REQUEST_BODY : entity ) .build(); } private List<String> toStringsList(Collection<?> c) { List<String> l = new ArrayList<>(); for (Object o : c) { l.add(o != null ? o.toString() : null); } return l; } }