package rest.vertx;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import rest.vertx.Annotations.Base;
import rest.vertx.Annotations.NoParam;
import rest.vertx.Annotations.RestIgnore;
import rest.vertx.models.Blocking;
import rest.vertx.models.RequestInfo;
import rest.vertx.models.RestResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
public class RestVertx {
public static <T> void register(Vertx _v, Router _r, T _toInvoke) {
@SuppressWarnings("unchecked")
Class<T> sub = (Class<T>) _toInvoke.getClass();
Router mainRouter = _r;
Router subRouter = Router.router(_v);
// So we can use getBodyAsJson() and/or getBodyAsString() in our handling methods
subRouter.route().handler(BodyHandler.create());
String basePath = getBasePathValue(sub);
Object toInvoke = _toInvoke;
for (Method m : sub.getMethods()) {
boolean ignore = getIgnore(m);
// If ignore set, skip this method right away
if (ignore)
continue;
String path = getPath(m);
// If path isn't set, skip this method right away
if (path == null)
continue;
String httpMethod = getHttpMethod(m);
HashMap<Integer, Parameter> paramOrder = new HashMap<Integer, Parameter>();
loadOrder(paramOrder, m);
HashMap<Integer, Class<?>> paramTypes = new HashMap<Integer, Class<?>>();
loadParamTypes(paramTypes, m);
Parameter[] params = m.getParameters();
ArrayList<String> paramNames = new ArrayList<String>();
for (int i = 0; i < params.length; i++) {
paramNames.add(m.getParameters()[i].getName());
}
if (httpMethod == null) {
// Assumption: if http method is not specified in annotation, http method might be embedded in the actual method name
// check if the name of the actual method is an http method
httpMethod = parseMethodName(m);
}
// Assumption: path and/or base path and the http method has been set (method defaults to GET)
String resultType = getResultType(m);
ArrayList<String> pathParamList = new ArrayList<String>();
RequestInfo requestInfo = new RequestInfo(getBlocking(m));
// If the path was something/:id, then 'id' would be added to the pathParamList
setArgumentNameIndex(pathParamList, path);
getRouteMethod(httpMethod, path, subRouter).handler(rc -> {
// Specifies whether we should parse the request body for the specified variables
boolean parseRequestBody = false;
// Store the argument values in a map since they're not guaranteed to be in order
HashMap<String, Object> argValues = new HashMap<String, Object>();
ArrayList<Object> argValueList = new ArrayList<Object>();
// If there is more than one parameter for this method, parse the arguments sent via request
if (paramOrder.size() > 0) {
if (!pathParamList.isEmpty()) {
// Assumption: the request body is not being used, argument(s) sent in path params
Iterator<String> iter = pathParamList.listIterator();
while (iter.hasNext()) {
String key = iter.next();
Object val = rc.request().getParam(key);
argValues.put(key, val);
argValueList.add(val);
}
} else if (paramOrder.size() == 1) {
JsonObject jObject = rc.getBodyAsJson();
// Do not trust the client. He may not have sent the json, or it could be malformed.
// In this case, send an empty response with status code = 400 (bad request)
if (jObject == null) {
rc.response().setStatusCode(400).end();
return;
}
if (jObject.size() == 1 && jObject.containsKey(paramNames.get(0))) {
//Object val = toString(jObject.getValue(paramNames.get(0)));
argValues.put(paramNames.get(0), jObject.getValue(paramNames.get(0)));
argValueList.add(jObject.getValue(paramNames.get(0)));
} else {
// One method parameter is set
// Assumption: User sent all objects in one serialized Json string
String val = rc.getBodyAsString();
argValues.put("__serialized", val);
argValueList.add(val);
parseRequestBody = true;
}
} else if (paramOrder.size() > 1) {
// There weren't any path variables set and there is more than one method parameter,
// therefore, we'll try parsing the request body and deserialize/reserialize
// Assumption: Request body must be in serialized Json format with each argument variable name set as in the arguments
// Deserialize body into Json Object
JsonObject jObject = rc.getBodyAsJson();
// Do not trust the client. He may not have sent the json, or it could be malformed.
// In this case, send an empty response with status code = 400 (bad request)
if (jObject == null) {
rc.response().setStatusCode(400).end();
return;
}
Iterator<HashMap.Entry<String, Object>> iter = jObject.iterator();
while (iter.hasNext()) {
Entry<String, Object> current = iter.next();
String key = current.getKey();
Object val = current.getValue().toString();
argValues.put(key, val);
argValueList.add(val);
}
parseRequestBody = true;
}
}
// Places the path variable/arguments in order specified by the parameter
Object[] arguments = buildArgs(paramOrder, paramTypes, argValues, argValueList, parseRequestBody);
final Object[] arguments_f = arguments;
// This can be pretty long. Since we have a grip on the Vertx object we can use it to create a blocking function
// Only use blocking if blocking is indicated in the handling method's blocking annotation
if (requestInfo.getBlocking().isBlocking()) {
_v.executeBlocking(
objectFuture -> {
invokeMethod(m, toInvoke, arguments_f, objectFuture);
},
requestInfo.getBlocking().isSerial(),
objectAsyncResult -> {
Object toret = objectAsyncResult.result();
invokeResponse(rc, m, toret, resultType);
}
);
} else {
// Non-blocking
Object toret = invokeMethod(m, toInvoke, arguments_f, null);
invokeResponse(rc, m, toret, resultType);
}
});
}
mainRouter.mountSubRouter((basePath == null) ? "/" : basePath, subRouter);
}
static void invokeResponse(RoutingContext rc, Method m, Object toret, String resultType) {
// Handle CORS stuff here (only if set individually on the method via annotation)
String[] _corsAllowedIPs = getCORS(m);
if (_corsAllowedIPs != null && _corsAllowedIPs.length > 0) {
CORS.allow(rc, _corsAllowedIPs);
}
// Combine errors
if (toret == null || !(toret instanceof RestResponse)){
// Handling function didn't return a RestResponse object, return an appropriate error message
rc.response().setStatusCode(500).setStatusMessage("Error: Function didn't return a RestResponse object").end();
// We don't "have" to throw an exception, a 500 may suffice and client may log error and message in their logs
// new RuntimeException("The return type of a REST function must be a RestResponse");
}else {
// Put the custom headers first, if any
putHeaders(((RestResponse) toret).getHeaders(), rc);
// PUt the custom status code/message, if any
putStatus((RestResponse) toret, rc);
if (resultType != null) {
if (resultType.equals("file")) {
// Send the file
rc.response().sendFile(((RestResponse) toret).getBody()).end();
} else if (resultType.equals("json")) {
// Set the header for json content - will override any custom header for content-type
rc.response().putHeader("content-type", "application/json; charset=utf-8");
rc.response().end(((RestResponse) toret).getBody());
}
} else {
// New assumption: If result type not set, we're sending back whatever the custom headers say to return
// or else if no custom headers for content-type, we'll return text
if (!rc.response().headers().contains("content-type")) {
rc.response().putHeader("content-type", "text/plain");
}
String result = ((((RestResponse) toret).getBody() != null) ? ((RestResponse) toret).getBody() : " ");
rc.response().end(result);
}
}
}
static Object invokeMethod(Method m, Object toInvoke, Object[] arguments_f, Future<Object> objectFuture)
{
try {
Object toret = m.invoke(toInvoke, arguments_f);
if (objectFuture != null) {
objectFuture.complete(toret);
}
else {
return toret;
}
} catch (InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
if (objectFuture != null) {
objectFuture.complete(null);
}
}
// If error & non-blocking
return null;
}
static Object[] buildArgs(HashMap<Integer, Parameter> _order, HashMap<Integer, Class<?>> _paramTypes,
HashMap<String, Object> _argValues, ArrayList<Object> _argValueList,
boolean parseRequestBody) {
if (_order.isEmpty() && _argValues.isEmpty())
return new Object[0];
Object[] toret = new Object[_order.size()];
if (_order.get(0).getName().contains("arg0")) {
// Then program wasn't compiled so parameters were saved or user named first parameter arg0
// Because the parameters weren't saved, we don't know anything about the parameter(s)
// We can't autobind or assume we know what the argument type is supposed to be (we could guess, but we could be wrong)
// Assumption: Path variables are in the same order as the parameters
if (parseRequestBody) {
if (_argValues.containsKey("__serialized")) {
// only one Json string sent in request body for one complex object
if (_paramTypes.size() == 1)
toret[0] = parse(_paramTypes.get(0), _argValues.get("__serialized"));
} else {
}
} else {
_argValueList.toArray(toret);
}
} else {
// The program was compiled so parameters were saved (yay!)
if (parseRequestBody) {
if (_argValues.containsKey("__serialized")) {
if (_paramTypes.size() == 1) {
toret[0] = tryParse(_paramTypes.get(0), _paramTypes.get(0).getName(), _argValues.get("__serialized"));
}
} else {
// each argValue will be deserialized into complex objects
for (int key = 0; key < _order.size(); key++) {
// Unbox and cast - Initially a String argument
toret[key] = tryParse(_paramTypes.get(key), _paramTypes.get(key).getName(), _argValues.get(_order.get(key).getName()));
}
}
} else {
// We'll match the path variable names to the method parameter names, no matter the path variable order
for (int key = 0; key < _order.size(); key++) {
// Unbox and cast - Initially a String argument
try {
toret[key] = tryParse(_paramTypes.get(key), _paramTypes.get(key).getName(), _argValues.get(_order.get(key).getName()));
} catch (Exception e) {
say(e.getMessage());
e.printStackTrace();
}
}
}
}
return toret;
}
private static Object tryParse(Class<?> _paramType, String paramName, Object o) {
switch (paramName) {
case "java.lang.String":
return (String) o;
case "int":
return Integer.parseInt((String) o);
case "boolean":
return Boolean.parseBoolean((String) o);
case "char":
return ((String) o).charAt(0);
case "double":
return Double.parseDouble((String) o);
case "long":
return Long.parseLong((String) o);
case "short":
return Short.parseShort((String) o);
case "byte":
return Byte.parseByte((String) o);
case "float":
return Float.parseFloat((String) o);
default:
// Treat it as a JSON Stringified/Serialized Object if the arg value is a string at this point,
// try to deserialize/autobind if it is
if (o instanceof String) {
return parse(_paramType, o);
} else {
return o.toString();
}
}
}
static String toString(Object o) {
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
String toret = "";
try {
toret = mapper.writeValueAsString(o);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return toret;
}
static Object parse(Class<?> p, Object o) {
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
Object toret = null;
String s = (String) o;
// Escape if URL Encoded string, which often times contains a % sign
if (s.contains("%")) {
try {
s = URLDecoder.decode(s, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
try {
toret = mapper.readValue((String) s, p);
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return toret;
}
static void loadOrder(HashMap<Integer, Parameter> _map, Method _method) {
Parameter[] params = _method.getParameters();
for (int i = 0; i < params.length; i++) {
_map.put(i, params[i]);
}
}
static void loadParamTypes(HashMap<Integer, Class<?>> _map, Method _method) {
Class<?>[] test = _method.getParameterTypes();
for (int i = 0; i < test.length; i++) {
_map.put(i, test[i]);
}
}
static void setArgumentNameIndex(ArrayList<String> _list, String _path) {
if (_path != null) {
String[] split = _path.split("/");
for (String arg : split) {
if (arg.startsWith(":")) {
_list.add(arg.substring(1));
}
}
}
}
static String parseMethodName(Method _method) {
String name = _method.getName().toLowerCase();
// Check if the method is an exact match of an http method, if it is then we'll treat it as such
switch (name) {
case "get":
case "post":
case "put":
case "delete":
case "options":
return _method.getName();
default:
break;
}
// If to this point, check if name contains the method name - performance hit but who are we to decide? Tradeoffs...
if (name.contains("get")) {
return "get";
} else if (name.contains("post")) {
return "post";
} else if (name.contains("put")) {
return "put";
} else if (name.contains("delete")) {
return "delete";
} else if (name.contains("options")) {
return "options";
}
// Defaults to get
return "get";
}
static <T> String getBasePathValue(Class<T> _sub) {
String basePath = "";
if (_sub.isAnnotationPresent(rest.vertx.Annotations.Base.class))
basePath = _sub.getAnnotation(Base.class).value();
// Make sure the forward slash is present at the very minimum for base path
if (basePath.length() > 0 && !basePath.equals("/"))
return "/" + basePath;
else
return "/";
}
static boolean hasNoParam(Method _method) {
return _method.isAnnotationPresent(NoParam.class);
}
static boolean getIgnore(Method _method) {
return _method.isAnnotationPresent(RestIgnore.class);
}
static String[] getCORS(Method _method) {
if (_method.isAnnotationPresent(rest.vertx.Annotations.CORS.class)) {
rest.vertx.Annotations.CORS[] cors = _method.getAnnotationsByType(rest.vertx.Annotations.CORS.class);
String[] ipAndPorts = new String[cors.length];
for (int i = 0; i < cors.length; i++) {
if (cors[i].value() != null && cors[i].value().length() > 0) {
// Only support one
ipAndPorts[i] = cors[i].value();
} else {
ipAndPorts[i] = "*";
}
if (i < cors.length - 1) {
ipAndPorts[i] += ",";
}
}
return ipAndPorts;
} else {
return null;
}
}
static String getPath(Method _method) {
if (_method.isAnnotationPresent(rest.vertx.Annotations.Path.class)) {
String path = _method.getAnnotation(rest.vertx.Annotations.Path.class).value();
// Make sure the forward slash is present at the very minimum for method path if it's set
if (path.length() > 0 && !(path.charAt(0) == '/'))
return "/" + path;
return path;
} else {
return null;
}
}
static String getHttpMethod(Method _method) {
if (_method.isAnnotationPresent(rest.vertx.Annotations.Method.class)) {
return _method.getAnnotation(rest.vertx.Annotations.Method.class).value();
} else {
return null;
}
}
static String getResultType(Method _method) {
if (_method.isAnnotationPresent(rest.vertx.Annotations.ResultType.class)) {
return _method.getAnnotation(rest.vertx.Annotations.ResultType.class).value().toLowerCase();
} else {
return null;
}
}
static Blocking getBlocking(Method _method) {
boolean blocking = false;
boolean serial = true;
if (_method.isAnnotationPresent(rest.vertx.Annotations.Blocking.class)) {
blocking = _method.getAnnotation(rest.vertx.Annotations.Blocking.class).value().toLowerCase().equals("true");
serial = _method.getAnnotation(rest.vertx.Annotations.Blocking.class).serial().toLowerCase().equals("true");
} else {
return new Blocking();
}
return new Blocking(blocking, serial);
}
private static final String TYPE_NAME_PREFIX = "class ";
public static String getClassName(Type type) {
if (type == null) {
return "";
}
String className = type.toString();
if (className.startsWith(TYPE_NAME_PREFIX)) {
className = className.substring(TYPE_NAME_PREFIX.length());
}
return className;
}
public static Class<?> getClass(Type type)
throws ClassNotFoundException {
String className = getClassName(type);
if (className == null || className.isEmpty()) {
return null;
}
return Class.forName(className);
}
private static Route getRouteMethod(String mthd, String path, Router subRouter) {
Route toret = null;
switch (mthd.toUpperCase()) {
case "GET":
if (path != null)
toret = subRouter.get(path);
else
toret = subRouter.get();
break;
case "POST":
if (path != null)
toret = subRouter.post(path);
else
toret = subRouter.post();
break;
case "PUT":
if (path != null)
toret = subRouter.put(path);
else
toret = subRouter.put();
break;
case "DELETE":
if (path != null)
toret = subRouter.delete(path);
else
toret = subRouter.delete();
break;
case "OPTIONS":
if (path != null)
toret = subRouter.options(path);
else
toret = subRouter.options();
break;
default:
if (path != null)
toret = subRouter.get(path);
else
toret = subRouter.get();
break;
}
return toret;
}
private static void putHeaders(Map<String, String> headers, RoutingContext rc) {
if (headers != null) {
for (String key : headers.keySet()) {
rc.response().putHeader(key, headers.get(key));
}
}
}
private static void putStatus(RestResponse rr, RoutingContext rc) {
if (rr.getStatusCode() != 200) {
rc.response().setStatusCode(rr.getStatusCode());
if (rr.getStatusMessage() != null && !rr.getStatusMessage().isEmpty())
{
rc.response().setStatusMessage(rr.getStatusMessage());
}
}
}
private static void say(String arg) {
System.out.println(arg);
}
}