Create javascript interface for all methods in cordova plugin

When you create a cordova plugin you need to create execute method and check for action parameter to do different task and in each task you need to create json object and call callbackContext. It would be nice if if you could just create a class with methods and each method is map to javascript method. Here is tutorial how to do this:

You need to create this ReflectService.java file:

package com.example.package;

import android.content.Context;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.PrintWriter;

import java.util.List;
import java.util.ArrayList;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class ReflectService extends CordovaPlugin {

    protected Method getMethod(String name) {
        Class aClass = this.getClass();
        Method[] methods = aClass.getMethods();
        Method method = null;
        for (int i=0; i<methods.length; ++i) {
            if (methods[i].getName().equals(name)) {
                method = methods[i];
                break;
            }
        }
        return method;
    }

    public String[] getMethods() {
        Class aClass = this.getClass();
        Method[] methods = aClass.getDeclaredMethods();
        List<String> list = new ArrayList<String>();
        int len = methods.length;
        String[] result = new String[len];
        for (Method method : methods) {
            String name = method.getName();
            if (Modifier.isPublic(method.getModifiers()) &&
                !name.equals("initialize")) {
                list.add(name);
            }
        }
        return list.toArray(new String[list.size()]);
    }

    protected String[] jsonArrayToString(JSONArray args) {
        String[] result = {};
        try {
            int len = args.length();
            result = new String[len];
            for (int i=0; i<len; ++i) {
                result[i] = args.getString(i);
            }
        } catch (JSONException e) {}
        return result;
    }

    protected String[] getStackTrace(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        return sw.toString().split("\n");
    }

    @Override
    public boolean execute(String action,
                           JSONArray args,
                           final CallbackContext callbackContext) throws JSONException {
        if (!action.equals("execute")) {
            final Method method = this.getMethod(action);
            if (method == null) {
                return false;
            }
            final Object[] arguments = this.jsonArrayToString(args);
            cordova.getThreadPool().execute(new Runnable() {
                public void run() {
                    JSONObject json;
                    Object result;
                    try {
                        result = method.invoke(ReflectService.this, arguments);
                        json = new JSONObject();
                        if (result instanceof Object[]) {
                            json.put("result", new JSONArray(result));
                        } else {
                            json.put("result", result);
                        }
                        json.put("error", null);
                        callbackContext.success(json);
                    } catch(JSONException e) {
                        callbackContext.success();
                    } catch(Exception e) {
                        try {
                            json = new JSONObject();
                            JSONObject error = new JSONObject();
                            error.put("error", "Exception");
                            error.put("code", 200);
                            error.put("message", e.getMessage());
                            String[] trace = ReflectService.this.getStackTrace(e);
                            error.put("trace", new JSONArray(trace));
                            json.put("error", error);
                            json.put("result", null);
                            callbackContext.success(json);
                        } catch(JSONException ee) {
                            callbackContext.success();
                        }
                    }
                }
            });
            return true;
        }
        return false;
    }
}

You probably don’t need to use threads but I’ve seen this in cordova-plugin-shell-exec.

and then you can write your Service like this:

package com.example.package;

import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;

public class Service extends ReflectService {

    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
        super.initialize(cordova, webView);
        // your init code here
    }
    public String echo(String input) {
        if (input.equals("ping")) {
            return "pong";
        } else {
            return null;
        }
    }
}

You will be able to execute each method you create from execute in ReflectService class.

Then you need to create js file to create function for each method in the class. We will use getMethods action (that will execute getMethods method) to get list of methods and create function for each name.

window.Service = function Service(callback) {
    function call(method, args, callback) {
        return cordova.exec(function(response) {
            callback(null, response);
        }, function(error) {
            callback(error);
        }, "Service", method, args || []);
    }
    var service = {};
    call("getMethods", [], function(err, response) {
        if (response.result instanceof Array) {
            response.result.forEach(function(method) {
                service[method] = function() {
                    var args = [].slice.call(arguments);
                    return function(callback) {
                        return call(method, args, function(err, response) {
                            err = err || response.error;
                            if (err) {
                                callback(err);
                            } else {
                                callback(null, response.result);
                            }
                        });
                    };
                };
            });
            callback(service);
        }
    });
};

Next you need to create plugin.xml file that look like this:

