// ---------------------------------------------------------------------------
// jWebSocket - RPCPlugIn Plug-In
// Copyright (c) 2010 Alexander Schulze, Innotrade GmbH
// ---------------------------------------------------------------------------
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by the
// Free Software Foundation; either version 3 of the License, or (at your
// option) any later version.
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
// more details.
// You should have received a copy of the GNU Lesser General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
// ---------------------------------------------------------------------------
package org.jwebsocket.plugins.rpc;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.Map.Entry;
import javolution.util.FastMap;
import org.apache.log4j.Logger;
import org.json.JSONObject;
import org.jwebsocket.api.WebSocketConnector;
import org.jwebsocket.api.WebSocketEngine;
import org.jwebsocket.config.JWebSocketConfig;
import org.jwebsocket.config.JWebSocketServerConstants;
import org.jwebsocket.factory.JWebSocketJarClassLoader;
import org.jwebsocket.logging.Logging;
import org.jwebsocket.kit.PlugInResponse;
import org.jwebsocket.plugins.TokenPlugIn;
import org.jwebsocket.security.SecurityFactory;
import org.jwebsocket.token.Token;
/**
* This plug-in provides all the functionality for remote procedure calls
* (RPC) for client-to-server (C2S) apps, and reverse remote procedure calls
* (RRPC) for server-to-client (S2C) or client-to-client apps (C2C).
* @author aschulze
*/
public class RPCPlugIn extends TokenPlugIn {
private static Logger mLog = Logging.getLogger(RPCPlugIn.class);
private Map<String, Object> mGrantedProcs = new FastMap<String, Object>();
// private DemoRPCServer mRpcServer = null;
// if namespace changed update client plug-in accordingly!
private String NS_RPC_DEFAULT = JWebSocketServerConstants.NS_BASE + ".plugins.rpc";
// private Map<String, String> mJars = new FastMap<String, String>();
// TODO: use RpcCallable instead of Object here!
private Map<String, Object> mClasses = new FastMap<String, Object>();
// TODO: RRPC calls do not yet allow quotes in arguments
// TODO: We need simple unique IDs to address a certain target, session id not suitable here.
// TODO: Show target(able) clients in a drop down box
// TODO: RPC demo does not show other clients logging in
/**
*
*/
public RPCPlugIn() {
if (mLog.isDebugEnabled()) {
mLog.debug("Instantiating rpc plug-in...");
}
// specify default name space
this.setNamespace(NS_RPC_DEFAULT);
// currently this is the only supported RPCPlugIn server
// mRpcServer = new DemoRPCServer();
}
@Override
public void engineStarted(WebSocketEngine aEngine) {
// TODO: move JWebSocketJarClassLoader into ServerAPI module ?
JWebSocketJarClassLoader lClassLoader = new JWebSocketJarClassLoader();
Class lClass = null;
// load map of RPC libraries first
// also load map of granted procs
Map<String, String> lSettings = getSettings();
String lKey;
String lValue;
for (Entry<String, String> lSetting : lSettings.entrySet()) {
lKey = lSetting.getKey();
lValue = lSetting.getValue();
if (lKey.startsWith("class:")) {
String lClassName = lKey.substring(6);
try {
if (mLog.isDebugEnabled()) {
mLog.debug("Trying to load class '" + lClassName + "' from classpath...");
}
lClass = Class.forName(lClassName);
} catch (Exception ex) {
mLog.error(ex.getClass().getSimpleName()
+ " loading class from classpath: "
+ ex.getMessage()
+ ", hence trying to load from jar.");
}
// if class could not be loaded from classpath...
if (lClass == null) {
String lJarFilePath = null;
try {
lJarFilePath = JWebSocketConfig.getLibraryFolderPath(lValue);
if (mLog.isDebugEnabled()) {
mLog.debug("Trying to load class '" + lClassName + "' from jar '" + lJarFilePath + "'...");
}
lClassLoader.addFile(lJarFilePath);
lClass = lClassLoader.loadClass(lClassName);
if (mLog.isDebugEnabled()) {
mLog.debug("Class '" + lClassName + "' successfully loaded from '" + lJarFilePath + "'.");
}
} catch (Exception ex) {
mLog.error(ex.getClass().getSimpleName() + " loading jar '" + lJarFilePath + "': " + ex.getMessage());
}
}
// could the class be loaded?
if (lClass != null) {
try {
Object lInstance = lClass.newInstance();
mClasses.put(lClassName, lInstance);
} catch (Exception ex) {
mLog.error(ex.getClass().getSimpleName() + " creating '" + lClassName + "' instance : " + ex.getMessage());
}
}
}
}
for (Entry<String, String> lSetting : lSettings.entrySet()) {
lKey = lSetting.getKey();
lValue = lSetting.getValue();
if (lKey.startsWith("roles:")) {
lKey = lKey.substring(6);
mGrantedProcs.put(lKey, lValue);
}
}
if (mLog.isDebugEnabled()) {
mLog.debug("Available RPC classes: " + mClasses.toString());
mLog.debug("Granted RPC methods: " + mGrantedProcs.toString());
}
}
/*
@Override
public void connectorStarted(WebSocketConnector aConnector) {
}
*/
@Override
public void processToken(PlugInResponse aResponse, WebSocketConnector aConnector, Token aToken) {
String lType = aToken.getType();
String lNS = aToken.getNS();
if (lType != null && (lNS == null || lNS.equals(getNamespace()))) {
// remote procedure call
if (lType.equals("rpc")) {
rpc(aConnector, aToken);
// reverse remote procedure call
} else if (lType.equals("rrpc")) {
rrpc(aConnector, aToken);
}
}
}
/**
* remote procedure call (RPC)
* @param aConnector
* @param aToken
*/
public void rpc(WebSocketConnector aConnector, Token aToken) {
// check if user is allowed to run 'rpc' command
if (!SecurityFactory.checkRight(getUsername(aConnector), NS_RPC_DEFAULT + ".rpc")) {
sendToken(aConnector, aConnector, createAccessDenied(aToken));
return;
}
Token lResponseToken = createResponse(aToken);
String lClassName = aToken.getString("classname");
String lMethod = aToken.getString("method");
Object lArgs = aToken.get("args");
// TODO: Tokens should always be a map of maps
if (lArgs instanceof JSONObject) {
lArgs = new Token((JSONObject) lArgs);
}
String lMsg = null;
if (mLog.isDebugEnabled()) {
mLog.debug("Processing RPC to class '" + lClassName + "', method '" + lMethod + "', args: '" + lArgs + "'...");
}
String lKey = lClassName + "." + lMethod;
if (mGrantedProcs.containsKey(lKey)) {
// class is ignored until security restrictions are finished.
try {
// TODO: use RpcCallable here!
Object lInstance = mClasses.get(lClassName);
if (lInstance != null) {
Object lObj = call(lInstance, lMethod, lArgs);
lResponseToken.put("result", lObj);
} else {
lMsg = "Class '" + lClassName + "' not found or not properly loaded.";
}
} catch (NoSuchMethodException ex) {
lMsg = "NoSuchMethodException calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage();
} catch (IllegalAccessException ex) {
lMsg = "IllegalAccessException calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage();
} catch (InvocationTargetException ex) {
lMsg = "InvocationTargetException calling '" + lMethod + "' for class " + lClassName + ": " + ex.getMessage();
}
} else {
lMsg = "Call to " + lKey + " is not granted!";
}
if (lMsg != null) {
lResponseToken.put("code", -1);
lResponseToken.put("msg", lMsg);
}
/* just for testing purposes of multi-threaded rpc's
try {
Thread.sleep(3000);
} catch (InterruptedException ex) {
}
*/
sendToken(aConnector, aConnector, lResponseToken);
}
/**
* reverse remote procedure call (RRPC)
* @param aConnector
* @param aToken
*/
public void rrpc(WebSocketConnector aConnector, Token aToken) {
// check if user is allowed to run 'rrpc' command
if (!SecurityFactory.checkRight(getUsername(aConnector), NS_RPC_DEFAULT + ".rrpc")) {
sendToken(aConnector, aConnector, createAccessDenied(aToken));
return;
}
String lNS = aToken.getNS();
// get the target
String lTargetId = aToken.getString("targetId");
// get the remote classname
String lClassname = aToken.getString("classname");
// get the remote method name
String lMethod = aToken.getString("method");
// get the remote arguments
String lArgs = aToken.getString("args");
// TODO: find solutions for hardcoded engine id
WebSocketConnector lTargetConnector =
getServer().getConnector("tcp0", lTargetId);
if (mLog.isDebugEnabled()) {
mLog.debug("Processing 'rrpc'...");
}
if (lTargetConnector != null) {
Token lRRPC = new Token(lNS, "rrpc");
lRRPC.put("classname", lClassname);
lRRPC.put("method", lMethod);
lRRPC.put("args", lArgs);
lRRPC.put("sourceId", aConnector.getRemotePort());
sendToken(aConnector, lTargetConnector, lRRPC);
} else {
Token lResponse = createResponse(aToken);
lResponse.put("code", -1);
lResponse.put("error", "Target " + lTargetId + " not found.");
sendToken(aConnector, aConnector, lResponse);
}
}
/**
*
* @param aClassName
* @param aURL
* @return
*/
public static Class loadClass(String aClassName, String aURL) {
Class lClass = null;
try {
URLClassLoader lUCL = new URLClassLoader(new URL[]{new URL(aURL)});
// load class using previously defined class loader
lClass = Class.forName(aClassName, true, lUCL);
if (mLog.isDebugEnabled()) {
mLog.debug("Class '" + lClass.getName() + "' loaded!");
}
} catch (ClassNotFoundException ex) {
mLog.error("Class not found exception: " + ex.getMessage());
} catch (MalformedURLException ex) {
mLog.error("MalformesURL exception: " + ex.getMessage());
}
return lClass;
}
/**
*
* @param aClassName
* @return
*/
public static Class loadClass(String aClassName) {
// return loadClass(aClassName, "file:/" + JWebSocketServerConstants.CLASS_OUT_PATH);
return null;
}
/**
*
* @param aClass
* @param aArgs
* @return
*/
public static Object createInstance(Class aClass, Object[] aArgs) {
Object lObj = null;
try {
Class[] lCA = new Class[aArgs != null ? aArgs.length : 0];
for (int i = 0; i < lCA.length; i++) {
lCA[i] = aArgs[i].getClass();
}
Constructor lConstructor = aClass.getConstructor(lCA);
lObj = lConstructor.newInstance(aArgs);
if (mLog.isDebugEnabled()) {
mLog.debug("Object '" + aClass.getName() + "' instantiated!");
}
} catch (Exception ex) {
mLog.error("Exception instantiating class " + aClass.getName() + ": " + ex.getMessage());
}
return lObj;
}
/**
*
* @param aClass
* @return
*/
public static Object createInstance(Class aClass) {
return createInstance(aClass, null);
}
/**
*
* @param aClassName
* @return
*/
public static Object createInstance(String aClassName) {
Class lClass = loadClass(aClassName);
return createInstance(lClass, null);
}
/**
*
* @param aClassName
* @param aArgs
* @return
*/
public static Object createInstance(String aClassName, Object[] aArgs) {
Class lClass = loadClass(aClassName);
return createInstance(lClass, aArgs);
}
/**
*
* @param aInstance
* @param aName
* @param aArgs
* @return
* @throws NoSuchMethodException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
public static Object call(Object aInstance, String aName, Object aArgs)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Object lObj = null;
Class lClass = aInstance.getClass();
Class[] lCA;
if (aArgs != null) {
lCA = new Class[]{aArgs.getClass()};
} else {
lCA = new Class[0];
}
Method lMethod = lClass.getMethod(aName, lCA);
Object lArg = aArgs;
lObj = lMethod.invoke(aInstance, lArg);
return lObj;
}
/*
public static Object call(Object aInstance, String aName, Object... aArgs) {
Object lObj = null;
try {
Class lClass = aInstance.getClass();
Method lMethod = lClass.getMethod(aName, new Class[]{Object.class});
Object lArg = aArgs;
lObj = lMethod.invoke(aInstance, lArg);
} catch (NoSuchMethodException ex) {
log.debug("No such method exception calling '" + aName + "' for class " + aInstance.getClass().getName() + ": " + ex.getMessage());
} catch (Exception ex) {
log.debug("Exception calling '" + aName + "' for class " + aInstance.getClass().getName() + ": " + ex.getMessage());
}
return lObj;
}
*/
}