package er.ajax; import java.util.NoSuchElementException; import org.jabsorb.JSONRPCBridge; import org.jabsorb.JSONRPCResult; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WOResponse; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableDictionary; import er.ajax.json.JSONBridge; import er.extensions.appserver.ERXResponseRewriter; import er.extensions.appserver.ERXWOContext; /** * A RPC mechanism to call methods on the Java server side from a JavaScript client. * It Handles javascript-java communication (client-server) between the javascript world running in a web browser and the * java world, running in a WebObject application. This remote-procedure-call communication is done using json protocol, * as implemented by the JSON-RPC library. * <p> * This component generates javascript code that will initialize a variable that will be the starting point for rpc * communication. The name of this variable is given in the <code>name</code> binding. There will be an object, server * side that will be the proxy and will handle the request. You can define the proxy on the server side (it has to be a * JSONRPCBridge). The proxy will be your window to the java world, from there you can access your java objects from the * javascript side. The name for this java variable is <code>proxyName</code>. By default, it will configure the * parent component as one java proxy object and name it <code>wopage</code> from the javascript side. * (WARNING: DON'T DO THIS. YOU SHOULD ALWAYS PROVIDE A SEPARATE PROXY OBJECT. See below.) The JSONRPCBridge * object is created if not given as a binding. If the binding is there but the value is null, it will create the bridge * then push it to the binding. That way, you can configure a single bridge for multiple proxy objects. It is of good * practice to provide a value in a binding (at least a binding) so that the object is not created on every ajax * request. * </p> * <p> * The <em>proxy</em> object will be the one visible for RPC from the javascript world. For example, the following * binding:<br> * <code> * PageProxy : AjaxProxy {<br> * proxyName = "wopage";<br> * name = "jsonrpc";<br> * }</code> <br> * will be used as follow : <table border="1"> * <tr> * <th style="width:50%">JavaScript (client)</th> * <th style="width:50%">Java (server)</th> * </tr> * <tr> * <td><pre><code> * <em>// index of the selection on the client</em> * var idx = 3; // fixed value for the demo * <em>// using rpc, ask our page to get the name for that client for that index.</em> * var nom = jsonrpc.wopage.clientNameAtIndex(idx); * </code></pre></td> * <td><pre><code> * <em>// Java-WebObject side, we receive the index and simple return what they asked for, as for any java call.</em> * public String clientNameAtIndex(int i) { * return <em>"something!"+i</em>; * } * </code></pre></td> * </tr> * </table> * <p> * <b>Every public instance and static method of the proxy object is published on the javascript side.</b> * Remember that if no proxy object is given, it will use the parent component, which is the component in which this component is embedded. * This is probably a very bad idea, as it possible to call something like 'valueForKeyPath("application.terminate")' from the client side. * And you usually don't control the client.</p> * <p> * You currently cannot publish more than one proxy object under a given bridge "name" (e.g. "jsonrpc"). The initialization code will only be * called for the first object.</p> * <p> * Note: The component used to have a binding "JSONRPCBridge". This seems to been renamed to "AjaxBridge". Both bindings are supported. In * future versions the JSONRPCBridge may be removed.</p> * <h2>Todo</h2> * <ul> * <li> Remove support to automatically proxy the parent component. Maybe even warn about proxying WOComponents per se.</li> * <li> Complete the JSON-RPC integration to be able to leverage all possibilities of that library (foreign references, * etc.).</li> * <li> Allow the use of interfaces to reduce the number of published methods ({@link JSONRPCBridge#registerObject(Object, Object, Class)}</li> * <li> Allow multiple proxy objects under a given bridge name.</li> * <li> Update org.jabsorb to latest version (1.3.2) </li> * <li> Extend JSONRPCBridge to only publish methods marked with an annotation.</li> * </ul> * * @binding proxy Server side object (Java) that will be visible for rpc communication (Javascript). * If no object is bound, the parent() object is assigned by default. You should ALWYAS provide an object. * @binding proxyName Client side name (Javascript) used to identify the proxy (Java) from the bridge object. * @binding name Client side name (Javascript) of the bridge object. * @binding AjaxBridge Server side object (Java) used to handle the request. If no value is bound, a new * object is created for every ajax request. If a binding is there but has a null value, a new * object will be created and pushed to the binding so that this new object can be shared * for multiple proxies. * @binding lazy (default false) if true, the proxy is only initialized on-demand, rather than on-load. The initialization requests * the available methods from the proxy object. * * @author Jean-François Veillette <jfveillette@os.ca> * @version $Revision $, $Date $ <br> * © 2005-2006 OS communications informatiques, inc. http://www.os.ca * Tous droits réservés. */ public class AjaxProxy extends AjaxComponent { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(AjaxProxy.class); public AjaxProxy(WOContext context) { super(context); } /** * Overridden because the component is stateless */ @Override public boolean isStateless() { return true; } /** * The binding is now called "AjaxBridge", but we also used "JSONRPCBridge" in the past. * @return the JSONRPCBridge to use */ private JSONRPCBridge getBridgeBinding() { Object bridge = valueForBinding("AjaxBridge"); if (bridge == null) { bridge = valueForBinding("JSONRPCBridge"); } return (JSONRPCBridge)bridge; } /** * Push the bridge object to "AjaxBridge" and "JSONRPCBridge" if available. * @param bridge the bridge object to set in the binding */ private void setBridgeBinding(JSONRPCBridge bridge) { if (canSetValueForBinding("AjaxBridge")) { setValueForBinding(bridge, "AjaxBridge"); } if (canSetValueForBinding("JSONRPCBridge")) { setValueForBinding(bridge, "JSONRPCBridge"); } } /** * Adds the jsonrpc.js script to the head in the response if not already present and also adds a javascript proxy * for the supplied bridge under the name "JSONRPC_<variableName>". * * @param res the response to write into */ @Override protected void addRequiredWebResources(WOResponse res) { addScriptResourceInHead(res, "jsonrpc.js"); NSMutableDictionary userInfo = ERXWOContext.contextDictionary(); String name = (String) valueForBinding("name"); String key = "JSONRPC_" + name; Object oldValue = userInfo.objectForKey(key); Object bridge = getBridgeBinding(); if (bridge == null) { bridge = NSKeyValueCoding.NullValue; } if (oldValue == null) { // add the javascript variable 'name' only if not already in the // response userInfo.setObjectForKey(bridge, key); String jsonRpcJavascript; if (booleanValueForBinding("lazy", false)) { String varName = "_" + name; jsonRpcJavascript = "function " + name + "(callback) { if (typeof " + varName + " == 'undefined') { " + varName + "=new JSONRpcClient(callback, '" + AjaxUtils.ajaxComponentActionUrl(context()) + "'); } else { callback(); } }"; } else { jsonRpcJavascript = name + "=new JSONRpcClient('" + AjaxUtils.ajaxComponentActionUrl(context()) + "');"; } ERXResponseRewriter.addScriptCodeInHead(res, context(), jsonRpcJavascript, key); } else { // ok, the javascript variable 'name' is already in the response, // was it referencing the same JSONRPCBridge object ? if (bridge != oldValue) { // well, it wasn't ... there is high chance of unexpected // problem. just warn the user (programmer), that it might cause // problem. log.warn("JSONRPCProxy detected a conflict. You defined the javascript variable '{}' multiple times, and linked to differents proxy objects: <{}> and <{}>", name, bridge, oldValue); } } } /** Ask the an JSONRPCBridge object to handle the json request. */ @Override public WOActionResults handleRequest(WORequest request, WOContext context) { WOResponse response = AjaxUtils.createResponse(request, context); response.setHeader("application/json", "content-type"); String inputString = request.contentString(); log.debug("AjaxProxy.handleRequest: input = {}", inputString); // Process the request JSONObject input = null; Object output = null; try { input = new JSONObject(inputString); Object proxy; if (canGetValueForBinding("proxy")) { proxy = valueForBinding("proxy"); } else { proxy = parent(); log.warn("No proxy binding given, so using parent component. This is probably a very bad idea."); } String proxyName = (String) valueForBinding("proxyName"); JSONRPCBridge bridge = getBridgeBinding(); if (bridge == null) { bridge = JSONBridge.createBridge(); setBridgeBinding(bridge); } bridge.registerObject(proxyName, proxy); output = bridge.call(new Object[] { request, context, response, proxy }, input); } catch (NoSuchElementException e) { log.error("No method in request"); output = JSONRPCResult.MSG_ERR_NOMETHOD; } catch (Exception e) { log.error("Exception", e); output = JSONRPCResult.MSG_ERR_NOMETHOD; } // Write the response log.debug("AjaxProxy.handleRequest: output = {}", output); response.appendContentString(output.toString()); return response; } }