/*
* jabsorb - a Java to JavaScript Advanced Object Request Broker
* http://www.jabsorb.org
*
* Copyright 2007 The jabsorb team
*
* based on original code from
* JSON-RPC-Java - a JSON-RPC to Java Bridge with dynamic invocation
*
* Copyright Metaparadigm Pte. Ltd. 2004.
* Michael Clark <michael@metaparadigm.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.jabsorb;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import org.jabsorb.callback.CallbackController;
import org.jabsorb.callback.InvocationCallback;
import org.jabsorb.localarg.LocalArgController;
import org.jabsorb.localarg.LocalArgResolver;
import org.jabsorb.reflect.ClassAnalyzer;
import org.jabsorb.reflect.ClassData;
import org.jabsorb.reflect.MethodKey;
import org.jabsorb.serializer.MarshallException;
import org.jabsorb.serializer.ObjectMatch;
import org.jabsorb.serializer.Serializer;
import org.jabsorb.serializer.SerializerState;
import org.jabsorb.serializer.UnmarshallException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* This class implements a bridge that unmarshalls JSON objects in JSON-RPC
* request format, invokes a method on the exported object, and then marshalls
* the resulting Java objects to JSON objects in JSON-RPC result format.
* </p>
* <p>
* There is a global bridge singleton object that allows exporting classes and
* objects to all HTTP clients. In addition to this, an instance of the
* JSONRPCBridge can optionally be placed in a users' HttpSession object
* registered under the attribute "JSONRPCBridge" to allow exporting of classes
* and objects to specific users. A session specific bridge will delegate
* requests for objects it does not know about to the global singleton
* JSONRPCBridge instance.
* </p>
* <p>
* Using session specific bridge instances can improve the security of
* applications by allowing exporting of certain objects only to specific
* HttpSessions as well as providing a convenient mechanism for JavaScript
* clients to access stateful data associated with the current user.
* </p>
* <p>
* You can create a HttpSession specific bridge in JSP with the usebean tag:
* </p>
* <code><jsp:useBean id="JSONRPCBridge" scope="session"
* class="org.jabsorb.JSONRPCBridge" /></code>
* <p>
* Then export an object for your JSON-RPC client to call methods on:
* </p>
* <code>JSONRPCBridge.registerObject("test", testObject);</code>
* <p>
* This will make available all public methods of the object as
* <code>test.<methodnames></code> to JSON-RPC clients. This approach
* should generally be performed after an authentication check to only export
* objects to clients that are authorised to use them.
* </p>
* <p>
* Alternatively, the global bridge singleton object allows exporting of classes
* and objects to all HTTP clients. It can be fetched with
* <code>JSONRPCBridge.getGlobalBridge()</code>.
* </p>
* <p>
* To export all public instance methods of an object to <b>all</b> clients:
* </p>
* <code>JSONRPCBridge.getGlobalBridge().registerObject("myObject",
* myObject);</code>
* <p>
* To export all public static methods of a class to <b>all</b> clients:
* </p>
* <code>JSONRPCBridge.getGlobalBridge().registerClass("MyClass",
* com.example.MyClass.class);</code>
*/
public class JSONRPCBridge implements Serializable
{
/**
* Used to determine whether two methods match
* TODO: There ought to be a better way of doing this!
*/
protected static class MethodCandidate
{
/**
* The method
*/
Method method;
/**
* The match data for each parameter of the method.
*/
ObjectMatch match[];
/**
* Creatse a new MethodCandidate
*
* @param method The method for this candidate
*/
public MethodCandidate(Method method)
{
this.method = method;
match = new ObjectMatch[method.getParameterTypes().length];
}
/**
* Gets an object Match for the method.
*
* @return An object match with the amount of mismatches
*/
public ObjectMatch getMatch()
{
int mismatch = -1;
for (int i = 0; i < match.length; i++)
{
mismatch = Math.max(mismatch, match[i].getMismatch());
}
if (mismatch == -1)
{
return ObjectMatch.OKAY;
}
return new ObjectMatch(mismatch);
}
}
/**
* Container for objects of which instances have been made
*/
protected static class ObjectInstance implements Serializable
{
/**
* Unique serialisation id.
*/
private final static long serialVersionUID = 2;
/**
* The object for the instance
*/
protected Object o;
/**
* The class the object is of
*/
protected Class clazz;
/**
* Creates a new ObjectInstance
*
* @param o The object for the instance
*/
public ObjectInstance(Object o)
{
this.o = o;
clazz = o.getClass();
}
/**
* Creates a new ObjectInstance
*
* @param o The object for the instance
* @param clazz The class the object is of
*/
public ObjectInstance(Object o, Class clazz)
{
if (!clazz.isInstance(o))
{
throw new ClassCastException(
"Attempt to register jsonrpc object with invalid class.");
}
this.o = o;
this.clazz = clazz;
}
}
/**
* Unique serialisation id.
*/
private final static long serialVersionUID = 2;
/**
* A simple transformer that makes no change
*/
private static final ExceptionTransformer IDENTITY_EXCEPTION_TRANSFORMER = new ExceptionTransformer()
{
/**
* Unique serialisation id.
*/
private final static long serialVersionUID = 2;
public Object transform(Throwable t)
{
return t;
}
};
/**
* The logger for this class
*/
private final static Logger log = LoggerFactory
.getLogger(JSONRPCBridge.class);
/**
* Global bridge (for exporting to all users)
*/
private final static JSONRPCBridge globalBridge = new JSONRPCBridge();
/**
* Global JSONSerializer instance
*/
private static JSONSerializer ser = new JSONSerializer();
static
{
try
{
ser.registerDefaultSerializers();
}
catch (Exception e)
{
e.printStackTrace();
}
}
/**
* This method retrieves the global bridge singleton. <p/> It should be used
* with care as objects should generally be registered within session specific
* bridges for security reasons.
*
* @return returns the global bridge object.
*/
public static JSONRPCBridge getGlobalBridge()
{
return globalBridge;
}
/**
* Get the global JSONSerializer object.
*
* @return the global JSONSerializer object.
*/
public static JSONSerializer getSerializer()
{
return ser;
}
/* Inner classes */
/**
* Registers a Class to be removed from the exported method signatures and
* instead be resolved locally using context information from the transport.
*
* @param argClazz The class to be resolved locally
* @param argResolver The user defined class that resolves the and returns the
* method argument using transport context information
* @param contextInterface The type of transport Context object the callback
* is interested in eg. HttpServletRequest.class for the servlet
* transport
*/
public static void registerLocalArgResolver(Class argClazz,
Class contextInterface, LocalArgResolver argResolver)
{
LocalArgController.registerLocalArgResolver(argClazz, contextInterface,
argResolver);
}
/**
* Set the global JSONSerializer object.
*
* @param ser the global JSONSerializer object.
*/
public static void setSerializer(JSONSerializer ser)
{
JSONRPCBridge.ser = ser;
}
/* Implementation */
/**
* Unregisters a LocalArgResolver</b>.
*
* @param argClazz The previously registered local class
* @param argResolver The previously registered LocalArgResolver object
* @param contextInterface The previously registered transport Context
* interface.
*/
public static void unregisterLocalArgResolver(Class argClazz,
Class contextInterface, LocalArgResolver argResolver)
{
LocalArgController.unregisterLocalArgResolver(argClazz, contextInterface,
argResolver);
}
/**
* Creates a signature for an array of arguments
*
* @param arguments The argumnts
* @return A comma seperated string listing the arguments
*/
private static String argSignature(JSONArray arguments)
{
StringBuilder buf = new StringBuilder();
for (int i = 0; i < arguments.length(); i += 1)
{
if (i > 0)
{
buf.append(',');
}
Object jso;
try
{
jso = arguments.get(i);
}
catch (JSONException e)
{
throw (NoSuchElementException)new NoSuchElementException(e.getMessage()).initCause(e);
}
if (jso == null)
{
buf.append("java.lang.Object");
}
else if (jso instanceof String)
{
buf.append("java.lang.String");
}
else if (jso instanceof Number)
{
buf.append("java.lang.Number");
}
else if (jso instanceof JSONArray)
{
buf.append("java.lang.Object[]");
}
else
{
buf.append("java.lang.Object");
}
}
return buf.toString();
}
/**
* Display a method call argument signature for a method as a String for
* debugging/logging purposes. The string contains the comma separated list of
* argument types that the given method takes.
*
* @param method Method instance to display the argument signature for.
* @return the argument signature for the method, as a String.
*/
private static String argSignature(Method method)
{
Class param[] = method.getParameterTypes();
StringBuilder buf = new StringBuilder();
for (int i = 0; i < param.length; i++)
{
if (i > 0)
{
buf.append(',');
}
buf.append(param[i].getName());
}
return buf.toString();
}
/**
* Create unique method names by appending the given prefix to the keys from
* the given HashMap and adding them all to the given HashSet.
*
* @param m HashSet to add unique methods to.
* @param prefix prefix to append to each method name found in the methodMap.
* @param methodMap a HashMap containing MethodKey keys specifying methods.
*/
private static void uniqueMethods(HashSet m, String prefix, HashMap methodMap)
{
Iterator i = methodMap.entrySet().iterator();
while (i.hasNext())
{
Map.Entry mentry = (Map.Entry) i.next();
MethodKey mk = (MethodKey) mentry.getKey();
m.add(prefix + mk.getMethodName());
}
}
/**
* The functor used to convert exceptions
*/
private ExceptionTransformer exceptionTransformer = IDENTITY_EXCEPTION_TRANSFORMER;
/**
* Bridge state
*/
private JSONRPCBridgeState state = new JSONRPCBridgeState(this);
/**
* The callback controller
*/
private CallbackController cbc = null;
/**
* Call a method using a JSON-RPC request object.
*
* @param context The transport context (the HttpServletRequest object in the
* case of the HTTP transport).
* @param jsonReq The JSON-RPC request structured as a JSON object tree.
* @return a JSONRPCResult object with the result of the invocation or an
* error.
*/
public JSONRPCResult call(Object context[], JSONObject jsonReq)
{
String encodedMethod;
Object requestId;
JSONArray arguments;
JSONArray fixups;
try
{
// Get method name, arguments and request id
encodedMethod = jsonReq.getString("method");
arguments = jsonReq.getJSONArray("params");
requestId = jsonReq.opt("id");
fixups = jsonReq.optJSONArray("fixups");
}
catch (JSONException e)
{
log.error("no method or parameters in request");
return new JSONRPCResult(JSONRPCResult.CODE_ERR_NOMETHOD, null,
JSONRPCResult.MSG_ERR_NOMETHOD);
}
if (log.isDebugEnabled())
{
if (fixups != null)
{
log.debug("call " + encodedMethod + "(" + arguments + ")"
+ ", requestId=" + requestId);
}
else
{
log.debug("call " + encodedMethod + "(" + arguments + ")"
+ ", fixups=" + fixups + ", requestId=" + requestId);
}
}
// apply the fixups (if any) to the parameters. This will result
// in a JSONArray that might have circular references-- so
// the toString method (or anything that internally tries to traverse
// the JSON (without being aware of this) should not be called after this point
if (fixups != null)
{
try
{
for (int i=0; i < fixups.length(); i++)
{
JSONArray assignment = fixups.getJSONArray(i);
JSONArray fixup = assignment.getJSONArray(0);
JSONArray original = assignment.getJSONArray(1);
applyFixup(arguments, fixup, original);
}
}
catch (JSONException e)
{
log.error("error applying fixups",e);
return new JSONRPCResult(JSONRPCResult.CODE_ERR_FIXUP, requestId,
JSONRPCResult.MSG_ERR_FIXUP + ": " + e.getMessage()) ;
}
}
String className = null;
String methodName = null;
int objectID = 0;
// Parse the class and methodName
StringTokenizer t = new StringTokenizer(encodedMethod, ".");
if (t.hasMoreElements())
{
className = t.nextToken();
}
if (t.hasMoreElements())
{
methodName = t.nextToken();
}
// See if we have an object method in the format ".obj#<objectID>"
if (encodedMethod.startsWith(".obj#"))
{
t = new StringTokenizer(className, "#");
t.nextToken();
objectID = Integer.parseInt(t.nextToken());
}
// one of oi or cd will resolve (first oi is attempted, and if that fails,
// then cd is attempted)
// object instance of object being invoked
ObjectInstance oi = null;
// ClassData for resolved object instance, or if object instance cannot
// resolve, class data for
// class instance (static method) we are resolving to
ClassData cd = null;
HashMap methodMap = null;
Method method = null;
Object itsThis = null;
if (objectID == 0)
{
// Handle "system.listMethods"
// this is called by the browser side javascript
// when a new JSONRpcClient object is initialized.
if (encodedMethod.equals("system.listMethods"))
{
HashSet m = new HashSet();
globalBridge.allInstanceMethods(m);
if (globalBridge != this)
{
globalBridge.allStaticMethods(m);
globalBridge.allInstanceMethods(m);
}
allStaticMethods(m);
allInstanceMethods(m);
JSONArray methods = new JSONArray();
Iterator i = m.iterator();
while (i.hasNext())
{
methods.put(i.next());
}
return new JSONRPCResult(JSONRPCResult.CODE_SUCCESS, requestId, methods);
}
// Look up the class, object instance and method objects
if (className == null
|| methodName == null
|| ((oi = resolveObject(className)) == null && (cd = resolveClass(className)) == null))
{
return new JSONRPCResult(JSONRPCResult.CODE_ERR_NOMETHOD, requestId,
JSONRPCResult.MSG_ERR_NOMETHOD);
}
if (oi != null)
{
itsThis = oi.o;
cd = ClassAnalyzer.getClassData(oi.clazz);
methodMap = cd.getMethodMap();
}
else
{
if (cd != null)
{
methodMap = cd.getStaticMethodMap();
}
}
}
else
{
if ((oi = resolveObject(Integer.valueOf(objectID))) == null)
{
return new JSONRPCResult(JSONRPCResult.CODE_ERR_NOMETHOD, requestId,
JSONRPCResult.MSG_ERR_NOMETHOD);
}
itsThis = oi.o;
cd = ClassAnalyzer.getClassData(oi.clazz);
methodMap = cd.getMethodMap();
// Handle "system.listMethods"
// this is called by the browser side javascript
// when a new JSONRpcClient object with an objectID is initialized.
if (methodName != null && methodName.equals("listMethods"))
{
HashSet m = new HashSet();
uniqueMethods(m, "", cd.getStaticMethodMap());
uniqueMethods(m, "", cd.getMethodMap());
JSONArray methods = new JSONArray();
Iterator i = m.iterator();
while (i.hasNext())
{
methods.put(i.next());
}
return new JSONRPCResult(JSONRPCResult.CODE_SUCCESS, requestId, methods);
}
}
// Find the specific method
if ((method = resolveMethod(methodMap, methodName, arguments)) == null)
{
return new JSONRPCResult(JSONRPCResult.CODE_ERR_NOMETHOD, requestId,
JSONRPCResult.MSG_ERR_NOMETHOD);
}
JSONRPCResult result;
// Call the method
try
{
if (log.isDebugEnabled())
{
log.debug("invoking " + method.getReturnType().getName() + " "
+ method.getName() + "(" + argSignature(method) + ")");
}
// Unmarshall arguments
Object javaArgs[] = unmarshallArgs(context, method, arguments);
// Call pre invoke callbacks
if (cbc != null)
{
for (int i = 0; i < context.length; i++)
{
cbc.preInvokeCallback(context[i], itsThis, method, javaArgs);
}
}
// Invoke the method
Object returnObj = method.invoke(itsThis, javaArgs);
// Call post invoke callbacks
if (cbc != null)
{
for (int i = 0; i < context.length; i++)
{
cbc.postInvokeCallback(context[i], itsThis, method, returnObj);
}
}
// Marshall the result
SerializerState serializerState = new SerializerState();
Object json = ser.marshall(serializerState, null, returnObj, "r");
result = new JSONRPCResult(JSONRPCResult.CODE_SUCCESS, requestId,
json, serializerState.getFixUps());
// Handle exceptions creating exception results and
// calling error callbacks
}
catch (UnmarshallException e)
{
if (cbc != null)
{
for (int i = 0; i < context.length; i++)
{
cbc.errorCallback(context[i], itsThis, method, e);
}
}
log.error("exception occured",e);
result = new JSONRPCResult(JSONRPCResult.CODE_ERR_UNMARSHALL, requestId,
e.getMessage());
}
catch (MarshallException e)
{
if (cbc != null)
{
for (int i = 0; i < context.length; i++)
{
cbc.errorCallback(context[i], itsThis, method, e);
}
}
log.error("exception occured",e);
result = new JSONRPCResult(JSONRPCResult.CODE_ERR_MARSHALL, requestId, e
.getMessage());
}
catch (Throwable e)
{
if (e instanceof InvocationTargetException)
{
e = ((InvocationTargetException) e).getTargetException();
}
if (cbc != null)
{
for (int i = 0; i < context.length; i++)
{
cbc.errorCallback(context[i], itsThis, method, e);
}
}
log.error("exception occured",e);
result = new JSONRPCResult(JSONRPCResult.CODE_REMOTE_EXCEPTION,
requestId, exceptionTransformer.transform(e));
}
// Return the results
return result;
}
/**
* Get the JSONRPCBridgeState object associated with this bridge.
*
* @return the JSONRPCBridgeState object associated with this bridge.
*/
public JSONRPCBridgeState getBridgeState()
{
return state;
}
/**
* Get the CallbackController object associated with this bridge.
*
* @return the CallbackController object associated with this bridge.
*/
public CallbackController getCallbackController()
{
return cbc;
}
/**
* Gets the map of referenced objects used by this bridge. <p/> The reference
* map contains objects of classes that have been registered as a Reference or
* CallableReference.
*
* @return a HashMap with the references currently in use on this bridge
* instance.
*/
public HashMap getReferenceMap()
{
return state.getReferenceMap();
}
/**
* Check whether a class is registered as a callable reference type.
*
* @param clazz The class object to check is a callable reference.
* @return true if it is, false otherwise
*/
public boolean isCallableReference(Class clazz)
{
if (this == globalBridge)
{
return false;
}
HashSet callableReferenceSet = state.getCallableReferenceSet();
if (callableReferenceSet == null)
{
return false;
}
Class parentClazz = clazz;
while (parentClazz != null) {
if (callableReferenceSet.contains(parentClazz))
{
return true;
}
parentClazz = parentClazz.getSuperclass();
}
return globalBridge.isCallableReference(clazz);
}
/**
* Check whether a class is registered as a reference type.
*
* @param clazz The class object to check is a reference.
* @return true if it is, false otherwise.
*/
public boolean isReference(Class clazz)
{
if (this == globalBridge)
{
return false;
}
HashSet referenceSet = state.getReferenceSet();
if (referenceSet == null)
{
return false;
}
if (referenceSet.contains(clazz))
{
return true;
}
return globalBridge.isReference(clazz);
}
/**
* Lookup a class that is registered with this bridge.
*
* @param name The registered name of the class to lookup.
* @return the class for the name
*/
public Class lookupClass(String name)
{
synchronized (state)
{
HashMap classMap = state.getClassMap();
return (Class) classMap.get(name);
}
}
/**
* Lookup an object that is registered with this bridge.
*
* @param key The registered name of the object to lookup.
* @return The object desired if it exists, else null.
*/
public Object lookupObject(Object key)
{
synchronized (state)
{
HashMap objectMap = state.getObjectMap();
ObjectInstance oi = (ObjectInstance) objectMap.get(key);
if (oi != null)
{
return oi.o;
}
}
return null;
}
/**
* <p>
* Registers a class to be returned as a callable reference.
* </p>
* <p>
* The JSONBridge will return a callable reference to the JSON-RPC client for
* registered classes instead of passing them by value. The JSONBridge will
* take a references to these objects and the JSON-RPC client will create an
* invocation proxy for objects of this class for which methods will be called
* on the instance on the server.
* </p>
* <p>
* <b>Note:</b> A limitation exists in the JSON-RPC client where only the top
* most object returned from a method can be made into a proxy.
* </p>
* <p>
* A Callable Reference in JSON format looks like this:
* </p>
* <code>{ "javaClass":"org.jabsorb.test.Bar",<br />
* "objectID":4827452,<br /> "JSONRPCType":"CallableReference" }</code>
*
* @param clazz The class object that should be marshalled as a callable
* reference.
* @throws Exception If the bridge is global, callable references cannot be
* added
*/
public void registerCallableReference(Class clazz) throws Exception
{
if (this == globalBridge)
{
throw new Exception("Can't register callable reference on global bridge");
}
synchronized (state)
{
if (state.getReferenceSerializer() == null)
{
state.enableReferences();
}
state.getCallableReferenceSet().add(clazz);
}
if (log.isDebugEnabled())
{
log.debug("registered callable reference " + clazz.getName());
}
}
/**
* Registers a callback to be called before and after method invocation
*
* @param callback The object implementing the InvocationCallback Interface
* @param contextInterface The type of transport Context interface the
* callback is interested in eg. HttpServletRequest.class for the
* servlet transport.
*/
public void registerCallback(InvocationCallback callback,
Class contextInterface)
{
if (cbc == null)
{
cbc = new CallbackController();
}
cbc.registerCallback(callback, contextInterface);
}
/**
* Registers a class to export static methods. <p/> The JSONBridge will export
* all static methods of the class. This is useful for exporting factory
* classes that may then return CallableReferences to the JSON-RPC client.
* <p/> Calling registerClass for a clazz again under the same name will have
* no effect. <p/> To export instance methods you need to use registerObject.
*
* @param name The name to register the class with.
* @param clazz The class to export static methods from.
* @throws Exception If a class is already registed with this name
*/
public void registerClass(String name, Class clazz) throws Exception
{
synchronized (state)
{
HashMap classMap = state.getClassMap();
Class exists = (Class) classMap.get(name);
if (exists != null && exists != clazz)
{
throw new Exception("different class registered as " + name);
}
if (exists == null)
{
classMap.put(name, clazz);
}
}
if (log.isDebugEnabled())
{
log.debug("registered class " + clazz.getName() + " as " + name);
}
}
/**
* Registers an object to export all instance methods and static methods. <p/>
* The JSONBridge will export all instance methods and static methods of the
* particular object under the name passed in as a key. <p/> This will make
* available all methods of the object as
* <code><key>.<methodnames></code> to JSON-RPC clients. <p/>
* Calling registerObject for a name that already exists will replace the
* existing entry.
*
* @param key The named prefix to export the object as
* @param o The object instance to be called upon
*/
public void registerObject(Object key, Object o)
{
ObjectInstance oi = new ObjectInstance(o);
synchronized (state)
{
HashMap objectMap = state.getObjectMap();
objectMap.put(key, oi);
}
if (log.isDebugEnabled())
{
log.debug("registered object " + o.hashCode() + " of class "
+ o.getClass().getName() + " as " + key);
}
}
/**
* Registers an object to export all instance methods defined by
* interfaceClass. <p/> The JSONBridge will export all instance methods
* defined by interfaceClass of the particular object under the name passed in
* as a key. <p/> This will make available these methods of the object as
* <code><key>.<methodnames></code> to JSON-RPC clients.
*
* @param key The named prefix to export the object as
* @param o The object instance to be called upon
* @param interfaceClass The type that this object should be registered as.
* <p/> This can be used to restrict the exported methods to the
* methods defined in a specific superclass or interface.
*/
public void registerObject(Object key, Object o, Class interfaceClass)
{
ObjectInstance oi = new ObjectInstance(o, interfaceClass);
synchronized (state)
{
HashMap objectMap = state.getObjectMap();
objectMap.put(key, oi);
}
if (log.isDebugEnabled())
{
log.debug("registered object " + o.hashCode() + " of class "
+ interfaceClass.getName() + " as " + key);
}
}
/**
* Registers a class to be returned by reference and not by value as is done
* by default. <p/> The JSONBridge will take a references to these objects and
* return an opaque object to the JSON-RPC client. When the opaque object is
* passed back through the bridge in subsequent calls, the original object is
* substitued in calls to Java methods. This should be used for any objects
* that contain security information or complex types that are not required in
* the Javascript client but need to be passed as a reference in methods of
* exported objects. <p/> A Reference in JSON format looks like this: <p/>
* <code>{ "javaClass":"org.jabsorb.test.Foo",<br />
* "objectID":5535614,<br /> "JSONRPCType":"Reference" }</code>
*
* @param clazz The class object that should be marshalled as a reference.
* @throws Exception If the bridge is global, callable references cannot be
* added
*/
public void registerReference(Class clazz) throws Exception
{
if (this == globalBridge)
{
throw new Exception("Can't register reference on global bridge");
}
synchronized (state)
{
if (state.getReferenceSerializer() == null)
{
state.enableReferences();
}
state.getReferenceSet().add(clazz);
}
if (log.isDebugEnabled())
{
log.debug("registered reference " + clazz.getName());
}
}
/**
* Register a new serializer on this bridge.
*
* @param serializer A class implementing the Serializer interface (usually
* derived from AbstractSerializer).
* @throws Exception If a serialiser has already been registered that
* serialises the same class
*/
public void registerSerializer(Serializer serializer) throws Exception
{
ser.registerSerializer(serializer);
}
/**
* Set the JSONRPCBridgeState object for this bridge.
*
* @param state the JSONRPCBridgeState object to be set for this bridge.
*/
public void setBridgeState(JSONRPCBridgeState state)
{
this.state = state;
}
/**
* Set the CallbackController object for this bridge.
*
* @param cbc the CallbackController object to be set for this bridge.
*/
public void setCallbackController(CallbackController cbc)
{
this.cbc = cbc;
}
/**
* Sets the exception transformer for the bridge.
*
* @param exceptionTransformer The new exception transformer to use.
*/
public void setExceptionTransformer(ExceptionTransformer exceptionTransformer)
{
this.exceptionTransformer = exceptionTransformer;
}
/**
* Unregisters a callback
*
* @param callback The previously registered InvocationCallback object
* @param contextInterface The previously registered transport Context
* interface.
*/
public void unregisterCallback(InvocationCallback callback,
Class contextInterface)
{
if (cbc == null)
{
return;
}
cbc.unregisterCallback(callback, contextInterface);
}
/**
* Unregisters a class exported with registerClass. <p/> The JSONBridge will
* unexport all static methods of the class.
*
* @param name The registered name of the class to unexport static methods
* from.
*/
public void unregisterClass(String name)
{
synchronized (state)
{
HashMap classMap = state.getClassMap();
Class clazz = (Class) classMap.get(name);
if (clazz != null)
{
classMap.remove(name);
if (log.isDebugEnabled())
{
log.debug("unregistered class " + clazz.getName() + " from " + name);
}
}
}
}
/**
* Unregisters an object exported with registerObject. <p/> The JSONBridge
* will unexport all instance methods and static methods of the particular
* object under the name passed in as a key.
*
* @param key The named prefix of the object to unexport
*/
public void unregisterObject(Object key)
{
synchronized (state)
{
HashMap objectMap = state.getObjectMap();
ObjectInstance oi = (ObjectInstance) objectMap.get(key);
if (oi.o != null)
{
objectMap.remove(key);
if (log.isDebugEnabled())
{
log.debug("unregistered object " + oi.o.hashCode() + " of class "
+ oi.clazz.getName() + " from " + key);
}
}
}
}
/**
* Add all instance methods that can be invoked on this bridge to a HashSet.
*
* @param m HashSet to add all static methods to.
*/
private void allInstanceMethods(HashSet m)
{
synchronized (state)
{
HashMap objectMap = state.getObjectMap();
Iterator i = objectMap.entrySet().iterator();
while (i.hasNext())
{
Map.Entry oientry = (Map.Entry) i.next();
Object key = oientry.getKey();
if (!(key instanceof String))
{
continue;
}
String name = (String) key;
ObjectInstance oi = (ObjectInstance) oientry.getValue();
ClassData cd = ClassAnalyzer.getClassData(oi.clazz);
uniqueMethods(m, name + ".", cd.getMethodMap());
uniqueMethods(m, name + ".", cd.getStaticMethodMap());
}
}
}
/**
* Add all static methods that can be invoked on this bridge to the given
* HashSet.
*
* @param m HashSet to add all static methods to.
*/
private void allStaticMethods(HashSet m)
{
synchronized (state)
{
HashMap classMap = state.getClassMap();
Iterator i = classMap.entrySet().iterator();
while (i.hasNext())
{
Map.Entry cdentry = (Map.Entry) i.next();
String name = (String) cdentry.getKey();
Class clazz = (Class) cdentry.getValue();
ClassData cd = ClassAnalyzer.getClassData(clazz);
uniqueMethods(m, name + ".", cd.getStaticMethodMap());
}
}
}
/**
* Apply one fixup assigment to the incoming json arguments.
*
* WARNING: the resultant "fixed up" arguments may contain circular references
* after this operation. That is the whole point of course-- but the JSONArray and JSONObject's
* themselves aren't aware of circular references when certain methods are called (e.g. toString)
* so be careful when handling these circular referenced json objects.
*
* @param arguments the json arguments for the incoming json call.
* @param fixup the fixup entry.
* @param original the original value to assign to the fixup.
* @throws org.json.JSONException if invalid or unexpected fixup data is encountered.
*/
private void applyFixup(JSONArray arguments, JSONArray fixup, JSONArray original) throws JSONException
{
int last = fixup.length()-1;
if (last<0)
{
throw new JSONException("fixup path must contain at least 1 reference");
}
Object originalObject = traverse(arguments, original, false);
Object fixupParent = traverse(arguments, fixup, true);
// the last ref in the fixup needs to be created
// it will be either a string or number depending on if the fixupParent is a JSONObject or JSONArray
if (fixupParent instanceof JSONObject)
{
String objRef = fixup.optString(last,null);
if (objRef == null)
{
throw new JSONException("last fixup reference not a string");
}
((JSONObject)fixupParent).put(objRef, originalObject);
}
else
{
int arrRef = fixup.optInt(last,-1);
if (arrRef==-1)
{
throw new JSONException("last fixup reference not a valid array index");
}
((JSONArray)fixupParent).put(arrRef, originalObject);
}
}
/**
* Traverse a list of references to find the target reference in an original or fixup list.
*
* @param origin origin JSONArray (arguments) to begin traversing at.
* @param refs JSONArray containing array integer references and or String object references.
* @param fixup if true, stop one short of the traversal chain to return the parent of the fixup
* rather than the fixup itself (which will be non-existant)
* @return either a JSONObject or JSONArray for the Object found at the end of the traversal.
* @throws JSONException if something unexpected is found in the data
*/
private Object traverse(JSONArray origin, JSONArray refs, boolean fixup) throws JSONException
{
try
{
JSONArray arr = origin;
JSONObject obj = null;
// where to stop when traversing
int stop = refs.length();
// if looking for the fixup, stop short by one to find the parent of the fixup instead.
// because the fixup won't exist yet and needs to be created
if (fixup)
{
stop--;
}
// find the target object by traversing the list of references
for (int i=0; i < stop; i++)
{
Object next;
if (arr == null)
{
next = next(obj,refs.optString(i,null));
}
else
{
next = next(arr,refs.optInt(i, -1));
}
if (next instanceof JSONObject)
{
obj = (JSONObject) next;
arr = null;
}
else
{
obj = null;
arr = (JSONArray) next;
}
}
if (arr==null)
{
return obj;
}
return arr;
}
catch (Exception e)
{
log.error("unexpected exception",e);
throw new JSONException("unexpected exception");
}
}
/**
* Given a previous json object, find the next object under the given index.
*
* @param prev object to find subobject of.
* @param idx index of sub object to find.
* @return the next object in a fixup reference chain (prev[idx])
*
* @throws JSONException if something goes wrong.
*/
private Object next(Object prev, int idx) throws JSONException
{
if (prev==null)
{
throw new JSONException("cannot traverse- missing object encountered");
}
if (prev instanceof JSONArray)
{
return ((JSONArray)prev).get(idx);
}
throw new JSONException("not an array");
}
/**
* Given a previous json object, find the next object under the given ref.
*
* @param prev object to find subobject of.
* @param ref reference of sub object to find.
* @return the next object in a fixup reference chain (prev[ref])
*
* @throws JSONException if something goes wrong.
*/
private Object next(Object prev, String ref) throws JSONException
{
if (prev==null)
{
throw new JSONException("cannot traverse- missing object encountered");
}
if (prev instanceof JSONObject)
{
return ((JSONObject)prev).get(ref);
}
throw new JSONException("not an object");
}
/**
* Returns the more fit of the two method candidates
*
* @param methodCandidate One of the methodCandidates to compare
* @param methodCandidate1 The other of the methodCandidates to compare
* @return The better of the two candidates
*/
private MethodCandidate betterSignature(MethodCandidate methodCandidate,
MethodCandidate methodCandidate1)
{
final Method method = methodCandidate.method;
final Method method1 = methodCandidate1.method;
final Class[] parameters = method.getParameterTypes();
final Class[] parameters1 = method1.getParameterTypes();
int c = 0, c1 = 0;
for (int i = 0; i < parameters.length; i++)
{
final Class parameterClass = parameters[i];
final Class parameterClass1 = parameters1[i];
if (parameterClass != parameterClass1)
{
if (parameterClass.isAssignableFrom(parameterClass1))
{
c1++;
}
else
{
c++;
}
}
}
if (c1 > c)
{
return methodCandidate1;
}
return methodCandidate;
}
/**
* Resolves a string to a class
*
* @param className The name of the class to resolve
* @return The data associated with the className
*/
private ClassData resolveClass(String className)
{
Class clazz;
ClassData cd = null;
synchronized (state)
{
HashMap classMap = state.getClassMap();
clazz = (Class) classMap.get(className);
}
if (clazz != null)
{
cd = ClassAnalyzer.getClassData(clazz);
}
if (cd != null)
{
if (log.isDebugEnabled())
{
log.debug("found class " + cd.getClazz().getName() + " named "
+ className);
}
return cd;
}
if (this != globalBridge)
{
return globalBridge.resolveClass(className);
}
return null;
}
/**
* Resolve which method the caller is requesting <p/> If a method with the
* requested number of arguments does not exist at all, null will be returned.
* <p/> If the object or class (for static methods) being invoked contains
* more than one overloaded methods that match the method key signature, find
* the closest matching method to invoke according to the JSON arguments being
* passed in.
*
* @param methodMap Map keyed by MethodKey objects and the values will be
* either a Method object, or an array of Method objects, if there is
* more than one possible method that can be invoked matching the
* MethodKey.
* @param methodName method name being called.
* @param arguments JSON arguments to the method, as a JSONArray.
* @return the Method that most closely matches the call signature, or null if
* there is not a match.
*/
private Method resolveMethod(HashMap methodMap, String methodName,
JSONArray arguments)
{
Method method[];
if (methodMap == null) {
return null;
}
// first, match soley by the method name and number of arguments passed in
// if there is a single match, return the single match
// if there is no match at all, return null
// if there are multiple matches, fall through to the second matching phase
// below
MethodKey mk = new MethodKey(methodName, arguments.length());
Object o = methodMap.get(mk);
if (o instanceof Method)
{
Method m = (Method) o;
if (log.isDebugEnabled())
{
log.debug("found method " + methodName + "(" + argSignature(m) + ")");
}
return m;
}
else if (o instanceof Method[])
{
method = (Method[]) o;
}
else
{
return null;
}
// second matching phase: there were overloaded methods on the object
// we are invoking so try and find the best match based on the types of
// the arguments passed in.
// try and unmarshall the arguments against each candidate method
// to determine which one matches the best
List candidate = new ArrayList();
if (log.isDebugEnabled())
{
log.debug("looking for method " + methodName + "("
+ argSignature(arguments) + ")");
}
for (int i = 0; i < method.length; i++)
{
try
{
candidate.add(tryUnmarshallArgs(method[i], arguments));
if (log.isDebugEnabled())
{
log.debug("+++ possible match with method " + methodName + "("
+ argSignature(method[i]) + ")");
}
}
catch (Exception e)
{
if (log.isDebugEnabled())
{
log.debug("xxx " + e.getMessage() + " in " + methodName + "("
+ argSignature(method[i]) + ")");
}
}
}
// now search through all the candidates and find one which matches
// the json arguments the closest
MethodCandidate best = null;
for (int i = 0; i < candidate.size(); i++)
{
MethodCandidate c = (MethodCandidate) candidate.get(i);
if (best == null)
{
best = c;
continue;
}
final ObjectMatch bestMatch = best.getMatch();
final ObjectMatch cMatch = c.getMatch();
if (bestMatch.getMismatch() > cMatch.getMismatch())
{
best = c;
}
else if (bestMatch.getMismatch() == cMatch.getMismatch())
{
best = betterSignature(best, c);
}
}
if (best != null)
{
Method m = best.method;
if (log.isDebugEnabled())
{
log.debug("found method " + methodName + "(" + argSignature(m) + ")");
}
return m;
}
return null;
}
/**
* Resolve the key to a specified instance object. If an instance object of
* the requested key is not found, and this is not the global bridge, then
* look in the global bridge too. <p/> If the key is not found in this bridge
* or the global bridge, the requested key may be a class method (static
* method) or may not exist (not registered under the requested key.)
*
* @param key registered object key being requested by caller.
* @return ObjectInstance that has been registered under this key, in this
* bridge or the global bridge.
*/
private ObjectInstance resolveObject(Object key)
{
ObjectInstance oi;
synchronized (state)
{
HashMap objectMap = state.getObjectMap();
oi = (ObjectInstance) objectMap.get(key);
}
if (log.isDebugEnabled() && oi != null)
{
log.debug("found object " + oi.o.hashCode() + " of class "
+ oi.clazz.getName() + " with key " + key);
}
if (oi == null && this != globalBridge)
{
return globalBridge.resolveObject(key);
}
return oi;
}
/**
* Tries to unmarshall the arguments to a method
*
* @param method The method to unmarshall the arguments for.
* @param arguments The arguments to unmarshall
* @return The MethodCandidate that should suit the arguements and method.
* @throws UnmarshallException If one of the arguments cannot be unmarshalled
*/
private MethodCandidate tryUnmarshallArgs(Method method, JSONArray arguments)
throws UnmarshallException
{
MethodCandidate candidate = new MethodCandidate(method);
Class param[] = method.getParameterTypes();
int i = 0, j = 0;
try
{
for (; i < param.length; i++)
{
SerializerState serialiserState = new SerializerState();
if (LocalArgController.isLocalArg(param[i]))
{
candidate.match[i] = ObjectMatch.OKAY;
}
else
{
candidate.match[i] = ser.tryUnmarshall(serialiserState, param[i], arguments.get(j++));
}
}
}
catch (JSONException e)
{
throw (NoSuchElementException) new NoSuchElementException(e.getMessage()).initCause(e);
}
catch (UnmarshallException e)
{
throw new UnmarshallException("arg " + (i + 1) + " " + e.getMessage(), e);
}
return candidate;
}
/**
* Convert the arguments to a method call from json into java objects to be
* used for invoking the method, later.
*
* @param context the context of the caller. This will be the servlet request
* and response objects in an http servlet call environment. These
* are used to insert local arguments (e.g. the request, response or
* session,etc.) when found in the java method call argument
* signature.
* @param method the java method that will later be invoked with the given
* args.
* @param arguments the arguments from the caller, in json format.
* @return the java arguments as unmarshalled from json.
* @throws UnmarshallException if there is a problem unmarshalling the
* arguments.
*/
private Object[] unmarshallArgs(Object context[], Method method,
JSONArray arguments) throws UnmarshallException
{
Class param[] = method.getParameterTypes();
Object javaArgs[] = new Object[param.length];
int i = 0, j = 0;
try
{
for (; i < param.length; i++)
{
SerializerState serializerState = new SerializerState();
if (LocalArgController.isLocalArg(param[i]))
{
javaArgs[i] = LocalArgController.resolveLocalArg(context, param[i]);
}
else
{
javaArgs[i] = ser.unmarshall(serializerState, param[i],
arguments.get(j++));
}
}
}
catch (JSONException e)
{
throw (NoSuchElementException) new NoSuchElementException(e.getMessage()).initCause(e);
}
catch (UnmarshallException e)
{
throw new UnmarshallException("arg " + (i + 1) + " could not unmarshall", e);
}
return javaArgs;
}
}