/** * license (MIT) Copyright Nubisa Inc. 2014 */ package jxm; import com.google.gson.Gson; import com.google.gson.internal.LinkedTreeMap; import javax.crypto.*; import javax.crypto.spec.*; import java.security.*; import java.lang.reflect.Method; import java.util.*; /** * jxm.io Java Client */ public class Client { private class Disconnector extends Thread { Client dc; public Disconnector(Client c) { super(c.clientId + "-Disconnect"); dc = c; } @Override public void run() { try { Thread.sleep(1); } catch (Exception e) { } if (dc.Events != null) dc.Events.OnClose(dc); } } private String applicationPath = ""; private Object classToCall; private String clientId = null; boolean closed = false; private boolean encrypted = false; /** * The ClientEvents object for listening of client's events. * @custom.example * <pre> * [code language="java" smarttabs="true"] * jxm.ClientEvents events = new ClientEvents(){ * @Override public void OnErrorReceived(Client c, String Message) { * //Error received * } * @Override public void OnClientConnected(Client c) { * //Client is connected * } * @Override public void OnClientDisconnected(Client c) { * //Client is disconnected * } * @Override public void OnEventLog(Client c, String log, LogLevel level) { * //get the event log from here * } * }; * //now we may define this listener into our Client instance * client.Events = event; * [/code] * </pre> */ public ClientEvents Events = null; private boolean isConnected = false; private boolean isListenerActive = false; private boolean isSecure = false; private PListen listener = null; private HashMap<String, Method> methodsOfCall = new HashMap<String, Method>(); private PListen send = null; private int socketPort = 8000; private String socketURL = null; @SuppressWarnings("rawtypes") private Class typeToCall; /** * Creates an instance of JXCore Java Client. * * @param localTarget The local object will be answering the calls from server. i.e new Test() * @param appName Application Name * @param appKey Secure Application Key * @param url JXcore server URL i.e. sampledomain.com or 120.1.2.3 * @param port Server port * @param secure Server SSL support * * @custom.example * <pre> * [code language="java" smarttabs="true"] * // let's create a client instance * Client client = new Client(new CustomMethods(), "channels", * "NUBISA-STANDARD-KEY-CHANGE-THIS", "localhost", 8000, false); * [/code] * </pre> */ public Client(Object localTarget, String appName, String appKey, String url, int port, boolean secure) { socketURL = url; socketPort = port; isSecure = secure; applicationPath = appName; applicationKey = PListen.getUID(false) + "|" + appName; setSecureKey(appKey); this.classToCall = localTarget; if (localTarget != null && CustomMethodsBase.class.isAssignableFrom(localTarget.getClass())) { ((CustomMethodsBase)localTarget).SetClient(this); } } /** * Creates an instance of JXcore Java Client. * * @param localTarget The local object will be answering the calls from server. i.e new Test() * @param appName Application Name * @param appKey Secure Application Key * @param url JXcore server URL i.e. sampledomain.com or 120.1.2.3 * @param port Server port * @param secure Server SSL support * @param resetUID Reset the unique instance id (session id) * @custom.example * <pre> * [code language="java" smarttabs="true"] * // let's create a client instance * Client client = new Client(new CustomMethods(), "channels", * "NUBISA-STANDARD-KEY-CHANGE-THIS", "localhost", 8000, false, true); * [/code] * </pre> */ public Client(Object localTarget, String appName, String appKey, String url, int port, boolean secure, boolean resetUID) { socketURL = url; socketPort = port; isSecure = secure; applicationPath = appName; applicationKey = PListen.getUID(resetUID) + "|" + appName; setSecureKey(appKey); this.classToCall = localTarget; if (localTarget != null && CustomMethodsBase.class.isAssignableFrom(localTarget.getClass())) { ((CustomMethodsBase)localTarget).SetClient(this); } } private String applicationKey; private String securedKey = null; private void setSecureKey(String key){ securedKey = encrypt(key, applicationKey); } public String encrypt(String key, String message){ try{ byte[] input = message.toString().getBytes("utf-8"); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] thedigest = md.digest(key.getBytes("UTF-8")); SecretKeySpec skc = new SecretKeySpec(thedigest, "AES"); Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, skc); byte[] cipherText = new byte[cipher.getOutputSize(input.length)]; int ctLength = cipher.update(input, 0, input.length, cipherText, 0); ctLength += cipher.doFinal(cipherText, ctLength); String str = Base64.encode(cipherText); str = PListen.escape( str ).replace("+", "**43;"); return str; }catch(Exception e){ if(Events!=null){Events.OnError(this, e.getMessage());} return null; } } /** * Establishes the connection on a separate thread. */ public void AsyncConnect() { Thread thread = new Thread() { @Override public void run() { Connect(); } }; thread.start(); } /** * Closes Client and disconnects from server. */ public void Close() { goClose(); } /** * Closes Client and disconnects from server. */ public void goClose() { if (closed) return; closed = true; fireLog("Closing connection", LogLevel.Informative); if (this.getIsConnected()) { listener.Dispose(); send.exit = true; this.isConnected = false; if (Events != null) { new Disconnector(this).start(); } } } /** * Subscribes the client to a group, or channel. From now on, messages sent to that group * by any other subscriber will be received by the client. * Also the client himself can send messages to this group - see jxcore.SendToGroup() method. * @param group Name of the group, to which the client is subscribing. * @param cb This is client's callback, which will be called after server will subscribe the client to the group. * @throws Exception * @custom.example * <pre> * [code language="java" smarttabs="true"] * try { * client.Subscribe("programmers", new Callback() { * @Override * public void call(Object o) throws Exception { * System.out.println("Subscribed to " + o.toString()); * client.SendToGroup("programmers", "clientMethod", * "Hello from client!"); * } * }); * } catch (Exception e) { * System.out.println("Cannot subscribe."); * } * [/code] * </pre> */ public void Subscribe(final String group, final Callback cb) throws Exception { if (group != null) { Map<String, Object> map = new HashMap<String, Object>(); map.put("gr", group); map.put("en", enc); this.Call("nb.ssTo", map, new Callback() { @Override public void call(Object o, Integer err) throws Exception { JSON js = (JSON) o; if (err == 0) { onSub(js.getValue("key").toString()); lastMessId = js.getValue("did").toString(); } if (cb != null) { cb.call(group, err); } } }); } else { Integer errCode = 6; /* must be non-empty string */ if (cb != null) { cb.call(group, errCode); } else { throw new Exception("Error no " + errCode); } } } private void onSub(String en){ enc = en; } private String enc = null; /** * Unsubscribes the client from a group, or channel. From now on, messages sent to that group cannot be received by this client. * @param group {string} - Name of the group, from which the client is unsubscribing. * @param cb {function} - This is client's callback, which will be called after server will unsubscribe the client to the group. * @throws Exception * @custom.example * <pre> * [code language="java" smarttabs="true"] * try { * client.Unubscribe("programmers", new Callback() { * @Override * public void call(Object o) throws Exception { * System.out.println("Unubscribed from " + o.toString()); * } * }); * } catch (Exception e) { * System.out.println("Cannot unubscribe."); * } * [/code] * </pre> */ public void Unsubscribe(final String group, final Callback cb) throws Exception { if (enc == null) { return; } if (group != null) { Map<String, Object> map = new HashMap<String, Object>(); map.put("gr", group); map.put("en", enc); this.Call("nb.unTo", map, new Callback() { @Override public void call(Object o, Integer err) throws Exception { JSON js = (JSON) o; if (err == 0) { onSub(js.getValue("key").toString()); } if (cb != null) { cb.call(group, err); } } }); } else { Integer errCode = 6; /* must be non-empty string */ if (cb != null) { cb.call(group, errCode); } else { throw new Exception("Error no " + errCode); } } } /** * Sends message to all clients, that have already subscribed to the specific group. * @param groupName {string} - Name of the group, to which message should be sent. * @param methodName {string} - Client's custom method, which should be invoked of each of the group subscribers. * @param params {object} - The argument for that method. * @custom.example * <pre> * [code language="java" smarttabs="true"] * // The "addText" method should be available on every client, which is subscribed to * // "programmers" group. * // While invoking the "addText" method at each client, the server will pass * // "Hello from client!" string as an argument. * cli.SendToGroup("programmers", "addText", "Hello from client!"); * [/code] * </pre> */ public void SendToGroup(String groupName, String methodName, Object params, Callback cb){ Map<String, Object> map = new HashMap<String, Object>(); map.put("gr", groupName); map.put("m", methodName); map.put("j", params); map.put("key", enc); this.Call("nb.stGr", map, cb); } /** * Starts the client. Connects to the server. * @return true/false based on the result. * @custom.example * <pre> * [code language="java" smarttabs="true"] * // we will try to connect now * if (client.Connect()) { * System.out.println("ready!"); * } * [/code] * </pre> */ public boolean Connect() { if (isConnected) { errorMessage("JXcore Client is already connected."); return false; } if (closed) { errorMessage("Once a Client is disconnected you may not use the same instance to reconnect back."); return false; } fireLog("Connecting to server", LogLevel.Informative); this.Initialize(); if(getSecuredKey() == null) return false; String connStr = socketURL.concat(":" + socketPort + "/" + applicationPath + "/jx?ms=connect&de=1&sid=" + getSecuredKey() + "&a"); if(!connStr.startsWith("http")) { if (isSecure) { connStr = "https://" + connStr; } else { connStr = "http://" + connStr; } }else{ if(connStr.startsWith("https")) isSecure = true; else isSecure = false; } String str = send.downloadString(connStr); boolean end = false; if (str != null && str != "") { String [] arr = str.split("\\|"); if(arr.length<2){ end = true; }else{ clientId = arr[0]; try { encrypted = Boolean.parseBoolean(arr[1]); } catch (Exception e) { errorMessage("Couldn't connect to server. more:" + e.getMessage()); return false; } isConnected = true; } } else end = true; if(end){ errorMessage("Couldn't connect to server. Check URL for service."); return false; } fireLog("Connection script parsed. Starting to listen.", LogLevel.Informative); // let listener start before OnClientConnected boolean ret = Listen(); // if(Events!=null){ // Events.OnClientConnected(this); // } return ret; } public boolean getIsSecure(){ return isSecure; } private void errorMessage(String message) { if (Events != null) Events.OnError(this, message); } public final class JSON { LinkedTreeMap<String, Object> obj = null; public JSON(String json, boolean isArray){ try{ if(!isArray){ obj = (LinkedTreeMap<String, Object>)aParser.fromJson(json, Object.class); }else{ array = aParser.fromJson(json, Object[].class); } initialized = true; } catch(Exception e){ initialized = false; if(Events != null){ Events.OnError(null, e.getMessage()); } } } private boolean initialized = false; public boolean isInitialized(){ return initialized; } private JSON(LinkedTreeMap<String, Object> o){ obj = o; } private Gson aParser = new Gson(); private Object [] array = null; public void toArray(){ if(obj==null) return; array = obj.values().toArray(); } public int size(){ if(array==null) return 0; return array.length; } public JSON getItem(int index){ if(array==null) return null; return new JSON((LinkedTreeMap<String, Object>)array[index]); } public boolean containsKey(String key){ if(obj==null) return false; return obj.containsKey(key); } public JSON getItem(String key){ if(obj==null) return null; return new JSON((LinkedTreeMap<String, Object>)obj.get(key)); } public boolean isKeyObject(String key){ if(obj==null) return false; if(!obj.containsKey(key)){ return false; } return obj.get(key) instanceof LinkedTreeMap; } public Object getValue(String key){ if(obj == null) return null; return obj.get(key); } } private String lastMessId = null; private void Eval(String msg) { msg = msg.replace(":null", ":'null'"); fireLog("evaluating message:" + msg, LogLevel.Informative); JSON json = new JSON("[" + msg + "]", true); int size = json.size(); for(int i=0;i<size;i++){ JSON js = json.getItem(i); if(js.containsKey("i")){ Object oi = js.getValue("i"); if(oi!=null){ lastMessId = oi.toString(); } } if(js.containsKey("o")){ js = js.getItem("o"); String methodName = null; String strIndex = null; if(js.containsKey("m")){ methodName = js.getValue("m").toString(); } else if(js.containsKey("i")){ strIndex = js.getValue("i").toString(); } else continue; Object param = null; if(js.containsKey("p")){ if(js.isKeyObject("p")){ param = js.getItem("p"); }else { param = js.getValue("p"); } } if(methodName != null && (methodName.contains("jxcore.Listen") || methodName.contains("jxcore.Close"))){ if(methodName.contains("jxcore.Close")) this.goClose(); } else if (strIndex!=null){ try{ float fl = Float.valueOf(strIndex).floatValue(); int n = (int)fl; if (n < 0) { ssCall(n, param); } else { Integer err = 0; if (JSON.class.isAssignableFrom(param.getClass())) { JSON p = (JSON)param; Object nb_err = p.containsKey("nb_err") ? p.getValue("nb_err") : null; if (nb_err != null) { float fl1 = Float.valueOf(nb_err.toString()).floatValue(); err = (int)fl1; } } if(callbacks.size()>n-1) { callbacks.get(n-1).call(param, err); callbacks.set(n-1, null); } } }catch(Exception e){ errorMessage("CallbackInvoke at (" + strIndex + ") :" + e.getMessage()); } } else if (classToCall != null && methodsOfCall.containsKey(methodName)) { try { methodsOfCall.get(methodName).invoke(classToCall, param); } catch (Exception e) { errorMessage("MethodInvoke (" + methodName + ") :" + e.getMessage()); } } else { fireLog("Method " + methodName + " wasn't exist on target object.", LogLevel.Critical); } } } json = null; } // server-side call private void ssCall(int id, Object param) { if (id==-1) { JSON js = (JSON)param; Object key = js.getValue("key"); Object did = js.getValue("did"); if (key != null) { onSub(key.toString()); } if (did != null) { lastMessId = did.toString(); } if (Events != null) { Events.OnSubscription(this,js.getValue("su").toString().toLowerCase() == "true", js.getValue("gr").toString()); } } } /** * Fires log event * * @param log * @param level */ public void fireLog(String log, LogLevel level) { if (Events != null) { Events.OnEventLog(this, log, level); } } /** * Gets unique id of the client. */ public String GetClientId() { return clientId; } public boolean getEncrypted() { return encrypted; } public boolean getIsConnected() { return isConnected; } public int getSocketPort() { return socketPort; } public String getSocketURL() { return socketURL; } public String getApplicationPath(){ return applicationPath; } private void Initialize() { fireLog("Initializing Client", LogLevel.Informative); listener = new PListen("listen", this); send = new PListen("send", this); final Client dc = this; listener.notifier = new PEvents() { @Override public void ErrorReceived(String message) { errorMessage(message); } @Override public void MessageReceived(String message) { messageReceived(message); } @Override public void UpdateIsConnected(boolean connected) { isConnected = connected; fireLog("Connection state is updated to " + connected, LogLevel.Informative); if (Events != null) { if (connected) Events.OnConnect(dc); else { if (!closed) Events.OnClose(dc); } } } }; send.notifier = listener.notifier; isListenerActive = false; if (classToCall != null) { typeToCall = classToCall.getClass(); Method[] methods = typeToCall.getMethods(); int ln = methods.length; for (int i = 0; i < ln; i++) { Method method = methods[i]; methodsOfCall.put(method.getName(), method); } } } private boolean Listen() { if (isListenerActive) return false; fireLog("Entering Listener Thread", LogLevel.Informative); isListenerActive = true; listener.start(); return true; } private void messageReceived(String message) { Eval(message); } public String getSecuredKey(){ return securedKey; } private String createJSON(String methodName, Object params, Callback callback){ StringBuilder sb = new StringBuilder(); sb.append("{"); if(methodName != null){ sb.append("\"m\":\"" + methodName + "\""); } if(params!=null){ sb.append(",\"p\":" + new Gson().toJson(params)); } if(callback!=null){ callbacks.add(callback); sb.append(",\"i\":" + callbacks.size()); } sb.append("}"); return sb.toString(); } static List<Callback> callbacks = new ArrayList<Callback>(); /** * Invokes specific custom method defined on the server-side. * @param methodName The name of custom method defined at the backend. It should contain also class definer name. i.e. MyClass.MyMethod. * @param params The argument for that method. * @param callback This is client's callback, which will be called after server completes invoking it's custom method. This parameter is optional. * @throws java.lang.UnsupportedOperationException * @custom.example * <pre> * [code language="java" smarttabs="true"] * // let's call the server-side method "serverMethod" from the client-side! * // in turn, as a response, the backend service will invoke * // client's local "callback" defined above! * client.Call("serverMethod", "Hello", callback); * [/code] * </pre> */ public void Call(String methodName, Object params, Callback callback) throws java.lang.UnsupportedOperationException { if (!this.getIsConnected()) return; String sb = createJSON(methodName, params, callback); if(getSecuredKey() == null) return; String connStr = socketURL.concat(":" + socketPort + "/" + applicationPath + "/jx?de=1&"); if(isSecure){ connStr = "https://" + connStr; }else{ connStr = "http://" + connStr; } connStr = connStr.concat("c=" + clientId + "&sid="+securedKey+"&co=" + ((Long) (new Date().getTime())).toString()); String mess = PListen.CreateText(this, sb, false); synchronized (sendList){ sendList.add(new SendQueue(methodName, connStr, mess)); } if(sendDone){ sendDone = false; sendThread = new Thread() { @Override public void run() { try{ while(true) { synchronized (sendList){ if(sendList.isEmpty()){ sendDone = true; break; } } sendFromQueue(); } }finally{ sendDone = true; } } }; sendThread.start(); } } Thread sendThread = null; boolean sendDone = true; private class SendQueue { public String methodName; public String connStr; public String mess; public SendQueue(String a, String b, String c) { methodName = a; connStr = b; mess = c; } } private Queue<SendQueue> sendList = new ArrayDeque<SendQueue>(); private void sendFromQueue() { SendQueue q = null; synchronized (sendList){ q = sendList.poll(); } if(q==null){ return; } String mess = q.mess; String connStr = q.connStr; String methodName = q.methodName; String result = null; if (!listener.socketEnabled()) { result = send.downloadString(connStr, "ms=" + mess); } else { listener.socketSend(mess); } if (result != null) { result = result.trim(); fireLog(result + " received for methodCall " + methodName, LogLevel.Informative); if (result.startsWith("/**/")) { if (result.contains("jxcore.Closed()")) { this.goClose(); } } else Eval(result); } } }