package com.alecgorge.minecraft.jsonapi.dynamic; import java.io.File; import java.io.FileReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Logger; import org.bukkit.plugin.Plugin; import org.json.simpleForBukkit.JSONArray; import org.json.simpleForBukkit.JSONObject; import org.json.simpleForBukkit.parser.JSONParser; import com.alecgorge.minecraft.jsonapi.JSONAPI; import com.alecgorge.minecraft.jsonapi.api.APIMethodName; import com.alecgorge.minecraft.jsonapi.api.JSONAPICallHandler; import com.alecgorge.minecraft.jsonapi.dynamic.Method; public class Caller implements JSONAPIMethodProvider { public HashMap<String, HashMap<String, Method>> methods = new HashMap<String, HashMap<String, Method>>(); private JSONParser p = new JSONParser(); private JSONAPI inst; private Logger outLog = JSONAPI.instance.outLog; public int methodCount = 0; private List<JSONAPICallHandler> handlers = new ArrayList<JSONAPICallHandler>(); private List<JSONAPIMethodProvider> objectsToCheck = new ArrayList<JSONAPIMethodProvider>(); public Caller(JSONAPI plugin) { inst = plugin; registerMethods(this); } public Object call(String methodAndNamespace, final Object[] params) throws Throwable { String[] methodParts = methodAndNamespace.split("\\.", 2); APIMethodName n = new APIMethodName(methodAndNamespace); for (JSONAPICallHandler c : handlers) { if (c.willHandle(n)) { return c.handle(n, params); } } checkObjects(); final Call c; if(methods.get("").containsKey(methodAndNamespace)) { c = methods.get("").get(methodAndNamespace).getCall(); } else if (methodParts.length == 1) { c = methods.get("").get(methodParts[0]).getCall(); } else { c = methods.get(methodParts[0]).get(methodParts[1]).getCall(); } if (params.length < c.getNumberOfExpectedArgs()) { throw new Exception("Incorrect number of args: gave " + params.length + " (" + Arrays.asList(params).toString() + "), expected " + c.getNumberOfExpectedArgs() + " for method " + methodAndNamespace); } return innerCall(c, params); } private void checkObjects() { for (JSONAPIMethodProvider o : objectsToCheck) { for (java.lang.reflect.Method m : o.getClass().getMethods()) { if (m.isAnnotationPresent(API_Method.class)) { API_Method a = m.getAnnotation(API_Method.class); if (methods.get(a.namespace()) == null) { methods.put(a.namespace(), new HashMap<String, Method>()); } // System.out.println(String.format("adding %s.%s: %s %s", a.namespace(), a.name().isEmpty() ? m.getName() : a.name(), o,m)); methods.get(a.namespace()).put(a.name().isEmpty() ? m.getName() : a.name(), new Method(o, m, a)); } } } objectsToCheck = new ArrayList<JSONAPIMethodProvider>(); } private Object innerCall(final Call c, final Object[] params) throws Throwable { Future<Object> f = inst.getServer().getScheduler().callSyncMethod(inst, new Callable<Object>() { public Object call() throws Exception { return c.call(params); } }); try { return f.get(); } catch (InterruptedException e) { return f.get(); } catch (ExecutionException e) { throw e.getCause(); } } @API_Method(namespace = "jsonapi", argumentDescriptions = { "The name of the method to test. Should be a FQN. Ex: dynmap.getHost or getPlayers" }, isProvidedByV2API=false) public boolean methodExists(String name) { if(methods.get("").containsKey(name)) { return true; } String[] methodParts = name.split("\\.", 2); APIMethodName n = new APIMethodName(name); for (JSONAPICallHandler c : handlers) { if (c.willHandle(n)) { return true; } } checkObjects(); if(methods.get("").containsKey(name)) { return true; } else if (methodParts.length == 1) { return methods.get("").containsKey(methodParts[0]); } else { return methods.containsKey(methodParts[0]) && methods.get(methodParts[0]).containsKey(methodParts[1]); } } @API_Method(namespace = "jsonapi", isProvidedByV2API=false) public HashMap<String, List<String>> getMethods() { HashMap<String, List<String>> r = new HashMap<String, List<String>>(); for (String key : methods.keySet()) { r.put(key, new ArrayList<String>(methods.get(key).keySet())); } return r; } @API_Method( namespace = "", name = "jsonapi.methods.all.for_namespace", returnDescription = "Returns details about all the API methods currently available for a given namespace. You should use `jsonapi.methods` unless you want to work with custom methods or ones loaded in a specific namespace." ) public List<Method> getMethodsForNamespace(String namespace) { return new ArrayList<Method>(methods.get(namespace).values()); } @API_Method( namespace = "", name = "jsonapi.methods.all.namespaces", returnDescription = "Returns a list of all the namespaces currently loaded on the server." ) public List<String> getMethodNamespaces() { return new ArrayList<String>(methods.keySet()); } @API_Method( namespace = "", name = "jsonapi.methods.all", returnDescription = "Returns details about all API methods in all namespaces. You should use `jsonapi.methods` unless you want to work with custom methods or ones loaded in a specific namespace." ) public Map<String, List<Method>> getAllMethodInfo() { Map<String, List<Method>> m = new HashMap<String, List<Method>>(); for (String key : methods.keySet()) { m.put(key, getMethodsForNamespace(key)); } return m; } @API_Method( namespace = "", name = "jsonapi.methods", returnDescription = "Returns details about API methods provided by default with JSONAPI." ) public List<Method> getAllV2APIMethods() { List<Method> m = new ArrayList<Method>(); for (String key : methods.keySet()) { for (Method meth : methods.get(key).values()) { if (meth.isProvidedByV2API()) { m.add(meth); } } } Collections.sort(m, new Comparator<Method>() { @Override public int compare(Method o1, Method o2) { return o1.getName().compareTo(o2.getName()); } }); return m; } public List<String> getAllMethods() { List<String> r = new ArrayList<String>(); for (String key : methods.keySet()) { r.addAll(new ArrayList<String>(methods.get(key).keySet())); } Collections.sort(r); return r; } public void loadFile(File methodsFile, boolean jsonapi4) { try { magicWithMethods(p.parse(new FileReader(methodsFile)), jsonapi4); } catch (Exception e) { e.printStackTrace(); } } public void loadInputStream(InputStream methodsFile, boolean jsonapi4) { try { magicWithMethods(p.parse(new InputStreamReader(methodsFile)), jsonapi4); } catch (Exception e) { e.printStackTrace(); } } public void registerAPICallHandler(JSONAPICallHandler handler) { handlers.add(handler); } public void registerMethods(JSONAPIMethodProvider o) { objectsToCheck.add(o); } public void deregisterAPICallHandler(JSONAPICallHandler handler) { handlers.remove(handler); } private void magicWithMethods(Object raw, boolean jsonapi4) throws Exception { if (raw instanceof JSONObject) { JSONObject methods = (JSONObject) raw; String name = ""; if (methods.containsKey("name")) { name = methods.get("name").toString(); } else { throw new Exception("A JSON file is not well formed: missing the key 'name' for the root object."); } if (methods.containsKey("depends")) { Object deps = methods.get("depends"); List<String> pluginNames = new ArrayList<String>(); if (deps instanceof JSONArray) { for (Object o : ((JSONArray) deps)) { pluginNames.add(o.toString()); } } else { pluginNames.add(deps.toString()); } for (String plugin : pluginNames) { plugin = plugin.trim(); Plugin p = inst.getServer().getPluginManager().getPlugin(plugin); if (p == null && !plugin.equals("JSONAPI")) { outLog.info("[JSONAPI] " + name + " cannot be loaded because it depends on a plugin that is not installed: '" + plugin + "'"); } else if (plugin.equals("JSONAPI") || p.isEnabled()) { if (methods.containsKey("methods")) { proccessMethodsWithNamespace((JSONArray) methods.get("methods"), methods.containsKey("namespace") ? methods.get("namespace").toString() : "", jsonapi4); } else { throw new Exception("A JSON file is not well formed: missing the key 'methods' for the root object."); } } else { outLog.info("[JSONAPI] " + name + " cannot be loaded because it depends on a plugin that is not enabled: '" + plugin + "'"); } } } } else if (raw instanceof JSONArray) { proccessMethodsWithNamespace((JSONArray) raw, "", jsonapi4); } else { throw new Exception("JSON file is not a valid methods file."); } } public void proccessMethodsWithNamespace(JSONArray methods, String namespace, boolean jsonapi4) { for (Object o : methods) { if (o instanceof JSONObject) { String name = ((JSONObject) o).get("name").toString(); if (this.methods.containsKey(name)) { Logger.getLogger("Minecraft").info("[JSONAPI] The method " + name + " already exists! It is being overridden."); } if (!this.methods.containsKey(namespace)) { this.methods.put(namespace, new HashMap<String, Method>()); } methodCount++; this.methods.get(namespace).put(name, new Method((JSONObject) o, jsonapi4)); } } } public void loadString(String methodsString, boolean jsonapi4) { try { magicWithMethods(p.parse(methodsString), jsonapi4); } catch (Exception e) { e.printStackTrace(); } } }