<?xml version="1.0" encoding="UTF-8"?>
<plugin id="pl.jcubic.leash.service" version="1.0.0"
        xmlns="http://apache.org/cordova/ns/plugins/1.0">
  <name>Service</name>
  <description>Apache Cordova Leash shell service plugin</description>
  <license>Apache 2.0</license>
  <keywords>cordova,exec,runtime,process,shell,command</keywords>
  <js-module name="service" src="www/service.js">
    <clobbers target="service"/>
  </js-module>
  <platform name="android">
    <config-file parent="/*" target="res/xml/config.xml">
      <feature name="Service">
        <param name="android-package" value="com.example.package.Service" />
        <param name="onload" value="true" />
      </feature>
    </config-file>
    <source-file src="src/com/example/package/Service.java"
                 target-dir="src/com/example/package/Service" />
    <source-file src="src/com/example/package/ReflectService.java"
                 target-dir="src/com/example/package/Service" />
  </platform>
</plugin>

When you add this plugin you can call it in javascript like this:

window.Service(function(service) {
    service.echo('ping')(function(err, result) {
        var pre = document.createElement('pre');
        pre.innerHTML = JSON.stringify(result, null, 4);
        document.body.appendChild(pre);
    });
});

NOTE: Instead of callbacks you can use promises.

You can find whole code in this gist.

Ready to use JSON-RPC Servlet in Java

TL;DR: You can find the whole code in this gist.

I needed to write some Web based project in Java as an assignment, so I decide to use single page application and JSON-RPC service.

Here is the code I use to create this task, step by step

First I found JSON-RPC library, I choose JSON-RPC 2.0 Base library.

I put both JSON Smart and JSON-RPC 2.0 Base jar files into lib directory (inside WEB-INF) of my java app.

In order to use JSON-RPC I needed to get raw POST data from request, I do this using this method (of the Servlet class):

    public String getInputData(HttpServletRequest request) throws IOException {
        ServletInputStream input = request.getInputStream();
        int buff;
        String output = "";
        while (true) {
            buff = input.read();
            if (buff == -1) {
                break;
            }
            output += (char)buff;
        }
        return output;
    }

Then I could parse that POST data (inside doPost Servlet method):

JSONRPC2Request reqIn = JSONRPC2Request.parse(this.getInputData(request));

Now I could extract data from the JSON-RPC request:

     Object id = reqIn.getID();
     Object[] params = reqIn.getPositionalParams().toArray();
     String method_name = reqIn.getMethod();

I decide to use other class as a Service and call method of that class based on JSON-RPC request, so I needed to use Reflection to do that

ClassLoader classLoader = ServletClass.class.getClassLoader();
Object service = classLoader.newInstance();
Class aClass = classLoader.loadClass("pl.jcubic.Service");
Method[] methods = aClass.getMethods();

Method method = null;
for (int i=0; i<methods.length; ++i) {
    if (methods[i].getName().equals(method_name)) {
        method = methods[i];
        break;
    }
}
if (method != null) {
    Object result = method.invoke(service, params);
    JSONRPC2Response respOut = new JSONRPC2Response(result, id);
    out.println(respOut);
} else {
    out.println(new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, id));
}

I use getMethods and search for method instead of using getMethod because I didn’t know what the classes I need to use for the params to get the right method.

I use class loader because I wanted to be able to recompile the Service (pl.jcubic.Service) and use it without the need to restart the app. But this loader don’t work as expected, the class file is loaded only once (when called the first time) the same as if I use import statement.

In order to have dynamic class load I need to write custom class loader, it look like this:

    class ServiceReloader extends ClassLoader {
        public ServiceReloader(ClassLoader parent) {
            super(parent);
        }

        public Class loadClass(String name) throws ClassNotFoundException {
            if (!"pl.jcubic.Service".equals(name)) {
                return super.loadClass(name);
            }
            try {
                String path = getClassPath(name);

                DataInputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream(new File(path))));

                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                while (true) {
                    int data = input.read();
                    if (data == -1) {
                        break;
                    } 
                    buffer.write(data);
                }
                byte[] classData = buffer.toByteArray();
                return defineClass(name, classData, 0, classData.length);
            } catch (IOException e) {
                throw new ClassNotFoundException(e.getMessage());
            }
        }
    }

I write it as inner class of my Servlet. Then I use this class to load my Service class.

    ClassLoader parentClassLoader = ServiceReloader.class.getClassLoader();
    ServiceReloader classLoader = new ServiceReloader(parentClassLoader);
    Class aClass = classLoader.loadClass("pl.jcubic.Service");

Now I could recompile the Service class and use it from JavaScript without need to restart the app (don’t even refresh the browser if I write new method and wanted to test it)

One more thing that need to be written is catch exceptions and display proper JSON-RPC error responses.

Now all you need is a class with methods that will become your JSON-RPC methods. Here is list of Java types mapend to JSON-RPC types. All paramters and return values of your methods need to be of those types.