package er.ajax.json; import java.lang.reflect.Method; import java.util.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; import org.jabsorb.JSONRPCBridge; import org.jabsorb.JSONRPCResult; import org.jabsorb.callback.InvocationCallback; import org.jabsorb.serializer.Serializer; import org.json.JSONException; import org.json.JSONObject; import com.webobjects.appserver.WOApplication; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WODynamicURL; import com.webobjects.appserver.WORequest; import com.webobjects.appserver.WORequestHandler; import com.webobjects.appserver.WOResponse; import com.webobjects.appserver.WOSession; import com.webobjects.foundation._NSUtilities; import er.extensions.foundation.ERXMutableURL; import er.extensions.foundation.ERXProperties; /** * JSONRequestHandler provides support for JSON RPC services that can be both * stateless or stateful (using JSON Components). * * @author mschrag * @property er.ajax.json.globalBacktrackCacheSize the maximum number of global components that can be in the session (defaults to backtrack cache size) * @property er.ajax.json.backtrackCacheSize the maximum number of non-global components that can be in the session (defaults to backtrack cache size) */ public class JSONRequestHandler extends WORequestHandler { public static final String RequestHandlerKey = "json"; private JSONRPCBridge _sharedBridge; /** * Registers the JSONRequestHandler with your application using the default key. * * @return the request handler instance */ public static JSONRequestHandler register() { JSONRequestHandler requestHandler = new JSONRequestHandler(); WOApplication.application().registerRequestHandler(requestHandler, JSONRequestHandler.RequestHandlerKey); return requestHandler; } /** * Creates a new JSONRequestHandler. */ public JSONRequestHandler() { _sharedBridge = JSONBridge.createBridge(); } /** * Returns the shared JSON Bridge for this request handler. * * @return the shared JSON Bridge for this request handler */ public JSONRPCBridge getJSONBridge() { return _sharedBridge; } /** * Registers a custom serializer into the global JSON serializers (see JSONRPCBridge). * * @param serializer the serializer to register * @throws Exception if the registration fails */ public static void registerSerializer(Serializer serializer) throws Exception { JSONRPCBridge.getSerializer().registerSerializer(serializer); } /** * Registers all of the methods of the given class to be available for services to call (see JSONRPCBridge). * * @param clazz the class to register * @throws Exception if the registration fails */ public static void registerClass(Class clazz) throws Exception { JSONRequestHandler.registerClass(clazz.getSimpleName(), clazz); } /** * Registers all of the methods of the given class to be available for services to call (see JSONRPCBridge). * * @param name the namespace to register the methods under * @param clazz the class to register * @throws Exception if the registration fails */ public static void registerClass(String name, Class clazz) throws Exception { JSONRPCBridge.getGlobalBridge().registerClass(name, clazz); } /** * Registers the given object in the shared JSON bridge. The shared JSON * bridge is used for stateless JSON services. As an example, if you call * registerService("myExampleService", new ExampleService()) you can then * call json.myExampleService.someMethodInExampleService from your Javascript. * The same instance is shared across all of your service users, so you should * not store any state in this class. * * @param name the name to register the object as * @param serviceObject the instance to register */ public void registerService(String name, Object serviceObject) { _sharedBridge.registerObject(name, serviceObject); } /** * Returns a URL pointing to the JSON request handler. This variant * should be used for the shared web service endpoint. * * @param context the current WOContext * @param queryString the query string to append * @return a JSON request handler URL */ public static String jsonUrl(WOContext context, String queryString) { return JSONRequestHandler.jsonUrl(context, JSONRequestHandler.RequestHandlerKey, null, null, queryString); } /** * Returns a URL pointing to the JSON request handler. This variant * should be used for the shared web service endpoint. * * @param context the current WOContext * @param requestHandlerKey if you registered a custom JSON request handler key * @param queryString the query string to append * @return a JSON request handler URL */ public static String jsonUrl(WOContext context, String requestHandlerKey, String queryString) { return JSONRequestHandler.jsonUrl(context, requestHandlerKey, null, null, queryString); } /** * Returns a URL pointing to the JSON request handler for a JSON component. * * @param context the current WOContext * @param componentName the name of the component to lookup * @param instance the instance identifier (any value) to create a unique instance (or null for a session-global) * @param queryString the query string to append * @return a JSON request handler URL */ public static String jsonUrl(WOContext context, String componentName, String instance, String queryString) { return JSONRequestHandler.jsonUrl(context, JSONRequestHandler.RequestHandlerKey, componentName, instance, queryString); } /** * Returns a URL pointing to the JSON request handler. * * @param context the current WOContext * @param requestHandlerKey if you registered a custom JSON request handler key * @param componentName the name of the component to lookup (or null for the shared bridge) * @param componentInstance the instance identifier (any value) to create a unique instance (or null for a session-global) * @param queryString the query string to append * @return a JSON request handler URL */ public static String jsonUrl(WOContext context, String requestHandlerKey, String componentName, String componentInstance, String queryString) { String componentNameAndInstance; if (componentName == null) { componentNameAndInstance = ""; } else { componentNameAndInstance = JSONRequestHandler.componentNameAndInstance(componentName, componentInstance); } return JSONRequestHandler._jsonUrl(context, requestHandlerKey, componentNameAndInstance, queryString); } /** * Returns a URL pointing to the JSON request handler. * * @param context the current WOContext * @param requestHandlerKey if you registered a custom JSON request handler key * @param componentNameAndInstance the name/instance identifier of the component to lookup (or null for the shared bridge) * @param queryString the query string to append * @return a JSON request handler URL */ public static String _jsonUrl(WOContext context, String requestHandlerKey, String componentNameAndInstance, String queryString) { String jsonUrl = context.urlWithRequestHandlerKey(JSONRequestHandler.RequestHandlerKey, componentNameAndInstance, queryString); return jsonUrl; } @SuppressWarnings("unchecked") @Override public WOResponse handleRequest(WORequest request) { WOApplication application = WOApplication.application(); application.awake(); try { WOContext context = application.createContextForRequest(request); WOResponse response = application.createResponseInContext(context); Object output; try { String inputString = request.contentString(); JSONObject input = new JSONObject(inputString); String sessionIdKey = WOApplication.application().sessionIdKey(); String sessionId = request.cookieValueForKey(sessionIdKey); if (sessionId == null) { ERXMutableURL url = new ERXMutableURL(); url.setQueryParameters(request.queryString()); sessionId = url.queryParameter(sessionIdKey); if (sessionId == null && input.has(sessionIdKey)) { sessionId = input.getString(sessionIdKey); } } context._setRequestSessionID(sessionId); WOSession session = null; if (context._requestSessionID() != null) { session = WOApplication.application().restoreSessionWithID(sessionId, context); } if (session != null) { session.awake(); } try { JSONComponentCallback componentCallback = null; WODynamicURL url = request._uriDecomposed(); String requestHandlerPath = url.requestHandlerPath(); JSONRPCBridge jsonBridge; if (requestHandlerPath != null && requestHandlerPath.length() > 0) { String componentNameAndInstance = requestHandlerPath; String componentInstance; String componentName; int slashIndex = componentNameAndInstance.indexOf('/'); if (slashIndex == -1) { componentName = componentNameAndInstance; componentInstance = null; } else { componentName = componentNameAndInstance.substring(0, slashIndex); componentInstance = componentNameAndInstance.substring(slashIndex + 1); } if (session == null) { session = context.session(); } String bridgesKey = (componentInstance == null) ? "_JSONGlobalBridges" : "_JSONInstanceBridges"; Map<String, JSONRPCBridge> componentBridges = (Map<String, JSONRPCBridge>) session.objectForKey(bridgesKey); if (componentBridges == null) { int limit = ERXProperties.intForKeyWithDefault((componentInstance == null) ? "er.ajax.json.globalBacktrackCacheSize" : "er.ajax.json.backtrackCacheSize", WOApplication.application().pageCacheSize()); componentBridges = new LRUMap<>(limit); session.setObjectForKey(componentBridges, bridgesKey); } jsonBridge = componentBridges.get(componentNameAndInstance); if (jsonBridge == null) { Class componentClass = _NSUtilities.classWithName(componentName); JSONComponent component; if (JSONComponent.class.isAssignableFrom(componentClass)) { component = (JSONComponent) _NSUtilities.instantiateObject(componentClass, new Class[] { WOContext.class }, new Object[] { context }, true, false); } else { throw new SecurityException("There is no JSON component named '" + componentName + "'."); } jsonBridge = createBridgeForComponent(component, componentName, componentInstance, componentBridges); } componentCallback = new JSONComponentCallback(context); jsonBridge.registerCallback(componentCallback, WOContext.class); } else { jsonBridge = _sharedBridge; } try { output = jsonBridge.call(new Object[] { request, response, context }, input); } finally { if (componentCallback != null) { jsonBridge.unregisterCallback(componentCallback, WOContext.class); } } if (context._session() != null) { WOSession contextSession = context._session(); // If this is a new session, then we have to force it to be a cookie session if (sessionId == null) { boolean storesIDsInCookies = contextSession.storesIDsInCookies(); try { contextSession.setStoresIDsInCookies(true); contextSession._appendCookieToResponse(response); } finally { contextSession.setStoresIDsInCookies(storesIDsInCookies); } } else { contextSession._appendCookieToResponse(response); } } response.appendContentString(output.toString()); response._finalizeInContext(context); response.disableClientCaching(); } finally { try { if (session != null) { session.sleep(); } } finally { if (context._session() != null) { WOApplication.application().saveSessionForContext(context); } } } } catch (NoSuchElementException e) { e.printStackTrace(); output = new JSONRPCResult(JSONRPCResult.CODE_ERR_NOMETHOD, null, JSONRPCResult.MSG_ERR_NOMETHOD); } catch (JSONException e) { e.printStackTrace(); output = new JSONRPCResult(JSONRPCResult.CODE_ERR_PARSE, null, JSONRPCResult.MSG_ERR_PARSE); } catch (Throwable t) { t.printStackTrace(); output = new JSONRPCResult(JSONRPCResult.CODE_ERR_PARSE, null, t.getMessage()); } return response; } finally { application.sleep(); } } protected static String componentNameAndInstance(String componentName, String componentInstance) { String componentNameAndInstance; if (componentInstance == null) { componentNameAndInstance = componentName; } else { componentNameAndInstance = componentName + "/" + componentInstance; } return componentNameAndInstance; } protected JSONRPCBridge createBridgeForComponent(JSONComponent component, String componentName, String componentInstance, Map<String, JSONRPCBridge> componentBridges) throws Exception { JSONRPCBridge jsonBridge = JSONBridge.createBridge(); jsonBridge.registerCallableReference(JSONComponent.class); jsonBridge.registerObject("component", component); String componentNameAndInstance = JSONRequestHandler.componentNameAndInstance(componentName, componentInstance); componentBridges.put(componentNameAndInstance, jsonBridge); return jsonBridge; } protected static class LRUMap<U, V> extends LinkedHashMap<U, V> { /** * 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 int _maxSize; public LRUMap(int maxSize) { super(16, 0.75f, true); _maxSize = maxSize; } @Override protected boolean removeEldestEntry(Map.Entry<U, V> eldest) { return size() > _maxSize; } } protected static class JSONComponentCallback implements InvocationCallback { /** * 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 WOContext _context; public JSONComponentCallback(WOContext context) { _context = context; } public void preInvoke(Object context, Object instance, Method method, Object[] arguments) throws Exception { if (instance instanceof JSONComponent) { JSONComponent component = (JSONComponent) instance; component._setContext(_context); component.checkAccess(); } } public void postInvoke(Object context, Object instance, Method method, Object result) throws Exception { // DO NOTHING } } }