All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.almende.eve.rpc.jsonrpc.JSONRPC Maven / Gradle / Ivy

There is a newer version: 3.1.1
Show newest version
package com.almende.eve.rpc.jsonrpc;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import com.almende.eve.agent.AgentInterface;
import com.almende.eve.agent.annotation.Access;
import com.almende.eve.agent.annotation.AccessType;
import com.almende.eve.agent.annotation.Name;
import com.almende.eve.agent.annotation.Required;
import com.almende.eve.agent.annotation.Sender;
import com.almende.eve.rpc.RequestParams;
import com.almende.eve.rpc.jsonrpc.jackson.JOM;
import com.almende.util.AnnotationUtil;
import com.almende.util.AnnotationUtil.AnnotatedClass;
import com.almende.util.AnnotationUtil.AnnotatedMethod;
import com.almende.util.AnnotationUtil.AnnotatedParam;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class JSONRPC {
	// static private Logger logger = Logger.getLogger(JSONRPC.class.getName());

	// TODO: the integration with requestParams is quite a mess.

	// TODO: implement JSONRPC 2.0 Batch
	/**
	 * Invoke a method on an object
	 * 
	 * @param obj
	 *            Request will be invoked on the given object
	 * @param request
	 *            A request in JSON-RPC format
	 * @return
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 */
	static public String invoke(AgentInterface destination, String request)
			throws JsonGenerationException, JsonMappingException, IOException {
		return invoke(destination, request, null);
	}

	/**
	 * Invoke a method on an object
	 * 
	 * @param obj
	 *            Request will be invoked on the given object
	 * @param request
	 *            A request in JSON-RPC format
	 * @param requestParams
	 *            Optional request parameters
	 * @return
	 * @throws IOException
	 * @throws JsonMappingException
	 * @throws JsonGenerationException
	 */
	static public String invoke(AgentInterface destination, String request,
			RequestParams requestParams) throws JsonGenerationException,
			JsonMappingException, IOException {
		JSONRequest jsonRequest = null;
		JSONResponse jsonResponse = null;
		try {
			jsonRequest = new JSONRequest(request);
			jsonResponse = invoke(destination, jsonRequest, requestParams);
		} catch (JSONRPCException err) {
			jsonResponse = new JSONResponse(err);
		}

		return jsonResponse.toString();
	}

	/**
	 * Invoke a method on an object
	 * 
	 * @param sender
	 *            Sender url
	 * @param obj
	 *            will be invoked on the given object
	 * @return
	 */
	static public JSONResponse invoke(AgentInterface destination, JSONRequest request) {
		return invoke(destination, request, null);
	}

	/**
	 * Invoke a method on an object
	 * 
	 * @param obj
	 *            Request will be invoked on the given object
	 * @param request
	 *            A request in JSON-RPC format
	 * @param requestParams
	 *            Optional request parameters
	 * @return
	 */
	static public JSONResponse invoke(AgentInterface destination, JSONRequest request,
			RequestParams requestParams) {
		JSONResponse resp = new JSONResponse();
		resp.setId(request.getId());

		try {
			AnnotatedMethod annotatedMethod = getMethod(destination,
					request.getMethod(), requestParams);
			if (annotatedMethod == null) {
				throw new JSONRPCException(
						JSONRPCException.CODE.METHOD_NOT_FOUND, "Method '"
								+ request.getMethod() + "' not found. The method does not exist or you are not authorized.");
			}

			Method method = annotatedMethod.getActualMethod();

			Object[] params = castParams(request.getParams(),
					annotatedMethod.getParams(), requestParams);
			Object result = method.invoke(destination, params);
			if (result == null) {
				result = JOM.createNullNode();
			}
			resp.setResult(result);
		} catch (Exception err) {
			if (err instanceof JSONRPCException) {
				resp.setError((JSONRPCException) err);
			} else if (err.getCause() != null
					&& err.getCause() instanceof JSONRPCException) {
				resp.setError((JSONRPCException) err.getCause());
			} else {
				err.printStackTrace(); // TODO: cleanup printing stacktrace
				
				JSONRPCException jsonError = new JSONRPCException(
						JSONRPCException.CODE.INTERNAL_ERROR, getMessage(err));
				// TODO: return useful, readable stacktrace
				jsonError.setData(err);
				resp.setError(jsonError);
				
			}
		}

		return resp;
	}

	/**
	 * Validate whether the given class contains valid JSON-RPC methods. A class
	 * if valid when:
* - There are no public methods with equal names
* - The parameters of all public methods have the @Name annotation
* If the class is not valid, an Exception is thrown * * @param c * The class to be verified * @param requestParams * optional request parameters * @return errors A list with validation errors. When no problems are found, * an empty list is returned */ static public List validate(Class c, RequestParams requestParams) { List errors = new ArrayList(); Set methodNames = new HashSet(); AnnotatedClass ac = null; try { ac = AnnotationUtil.get(c); } catch (Exception e) { e.printStackTrace(); errors.add("Class as a whole can't be wrapped for annotation, probably because it's not an AgentInterface implementation."); } if (ac != null) { for (AnnotatedMethod method : ac.getMethods()) { boolean available = false; try { available = isAvailable(method, null, requestParams); } catch (Exception e) { e.printStackTrace(); errors.add("Problems running isAvailable method on annotated class."); } if (available) { // The method name may only occur once String name = method.getName(); if (methodNames.contains(name)) { errors.add("Public method '" + name + "' is defined more than once, which is not" + " allowed for JSON-RPC."); } methodNames.add(name); // each of the method parameters must have the @Name // annotation List params = method.getParams(); for (int i = 0; i < params.size(); i++) { List matches = new ArrayList(); for (Annotation a : params.get(i).getAnnotations()) { if (requestParams != null && requestParams.has(a)) { matches.add(a); } else if (a instanceof Name) { matches.add(a); } } if (matches.size() == 0) { errors.add("Parameter " + i + " in public method '" + name + "' is missing the @Name annotation, which is" + " required for JSON-RPC."); } else if (matches.size() > 1) { String str = ""; for (Annotation a : matches) { str += a + " "; } errors.add("Parameter " + i + " in public method '" + name + "' contains " + matches.size() + " annotations " + "(" + str + "), but only one is allowed."); } } } } } return errors; } /** * Describe all JSON-RPC methods of given class * * @param c * The class to be described * @param requestParams * Optional request parameters. * @param asString * If false (default), the returned description is a JSON * structure. If true, the described methods will be in an easy * to read string. * @return */ public static List describe(Class c, RequestParams requestParams, Boolean asString) { try { Map methods = new TreeMap(); AnnotatedClass annotatedClass = AnnotationUtil.get(c); for (AnnotatedMethod method : annotatedClass.getMethods()) { if (isAvailable(method, null, requestParams)) { if (asString == null || asString != true) { // format as JSON List descParams = new ArrayList(); for (AnnotatedParam param : method.getParams()) { if (getRequestAnnotation(param, requestParams) == null) { String name = getName(param); Map paramData = new HashMap(); paramData.put("name", name); paramData.put("type", typeToString(param.getGenericType())); paramData.put("required", isRequired(param)); descParams.add(paramData); } } Map result = new HashMap(); result.put("type", typeToString(method.getGenericReturnType())); Map desc = new HashMap(); desc.put("method", method.getName()); desc.put("params", descParams); desc.put("result", result); methods.put(method.getName(), desc); } else { // format as string String p = ""; for (AnnotatedParam param : method.getParams()) { if (getRequestAnnotation(param, requestParams) == null) { String name = getName(param); String type = typeToString(param .getGenericType()); if (!p.isEmpty()) { p += ", "; } String ps = type + " " + name; p += isRequired(param) ? ps : ("[" + ps + "]"); } } String desc = typeToString(method .getGenericReturnType()) + " " + method.getName() + "(" + p + ")"; methods.put(method.getName(), desc); } } } // create a sorted array List sortedMethods = new ArrayList(); TreeSet methodNames = new TreeSet(methods.keySet()); for (String methodName : methodNames) { sortedMethods.add(methods.get(methodName)); } return sortedMethods; } catch (Exception e) { e.printStackTrace(); return null; } } /** * Get type description from a class. Returns for example "String" or * "List". * * @param c * @return */ private static String typeToString(Type c) { String s = c.toString(); // replace full namespaces to short names int point = s.lastIndexOf("."); while (point >= 0) { int angle = s.lastIndexOf("<", point); int space = s.lastIndexOf(" ", point); int start = Math.max(angle, space); s = s.substring(0, start + 1) + s.substring(point + 1); point = s.lastIndexOf("."); } // remove modifiers like "class blabla" or "interface blabla" int space = s.indexOf(" "); int angle = s.indexOf("<", point); if (space >= 0 && (angle < 0 || angle > space)) { s = s.substring(space + 1); } return s; /* * // TODO: do some more professional reflection... String s = * c.getSimpleName(); * * // the following seems not to work TypeVariable[] types = * c.getTypeParameters(); if (types.length > 0) { s += "<"; for (int j = * 0; j < types.length; j++) { TypeVariable jj = types[j]; s += * jj.getName(); ... not working //s += * types[j].getClass().getSimpleName(); } s += ">"; } */ } /** * Retrieve a description of an error * * @param error * @return message String with the error description of the cause */ static private String getMessage(Throwable error) { Throwable cause = error; while (cause.getCause() != null) { cause = cause.getCause(); } return cause.toString(); } /** * Find a method by name, which is available for JSON-RPC, and has named * parameters * * @param destination * @param method * @param requestParams * @return methodType meta information on the method, or null if not found */ static private AnnotatedMethod getMethod(AgentInterface destination, String method, RequestParams requestParams) { AnnotatedClass annotatedClass; try { annotatedClass = AnnotationUtil.get(destination.getClass()); List methods = annotatedClass.getMethods(method); for (AnnotatedMethod m : methods) { if (isAvailable(m, destination, requestParams)) { return m; } } } catch (Exception e) { e.printStackTrace(); } return null; } /** * Cast a JSONArray or JSONObject params to the desired paramTypes * * @param params * @param paramTypes * @param requestParams * @return * @throws Exception */ static private Object[] castParams(Object params, List annotatedParams, RequestParams requestParams) throws Exception { ObjectMapper mapper = JOM.getInstance(); if (annotatedParams.size() == 0) { return new Object[0]; } if (params instanceof ObjectNode) { // JSON-RPC 2.0 with named parameters in a JSONObject if (annotatedParams.size() == 1 && annotatedParams.get(0).getType() .equals(ObjectNode.class) && annotatedParams.get(0).getAnnotations().size() == 0) { // the method expects one parameter of type JSONObject // feed the params object itself to it. Object[] objects = new Object[1]; objects[0] = params; return objects; } else { ObjectNode paramsObject = (ObjectNode) params; Object[] objects = new Object[annotatedParams.size()]; for (int i = 0; i < annotatedParams.size(); i++) { AnnotatedParam p = annotatedParams.get(i); Annotation a = getRequestAnnotation(p, requestParams); if (a != null) { // this is a systems parameter objects[i] = requestParams.get(a); } else { String name = getName(p); if (name != null) { // this is a named parameter if (paramsObject.has(name)) { objects[i] = mapper.convertValue( paramsObject.get(name), p.getType()); } else { if (isRequired(p)) { throw new Exception("Required parameter '" + name + "' missing"); } // else if (paramType.getSuperclass() == null) { else if (p.getType().isPrimitive()) { throw new Exception("Parameter '" + name + "' cannot be both optional and " + "a primitive type (" + p.getType().getSimpleName() + ")"); } else { objects[i] = null; } } } else { // this is a problem throw new Exception("Name of parameter " + i + " not defined"); } } } return objects; } } else { throw new Exception("params must be a JSONObject"); } } /** * Create a JSONRequest from a java method and arguments * * @param method * @param args * @return */ public static JSONRequest createRequest(Method method, Object[] args) { AnnotatedMethod annotatedMethod = null; try { annotatedMethod = new AnnotationUtil.AnnotatedMethod(method); } catch (Exception e) { e.printStackTrace(); throw new IllegalArgumentException("Method '" + method.getName() + "' can't be used as annotated method."); } List annotatedParams = annotatedMethod.getParams(); ObjectNode params = JOM.createObjectNode(); for (int i = 0; i < annotatedParams.size(); i++) { AnnotatedParam annotatedParam = annotatedParams.get(i); if (i < args.length && args[i] != null) { String name = getName(annotatedParam); if (name != null) { JsonNode paramValue = JOM.getInstance().convertValue( args[i], JsonNode.class); params.put(name, paramValue); } else { throw new IllegalArgumentException("Parameter " + i + " in method '" + method.getName() + "' is missing the @Name annotation."); } } else if (isRequired(annotatedParam)) { throw new IllegalArgumentException("Required parameter " + i + " in method '" + method.getName() + "' is null."); } } return new JSONRequest(method.getName(), params); } public static boolean hasPrivate(Class clazz) throws SecurityException, Exception{ AnnotatedClass annotated = AnnotationUtil.get(clazz); for (Annotation anno: annotated.getAnnotations()){ if (anno.annotationType().equals(Access.class) && ((Access)anno).value() == AccessType.PRIVATE) return true; if (anno.annotationType().equals(Sender.class)) return true; } return false; } /** * Check whether a method is available for JSON-RPC calls. This is the case * when it is public, has named parameters, and has no annotation * * @Access(UNAVAILABLE) * * @param annotatedMethod * @param requestParams * @return available * @throws Exception * @throws SecurityException */ private static boolean isAvailable(AnnotatedMethod method, AgentInterface destination, RequestParams requestParams) throws SecurityException, Exception { int mod = method.getActualMethod().getModifiers(); Access MethodAccess = method.getAnnotation(Access.class); if (destination != null && !method.getActualMethod().getDeclaringClass().isAssignableFrom(destination.getClass())) return false; if (!(Modifier.isPublic(mod) && hasNamedParams(method, requestParams))) return false; Access ClassAccess = AnnotationUtil.get(destination != null?destination.getClass():method.getActualMethod().getDeclaringClass()) .getAnnotation(Access.class); if (MethodAccess == null) MethodAccess = ClassAccess; if (MethodAccess == null) return true; if (MethodAccess.value() == AccessType.UNAVAILABLE) return false; if (destination != null && MethodAccess.value() == AccessType.PRIVATE) { return destination.onAccess((String) requestParams.get(Sender.class), MethodAccess.tag()); } return true; } /** * Test whether a method has named parameters * * @param annotatedMethod * @param requestParams * @return hasNamedParams */ private static boolean hasNamedParams(AnnotatedMethod method, RequestParams requestParams) { for (AnnotatedParam param : method.getParams()) { boolean found = false; for (Annotation a : param.getAnnotations()) { if (requestParams != null && requestParams.has(a)) { found = true; break; } else if (a instanceof Name) { found = true; break; } } if (!found) { return false; } } return true; } /** * Test if a parameter is required Reads the parameter annotation @Required. * Returns True if the annotation is not provided. * * @param param * @return required */ private static boolean isRequired(AnnotatedParam param) { boolean required = true; Required requiredAnnotation = param.getAnnotation(Required.class); if (requiredAnnotation != null) { required = requiredAnnotation.value(); } return required; } /** * Get the name of a parameter Reads the parameter annotation @Name. Returns * null if the annotation is not provided. * * @param param * @return name */ private static String getName(AnnotatedParam param) { String name = null; Name nameAnnotation = param.getAnnotation(Name.class); if (nameAnnotation != null) { name = nameAnnotation.value(); } return name; } /** * Find a request annotation in the given parameters Returns null if no * system annotation is not found * * @param param * @param requestParams * @return annotation */ private static Annotation getRequestAnnotation(AnnotatedParam param, RequestParams requestParams) { for (Annotation annotation : param.getAnnotations()) { if (requestParams != null && requestParams.has(annotation)) { return annotation; } } return null; } }