Java tutorial
/** * ************************************************************************ * * server-objects - a contrib to the Qooxdoo project that makes server * and client objects operate seamlessly; like Qooxdoo, server objects * have properties, events, and methods all of which can be access from * either server or client, regardless of where the original object was * created. * * http://qooxdoo.org * * Copyright: * 2010 Zenesis Limited, http://www.zenesis.com * * License: * LGPL: http://www.gnu.org/licenses/lgpl.html * EPL: http://www.eclipse.org/org/documents/epl-v10.php * * This software is provided under the same licensing terms as Qooxdoo, * please see the LICENSE file in the Qooxdoo project's top-level directory * for details. * * Authors: * * John Spackman (john.spackman@zenesis.com) * * ************************************************************************ */ package com.zenesis.qx.remote; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import org.apache.log4j.Logger; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.ObjectMapper; import com.zenesis.qx.event.EventManager; import com.zenesis.qx.remote.CommandId.CommandType; /** * Handles the request and responses for a client. * * This uses the Jackson JSON parser to pull data incrementally from the request; this makes the * code harder to read/write and means that we expect the JSON data to occur in a particular * order even though the JSON specification does not allow ordering to be enforced. However, * by dealing with data incrementally in the way we are able to delay deciding what type of * data to instantiate until we have worked out where it is going - i.e. we look at the types * of a method's parameters and use that type information to change the way we parse. In this * way, we can support any arbitrary mapping between JSON and Java thanks to Jackson. * * @author "John Spackman <john.spackman@zenesis.com>" * */ public class RequestHandler { private static final Logger log = Logger.getLogger(RequestHandler.class); // Command type strings received from the client private static final String CMD_BOOTSTRAP = "bootstrap"; // Reset application session and get bootstrap private static final String CMD_CALL = "call"; // Call server object method private static final String CMD_DISPOSE = "dispose"; // The client has disposed of a Proxied object private static final String CMD_EDIT_ARRAY = "edit-array"; // Changes to an array private static final String CMD_EXPIRE = "expire"; // Expires a flushed property value private static final String CMD_LISTEN = "listen"; // Add an event listener private static final String CMD_NEW = "new"; // Create a new object private static final String CMD_POLL = "poll"; // Poll for changes (ie do nothing) private static final String CMD_SET = "set"; // Set a property value private static final String CMD_UNLISTEN = "unlisten"; // Remove an event listener // This class is sent as data by cmdNewObject to change a client ID into a server ID public static final class MapClientId { public final int serverId; public final int clientId; public MapClientId(int serverId, int clientId) { super(); this.serverId = serverId; this.clientId = clientId; } } // This class is thrown to provide Exception information to the client public static class ExceptionDetails { public final String exceptionClass; public final String message; /** * @param exceptionClass * @param message */ public ExceptionDetails(String exceptionClass, String message) { super(); this.exceptionClass = exceptionClass; this.message = message; } } // Sent when a function returns public static final class FunctionReturn { public final int asyncId; public final Object result; public FunctionReturn(int asyncId, Object result) { super(); this.asyncId = asyncId; this.result = result; } } // This class is sent as data when an exception is thrown while setting a property value public static final class PropertyReset extends ExceptionDetails { public final Object oldValue; /** * @param oldValue * @param exceptionClass * @param message */ public PropertyReset(Object oldValue, String exceptionClass, String message) { super(exceptionClass, message); this.oldValue = oldValue; } } // RequestHandler for the current thread private static ThreadLocal<RequestHandler> s_currentHandler = new ThreadLocal<RequestHandler>(); // Tracker for the session private final ProxySessionTracker tracker; // Client Objects, indexed by client ID (negative) private HashMap<Integer, Proxied> clientObjects; // The property currently having it's property set private Proxied setPropertyObject; private String setPropertyName; /** * @param tracker */ public RequestHandler(ProxySessionTracker tracker) { super(); this.tracker = tracker; } /** * Handles the callback from the client; expects either an object or an array of objects * @param request * @param response * @throws ServletException * @throws IOException */ public void processRequest(Reader request, OutputStream response) throws ServletException, IOException { processRequest(request, new OutputStreamWriter(response)); } /** * Handles the callback from the client; expects either an object or an array of objects * @param request * @param response * @throws ServletException * @throws IOException */ public void processRequest(Reader request, Writer response) throws ServletException, IOException { s_currentHandler.set(this); ObjectMapper objectMapper = tracker.getObjectMapper(); try { if (log.isDebugEnabled()) { StringWriter sw = new StringWriter(); char[] buffer = new char[32 * 1024]; int length; while ((length = request.read(buffer)) > 0) { sw.write(buffer, 0, length); } log.debug("Received: " + sw.toString()); request = new StringReader(sw.toString()); } JsonParser jp = objectMapper.getJsonFactory().createJsonParser(request); if (jp.nextToken() == JsonToken.START_ARRAY) { while (jp.nextToken() != JsonToken.END_ARRAY) processCommand(jp); } else if (jp.getCurrentToken() == JsonToken.START_OBJECT) processCommand(jp); if (tracker.hasDataToFlush()) { Writer actualResponse = response; if (log.isDebugEnabled()) { final Writer tmp = response; actualResponse = new Writer() { @Override public void close() throws IOException { tmp.close(); } @Override public void flush() throws IOException { tmp.flush(); } @Override public void write(char[] arg0, int arg1, int arg2) throws IOException { System.out.print(new String(arg0, arg1, arg2)); tmp.write(arg0, arg1, arg2); } }; } objectMapper.writeValue(actualResponse, tracker.getQueue()); } } catch (ProxyTypeSerialisationException e) { log.fatal("Unable to serialise type information to client: " + e.getMessage(), e); } catch (ProxyException e) { handleException(response, objectMapper, e); } catch (Exception e) { log.error("Exception during callback: " + e.getMessage(), e); tracker.getQueue().queueCommand(CommandType.EXCEPTION, null, null, new ExceptionDetails(e.getClass().getName(), e.getMessage())); objectMapper.writeValue(response, tracker.getQueue()); } finally { s_currentHandler.set(null); } } /** * Called to handle exceptions during processRequest * @param response * @param objectMapper * @param e * @throws IOException */ protected void handleException(Writer response, ObjectMapper objectMapper, ProxyException e) throws IOException { tracker.getQueue().queueCommand(CommandType.EXCEPTION, e.getServerObject(), null, new ExceptionDetails(e.getClass().getName(), e.getMessage())); objectMapper.writeValue(response, tracker.getQueue()); } /** * Returns the request handler for the current thread * @return */ public static RequestHandler getCurrentHandler() { return s_currentHandler.get(); } /** * Handles an object from the client; expects the object to have a property "cmd" which is * the type of command * @param jp * @throws ServletException * @throws IOException */ protected void processCommand(JsonParser jp) throws ServletException, IOException { String cmd = getFieldValue(jp, "cmd", String.class); if (cmd.equals(CMD_BOOTSTRAP)) cmdBootstrap(jp); else if (cmd.equals(CMD_CALL)) cmdCallServerMethod(jp); else if (cmd.equals(CMD_DISPOSE)) cmdDispose(jp); else if (cmd.equals(CMD_EDIT_ARRAY)) cmdEditArray(jp); else if (cmd.equals(CMD_EXPIRE)) cmdExpire(jp); else if (cmd.equals(CMD_LISTEN)) cmdAddListener(jp); else if (cmd.equals(CMD_NEW)) cmdNewObject(jp); else if (cmd.equals(CMD_POLL)) cmdPoll(jp); else if (cmd.equals(CMD_SET)) cmdSetProperty(jp); else if (cmd.equals(CMD_UNLISTEN)) cmdRemoveListener(jp); else throw new ServletException("Unrecognised command from client: " + cmd); } /** * Resets the application session and returns the bootstrap object to the client * @param jp */ protected void cmdBootstrap(JsonParser jp) throws ServletException, IOException { tracker.resetSession(); tracker.getQueue().queueCommand(CommandId.CommandType.BOOTSTRAP, null, null, tracker.getBootstrap()); jp.nextToken(); } /** * Handles a server method call from the client; expects a serverId, methodName, and an optional * array of parameters * @param jp * @throws ServletException * @throws IOException */ protected void cmdCallServerMethod(JsonParser jp) throws ServletException, IOException { // Get the basics Object obj = getFieldValue(jp, "serverId", Object.class); Class serverClass = null; Proxied serverObject = null; if (obj instanceof Integer) { int serverId = (Integer) obj; serverObject = getProxied(serverId); serverClass = serverObject.getClass(); } else if (obj != null) { try { serverClass = Class.forName(obj.toString()); } catch (ClassNotFoundException e) { log.error("Cannot find server class " + obj + ": " + e.getMessage()); } } String methodName = getFieldValue(jp, "methodName", String.class); int asyncId = getFieldValue(jp, "asyncId", Integer.class); // Onto what should be parameters jp.nextToken(); // Find the method by hand - we have already guaranteed that there will not be conflicting // method names (ie no overridden methods) but Java needs a list of parameter types // so we do it ourselves. boolean found = false; // Check for property accessors; if serverObject is null then it's static method call and // properties are not supported if (serverObject != null && methodName.length() > 3 && (methodName.startsWith("get") || methodName.startsWith("set"))) { String name = methodName.substring(3, 4).toLowerCase(); if (methodName.length() > 4) name += methodName.substring(4); for (ProxyType type = ProxyTypeManager.INSTANCE.getProxyType(serverClass); type != null; type = type .getSuperType()) { ProxyProperty property = type.getProperties().get(name); if (property != null) { Object result = null; if (methodName.startsWith("get")) { readParameters(jp, null); result = property.getValue(serverObject); } else { Object[] values = readParameters(jp, new Class[] { property.getPropertyClass().getJavaType() }); property.setValue(serverObject, values[0]); } if (property.isOnDemand()) tracker.setClientHasValue(serverObject, property); tracker.getQueue().queueCommand(CommandId.CommandType.FUNCTION_RETURN, serverObject, null, new FunctionReturn(asyncId, result)); found = true; break; } } } if (!found) { for (ProxyType type = ProxyTypeManager.INSTANCE.getProxyType(serverClass); type != null && !found; type = type.getSuperType()) { ProxyMethod[] methods = type.getMethods(); for (int i = 0; i < methods.length; i++) if (methods[i].getName().equals(methodName)) { Method method = methods[i].getMethod(); // Call the method Object[] values = null; try { values = readParameters(jp, method.getParameterTypes()); Object result = method.invoke(serverObject, values); tracker.getQueue().queueCommand(CommandId.CommandType.FUNCTION_RETURN, serverObject, null, new FunctionReturn(asyncId, result)); } catch (InvocationTargetException e) { Throwable t = e.getCause(); log.error("Exception while invoking " + method + "(" + Helpers.toString(values) + ") on " + serverObject + ": " + t.getMessage(), t); throw new ProxyException(serverObject, "Exception while invoking " + method + " on " + serverObject + ": " + t.getMessage(), t); } catch (RuntimeException e) { log.error("Exception while invoking " + method + "(" + Helpers.toString(values) + ") on " + serverObject + ": " + e.getMessage(), e); throw new ProxyException(serverObject, "Exception while invoking " + method + " on " + serverObject + ": " + e.getMessage(), e); } catch (IllegalAccessException e) { throw new ServletException("Exception while running " + method + "(" + Helpers.toString(values) + "): " + e.getMessage(), e); } found = true; break; } } } if (!found) throw new ServletException("Cannot find method called " + methodName + " in " + (serverObject != null ? serverObject : serverClass)); jp.nextToken(); } private Object[] readParameters(JsonParser jp, Class[] types) throws IOException { if (types == null) { // Check for parameters if (jp.getCurrentToken() == JsonToken.FIELD_NAME && jp.getCurrentName().equals("parameters") && jp.nextToken() == JsonToken.START_ARRAY) { while (jp.nextToken() != JsonToken.END_ARRAY) ; } return null; } Object[] values = new Object[types.length]; Object[] params = null; // Check for parameters if (jp.getCurrentToken() == JsonToken.FIELD_NAME && jp.getCurrentName().equals("parameters") && jp.nextToken() == JsonToken.START_ARRAY) { params = readArray(jp, types); } for (int i = 0; i < values.length; i++) if (i < params.length) values[i] = params[i]; else values[i] = null; return values; } /** * Called when the client has disposed of * @param jp * @throws ServletException * @throws IOException */ protected void cmdDispose(JsonParser jp) throws ServletException, IOException { skipFieldName(jp, "serverIds"); while (jp.nextToken() != JsonToken.END_ARRAY) { int serverId = jp.readValueAs(Integer.class); tracker.forget(serverId); } jp.nextToken(); } /** * Handles setting a server object property from the client; expects a serverId, propertyName, and a value * @param jp * @throws ServletException * @throws IOException */ protected void cmdSetProperty(JsonParser jp) throws ServletException, IOException { // Get the basics int serverId = getFieldValue(jp, "serverId", Integer.class); String propertyName = getFieldValue(jp, "propertyName", String.class); Object value = null; Proxied serverObject = getProxied(serverId); ProxyType type = ProxyTypeManager.INSTANCE.getProxyType(serverObject.getClass()); ProxyProperty prop = getProperty(type, propertyName); skipFieldName(jp, "value"); MetaClass propClass = prop.getPropertyClass(); if (propClass.isSubclassOf(Proxied.class)) { if (propClass.isArray() || propClass.isCollection()) { value = readArray(jp, propClass.getJavaType()); } else if (propClass.isMap()) { value = readMap(jp, propClass.getKeyClass(), propClass.getJavaType()); } else { Integer id = jp.readValueAs(Integer.class); if (id != null) value = getProxied(id); } } else { if (propClass.isArray() || propClass.isCollection()) { value = readArray(jp, propClass.getJavaType()); } else if (propClass.isMap()) { value = readMap(jp, propClass.getKeyClass(), propClass.getJavaType()); } else { value = jp.readValueAs(Object.class); if (value != null && Enum.class.isAssignableFrom(propClass.getJavaType())) { String str = Helpers.camelCaseToEnum(value.toString()); value = Enum.valueOf(propClass.getJavaType(), str); } } } setPropertyValue(type, serverObject, propertyName, value); jp.nextToken(); } /** * Sent when the client expires a cached property value, allowing the server property * to also its flush caches; expects a serverId and propertyName * @param jp * @throws ServletException * @throws IOException */ protected void cmdExpire(JsonParser jp) throws ServletException, IOException { // Get the basics int serverId = getFieldValue(jp, "serverId", Integer.class); String propertyName = getFieldValue(jp, "propertyName", String.class); Proxied serverObject = getProxied(serverId); ProxyType type = ProxyTypeManager.INSTANCE.getProxyType(serverObject.getClass()); ProxyProperty prop = getProperty(type, propertyName); prop.expire(serverObject); jp.nextToken(); } /** * Handles dynamic changes to a qa.data.Array instance without having a complete replacement; expects a * serverId, propertyName, type (one of "add", "remove", "order"), start, end, and optional array of items * @param jp * @throws ServletException * @throws IOException */ protected void cmdEditArray(JsonParser jp) throws ServletException, IOException { // Get the basics int serverId = getFieldValue(jp, "serverId", Integer.class); String propertyName = getFieldValue(jp, "propertyName", String.class); String action = getFieldValue(jp, "type", String.class); Integer start = null; Integer end = null; if (!action.equals("replaceAll")) { start = getFieldValue(jp, "start", Integer.class); end = getFieldValue(jp, "end", Integer.class); } // Get our info Proxied serverObject = getProxied(serverId); ProxyType type = ProxyTypeManager.INSTANCE.getProxyType(serverObject.getClass()); ProxyProperty prop = getProperty(type, propertyName); if (prop.getPropertyClass().isMap()) { Map items = null; // Get the optional array of items if (jp.nextToken() == JsonToken.FIELD_NAME && jp.getCurrentName().equals("items") && jp.nextToken() == JsonToken.START_OBJECT) { items = readMap(jp, prop.getPropertyClass().getKeyClass(), prop.getPropertyClass().getJavaType()); } // Quick logging if (log.isInfoEnabled()) { String str = ""; if (items != null) for (Object key : items.keySet()) { if (str.length() > 0) str += ", "; str += String.valueOf(key) + "=" + String.valueOf(items.get(key)); } log.info("edit-array: property=" + prop + ", type=" + action + ", start=" + start + ", end=" + end + str); } if (action.equals("replaceAll")) { Map map = (Map) prop.getValue(serverObject); if (map == null) { try { map = (Map) prop.getPropertyClass().getCollectionClass().newInstance(); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage(), e); } prop.setValue(serverObject, map); } map.clear(); map.putAll(items); } else throw new IllegalArgumentException("Unsupported action in cmdEditArray: " + action); // Because collection properties are objects and we change them without the serverObject's // knowledge, we have to make sure we notify other trackers ourselves ProxyManager.propertyChanged(serverObject, propertyName, items, null); jp.nextToken(); } else { // NOTE: items is an Array!! But because it may be an array of primitive types, we have // to use java.lang.reflect.Array to access members because we cannot cast arrays of // primitives to Object[] Object items = null; // Get the optional array of items if (jp.nextToken() == JsonToken.FIELD_NAME && jp.getCurrentName().equals("items") && jp.nextToken() == JsonToken.START_ARRAY) { items = readArray(jp, prop.getPropertyClass().getJavaType()); } int itemsLength = Array.getLength(items); // Quick logging if (log.isInfoEnabled()) { String str = ""; if (items != null) for (int i = 0; i < itemsLength; i++) { if (str.length() != 0) str += ", "; str += Array.get(items, i); } log.info("edit-array: property=" + prop + ", type=" + action + ", start=" + start + ", end=" + end + str); } if (action.equals("replaceAll")) { if (prop.getPropertyClass().isCollection()) { Collection list = (Collection) prop.getValue(serverObject); if (list == null) { try { list = (Collection) prop.getPropertyClass().getCollectionClass().newInstance(); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage(), e); } prop.setValue(serverObject, list); } list.clear(); if (items != null) for (int i = 0; i < itemsLength; i++) list.add(Array.get(items, i)); // Because collection properties are objects and we change them without the serverObject's // knowledge, we have to make sure we notify other trackers ourselves ProxyManager.propertyChanged(serverObject, propertyName, list, null); } else { prop.setValue(serverObject, items); } } else throw new IllegalArgumentException("Unsupported action in cmdEditArray: " + action); jp.nextToken(); } } /** * Handles creating a server object to match one created on the client; expects className, * clientId, properties * @param jp * @throws ServletException * @throws IOException */ protected void cmdNewObject(JsonParser jp) throws ServletException, IOException { // Get the basics String className = getFieldValue(jp, "className", String.class); int clientId = getFieldValue(jp, "clientId", Integer.class); // Get the class Class<? extends Proxied> clazz; try { clazz = (Class<? extends Proxied>) Class.forName(className); } catch (ClassNotFoundException e) { throw new ServletException("Unknown class " + className); } ProxyType type = ProxyTypeManager.INSTANCE.getProxyType(clazz); // Create the instance Proxied proxied; try { proxied = type.newInstance(clazz); } catch (InstantiationException e) { throw new ServletException("Cannot create class " + className + ": " + e.getMessage(), e); } catch (InvocationTargetException e) { throw new ServletException("Cannot create class " + className + ": " + e.getMessage(), e); } catch (IllegalAccessException e) { throw new ServletException("Cannot create class " + className + ": " + e.getMessage(), e); } // Get the server ID int serverId = tracker.addClientObject(proxied); // Remember the client ID, in case there are subsequent commands which refer to it if (clientObjects == null) clientObjects = new HashMap<Integer, Proxied>(); clientObjects.put(clientId, proxied); // Tell the client about the new ID - do this before changing properties tracker.invalidateCache(proxied); tracker.getQueue().queueCommand(CommandId.CommandType.MAP_CLIENT_ID, proxied, null, new MapClientId(serverId, clientId)); // Set property values if (jp.nextToken() == JsonToken.FIELD_NAME) { if (jp.nextToken() != JsonToken.START_OBJECT) throw new ServletException("Unexpected properties definiton for 'new' command"); while (jp.nextToken() != JsonToken.END_OBJECT) { String propertyName = jp.getCurrentName(); jp.nextToken(); // Read a Proxied object? ProxyProperty prop = getProperty(type, propertyName); MetaClass propClass = prop.getPropertyClass(); Object value = null; if (propClass.isSubclassOf(Proxied.class)) { if (propClass.isArray() || propClass.isCollection()) { value = readArray(jp, propClass.getJavaType()); } else if (propClass.isMap()) { value = readMap(jp, propClass.getKeyClass(), propClass.getJavaType()); } else { Integer id = jp.readValueAs(Integer.class); if (id != null) value = getProxied(id); } } else { value = jp.readValueAs(Object.class); if (value != null && Enum.class.isAssignableFrom(propClass.getJavaType())) { String str = Helpers.camelCaseToEnum(value.toString()); value = Enum.valueOf(propClass.getJavaType(), str); } } setPropertyValue(type, proxied, propertyName, value); } } // Done jp.nextToken(); } /** * Handles creating a server object to match one created on the client; expects className, * clientId, properties * @param jp * @throws ServletException * @throws IOException */ protected void cmdPoll(JsonParser jp) throws ServletException, IOException { jp.nextToken(); } /** * Handles adding an event listener; expects serverId, eventName * @param jp * @throws ServletException * @throws IOException */ protected void cmdAddListener(JsonParser jp) throws ServletException, IOException { int serverId = getFieldValue(jp, "serverId", Integer.class); String eventName = getFieldValue(jp, "eventName", String.class); Proxied serverObject = getProxied(serverId); EventManager.addListener(serverObject, eventName, ProxyManager.getInstance()); jp.nextToken(); } /** * Handles removing an event listener; expects serverId, eventName * @param jp * @throws ServletException * @throws IOException */ protected void cmdRemoveListener(JsonParser jp) throws ServletException, IOException { int serverId = getFieldValue(jp, "serverId", Integer.class); String eventName = getFieldValue(jp, "eventName", String.class); Proxied serverObject = getProxied(serverId); EventManager.removeListener(serverObject, eventName, ProxyManager.getInstance()); jp.nextToken(); } /** * Returns the proxied object, by serverID or client ID * @param id * @return */ protected Proxied getProxied(int id) { Proxied proxied; if (id < 0) proxied = clientObjects.get(id); else proxied = tracker.getProxied(id); return proxied; } /** * Finds a property in a type, recursing up the class hierarchy * @param type * @param name * @return */ protected ProxyProperty getProperty(ProxyType type, String name) { while (type != null) { ProxyProperty prop = type.getProperties().get(name); if (prop != null) return prop; type = type.getSuperType(); } return null; } /** * Sets a property value, tracking which property is being set so that isSettingProperty can * detect recursive sets * @param type * @param proxied * @param propertyName * @param value */ protected void setPropertyValue(ProxyType type, Proxied proxied, String propertyName, Object value) throws ProxyException { if (setPropertyObject != null || setPropertyName != null) throw new IllegalStateException("Recursive property setting!"); setPropertyObject = proxied; setPropertyName = propertyName; try { ProxyProperty property = getProperty(type, propertyName); value = coerce(property.getPropertyClass().getJavaType(), value); if (!property.isSendExceptions()) property.setValue(proxied, value); else { Object oldValue = property.getValue(proxied); try { property.setValue(proxied, value); } catch (Exception e) { tracker.getQueue().queueCommand(CommandId.CommandType.RESTORE_VALUE, proxied, propertyName, new PropertyReset(oldValue, e.getClass().getName(), e.getMessage())); } } } finally { setPropertyObject = null; setPropertyName = null; } } /** * Attempts to convert a native type - Jackson will interpret floating point numbers as * Double, which will cause an exception if the destination only accepts float. * @param clazz * @param value * @return */ protected Object coerce(Class clazz, Object value) { if (value == null) return null; if (value.getClass() == clazz) return value; if (value.getClass() == Double.class) { double val = (Double) value; if (clazz == float.class) value = (float) val; else if (clazz == int.class) value = (int) val; else if (clazz == long.class) value = (long) val; } else if (value.getClass() == Long.class) { long val = (Long) value; if (clazz == float.class) value = (float) val; else if (clazz == int.class) value = (int) val; else if (clazz == long.class) value = (long) val; } return value; } /** * Detects if a property is currently being set as part of synchronising with the client * @param proxied * @param propertyName * @return */ public boolean isSettingProperty(Proxied proxied, String propertyName) { if (setPropertyObject == null || setPropertyName == null) return false; return setPropertyObject == proxied && setPropertyName.equals(propertyName); } /** * Reads an array from JSON, where each value is of the listed in types; EG the first element * is class type[0], the second element is class type[1] etc * @param jp * @param types * @return * @throws IOException */ private Object[] readArray(JsonParser jp, Class[] types) throws IOException { if (jp.getCurrentToken() == JsonToken.VALUE_NULL) return null; ArrayList result = new ArrayList(); for (int paramIndex = 0; jp.nextToken() != JsonToken.END_ARRAY; paramIndex++) { Class type = null; if (types != null && paramIndex < types.length) type = types[paramIndex]; if (type != null && type.isArray()) { if (jp.getCurrentToken() == JsonToken.VALUE_NULL) result.add(null); else if (jp.getCurrentToken() == JsonToken.START_ARRAY) { Object obj = readArray(jp, type.getComponentType()); result.add(obj); } else throw new IllegalStateException("Expected array but found " + jp.getCurrentToken()); } else if (type != null && Proxied.class.isAssignableFrom(type)) { Integer id = jp.readValueAs(Integer.class); if (id != null) { Proxied obj = getProxied(id); result.add(obj); } else result.add(null); } else if (type != null && Enum.class.isAssignableFrom(type)) { Object obj = jp.readValueAs(Object.class); if (obj != null) { String str = Helpers.camelCaseToEnum(obj.toString()); obj = Enum.valueOf(type, str); result.add(obj); } } else { Object obj = jp.readValueAs(type != null ? type : Object.class); result.add(obj); } } return result.toArray(new Object[result.size()]); } /** * Reads an array from JSON, where each value is of the class clazz. Note that while the result * is an array, you cannot assume that it is an array of Object, or use generics because generics * are always Objects - this is because arrays of primitive types are not arrays of Objects * @param jp * @param clazz * @return * @throws IOException */ private Object readArray(JsonParser jp, Class clazz) throws IOException { if (jp.getCurrentToken() == JsonToken.VALUE_NULL) return null; boolean isProxyClass = Proxied.class.isAssignableFrom(clazz); ArrayList result = new ArrayList(); for (; jp.nextToken() != JsonToken.END_ARRAY;) { if (isProxyClass) { Integer id = jp.readValueAs(Integer.class); if (id != null) { Proxied obj = getProxied(id); if (obj == null) log.fatal("Cannot read object of class " + clazz + " from id=" + id); else if (!clazz.isInstance(obj)) throw new ClassCastException( "Cannot cast " + obj + " class " + obj.getClass() + " to " + clazz); else result.add(obj); } else result.add(null); } else { Object obj = readSimpleValue(jp, clazz); result.add(obj); } } Object arr = Array.newInstance(clazz, result.size()); for (int i = 0; i < result.size(); i++) Array.set(arr, i, result.get(i)); return arr; //return result.toArray(Array.newInstance(clazz, result.size())); } /** * Reads an array from JSON, where each value is of the class clazz. Note that while the result * is an array, you cannot assume that it is an array of Object, or use generics because generics * are always Objects - this is because arrays of primitive types are not arrays of Objects * @param jp * @param clazz * @return * @throws IOException */ private Map readMap(JsonParser jp, Class keyClazz, Class clazz) throws IOException { if (jp.getCurrentToken() == JsonToken.VALUE_NULL) return null; boolean isProxyClass = Proxied.class.isAssignableFrom(clazz); if (keyClazz == null) keyClazz = String.class; HashMap result = new HashMap(); for (; jp.nextToken() != JsonToken.END_OBJECT;) { Object key = readSimpleValue(jp, keyClazz); jp.nextToken(); if (isProxyClass) { Integer id = jp.readValueAs(Integer.class); if (id != null) { Proxied obj = getProxied(id); if (!clazz.isInstance(obj)) throw new ClassCastException( "Cannot cast " + obj + " class " + obj.getClass() + " to " + clazz); result.put(key, obj); } else result.put(key, null); } else { Object obj = readSimpleValue(jp, clazz); result.put(key, obj); } } return result; } /** * Reads the current token value, with special consideration for enums * @param jp * @param clazz * @return * @throws IOException */ private Object readSimpleValue(JsonParser jp, Class clazz) throws IOException { if (jp.getCurrentToken() == JsonToken.VALUE_NULL) return null; Object obj = null; if (Enum.class.isAssignableFrom(clazz)) { if (jp.getCurrentToken() == JsonToken.FIELD_NAME) obj = jp.getCurrentName(); else obj = jp.readValueAs(Object.class); if (obj != null) { String str = Helpers.camelCaseToEnum(obj.toString()); obj = Enum.valueOf(clazz, str); } } else { if (jp.getCurrentToken() == JsonToken.FIELD_NAME) obj = jp.getCurrentName(); else obj = jp.readValueAs(clazz); } return obj; } /** * Gets a field value from the parser, checking that it is the type expected * @param <T> The desired type of object returned * @param jp the parser * @param fieldName the name of the field to get * @param clazz the class of the type to get * @return * @throws ServletException * @throws IOException */ private <T> T getFieldValue(JsonParser jp, String fieldName, Class<T> clazz) throws ServletException, IOException { skipFieldName(jp, fieldName); T obj = (T) jp.readValueAs(clazz); return obj; } /** * Reads the next token and ensures that it is a field name called <code>fieldName</code>; leaves the current * token on the start of the field value * @param jp * @param fieldName * @throws ServletException * @throws IOException */ private void skipFieldName(JsonParser jp, String fieldName) throws ServletException, IOException { if (jp.nextToken() != JsonToken.FIELD_NAME) throw new ServletException("Cannot find field name - looking for " + fieldName + " found " + jp.getCurrentToken() + ":" + jp.getText()); String str = jp.getText(); if (!fieldName.equals(str)) throw new ServletException("Cannot find field called " + fieldName + " found " + str); jp.nextToken(); } }