package com.vci.rmip.workflow.client.editor.code; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import com.mxgraph.io.mxCodecRegistry; import com.mxgraph.io.mxObjectCodec; import com.mxgraph.util.mxUtils; import com.vci.rmip.workflow.client.editor.ui.IProcessProperty; public class GraphObjectCodec { /** * Immutable empty set. */ private static Set EMPTY_SET = new HashSet(); /** * Holds the template object associated with this codec. */ protected Object template; /** * Array containing the variable names that should be ignored by the codec. */ protected Set exclude; /** * Array containing the variable names that should be turned into or * converted from references. See and . */ protected Set idrefs; /** * Maps from from fieldnames to XML attribute names. */ protected Map mapping; /** * Maps from from XML attribute names to fieldnames. */ protected Map reverse; /** * Constructs a new codec for the specified template object. */ public GraphObjectCodec(Object template) { this(template, null, null, null); } /** * Constructs a new codec for the specified template object. The variables * in the optional exclude array are ignored by the codec. Variables in the * optional idrefs array are turned into references in the XML. The * optional mapping may be used to map from variable names to XML * attributes. The argument is created as follows: * * @param template Prototypical instance of the object to be encoded/decoded. * @param exclude Optional array of fieldnames to be ignored. * @param idrefs Optional array of fieldnames to be converted to/from references. * @param mapping Optional mapping from field- to attributenames. */ public GraphObjectCodec(Object template, String[] exclude, String[] idrefs, Map mapping) { this.template = template; if (exclude != null) { this.exclude = new HashSet(); for (int i = 0; i < exclude.length; i++) { this.exclude.add(exclude[i]); } } else { this.exclude = EMPTY_SET; } if (idrefs != null) { this.idrefs = new HashSet(); for (int i = 0; i < idrefs.length; i++) { this.idrefs.add(idrefs[i]); } } else { this.idrefs = EMPTY_SET; } if (mapping == null) { mapping = new Hashtable(); } this.mapping = mapping; reverse = new Hashtable(); Iterator> it = mapping.entrySet().iterator(); while (it.hasNext()) { Map.Entry e = it.next(); reverse.put(e.getValue(), e.getKey()); } } /** * Returns the name used for the nodenames and lookup of the codec when * classes are encoded and nodes are decoded. For classes to work with * this the codec registry automatically adds an alias for the classname * if that is different than what this returns. The default implementation * returns the classname of the template class. * * Here is an example on how to use this for renaming mxCell nodes: * * mxCodecRegistry.register(new mxCellCodec() * { * public String getName() * { * return "anotherName"; * } * }); * */ public String getName() { return mxCodecRegistry.getName(getTemplate()); } /** * Returns the template object associated with this codec. * * @return Returns the template object. */ public Object getTemplate() { return template; } /** * Returns a new instance of the template object for representing the given * node. * * @param node XML node that the object is going to represent. * @return Returns a new template instance. */ protected Object cloneTemplate(Node node) { Object obj = null; try { if (template.getClass().isEnum()) { obj = template.getClass().getEnumConstants()[0]; } else { obj = template.getClass().newInstance(); } // Special case: Check if the collection // should be a map. This is if the first // child has an "as"-attribute. This // assumes that all childs will have // as attributes in this case. This is // required because in JavaScript, the // map and array object are the same. if (obj instanceof Collection) { node = node.getFirstChild(); if (node != null && node instanceof Element && ((Element) node).hasAttribute("as")) { obj = new Hashtable(); } } } catch (InstantiationException e) { // ignore e.printStackTrace(); } catch (IllegalAccessException e) { // ignore e.printStackTrace(); } return obj; } /** * Returns true if the given attribute is to be ignored by the codec. This * implementation returns true if the given fieldname is in * {@link #exclude}. * * @param obj Object instance that contains the field. * @param attr Fieldname of the field. * @param value Value of the field. * @param write Boolean indicating if the field is being encoded or * decoded. write is true if the field is being encoded, else it is * being decoded. * @return Returns true if the given attribute should be ignored. */ public boolean isExcluded(Object obj, String attr, Object value, boolean write) { return exclude.contains(attr); } /** * Returns true if the given fieldname is to be treated as a textual * reference (ID). This implementation returns true if the given fieldname * is in {@link #idrefs}. * * @param obj Object instance that contains the field. * @param attr Fieldname of the field. * @param value Value of the field. * @param isWrite Boolean indicating if the field is being encoded or * decoded. isWrite is true if the field is being encoded, else it is being * decoded. * @return Returns true if the given attribute should be handled as a * reference. */ public boolean isReference(Object obj, String attr, Object value, boolean isWrite) { return idrefs.contains(attr); } /** * Encodes the specified object and returns a node representing then given * object. Calls beforeEncode after creating the node and afterEncode * with the resulting node after processing. * * Enc is a reference to the calling encoder. It is used to encode complex * objects and create references. * * This implementation encodes all variables of an object according to the * following rules: * *
    *
  • If the variable name is in {@link #exclude} then it is ignored.
  • *
  • If the variable name is in {@link #idrefs} then * {@link GraphCodec#getId(Object)} is used to replace the object with its ID. *
  • *
  • The variable name is mapped using {@link #mapping}.
  • *
  • If obj is an array and the variable name is numeric (ie. an index) then it * is not encoded.
  • *
  • If the value is an object, then the codec is used to create a child * node with the variable name encoded into the "as" attribute.
  • *
  • Else, if {@link com.mxgraph.io.GraphCodec#isEncodeDefaults()} is true or * the value differs from the template value, then ... *
      *
    • ... if obj is not an array, then the value is mapped to an * attribute.
    • *
    • ... else if obj is an array, the value is mapped to an add child * with a value attribute or a text child node, if the value is a function. *
    • *
    *
  • *
* * If no ID exists for a variable in {@link #idrefs} or if an object cannot be * encoded, a warning is printed to System.err. * * @param enc Codec that controls the encoding process. * @param obj Object to be encoded. * @return Returns the resulting XML node that represents the given object. */ public Node encode(GraphCodec enc, Object obj) { Node node = enc.document.createElement(getName()); obj = beforeEncode(enc, obj, node); encodeObject(enc, obj, node); return afterEncode(enc, obj, node); } /** * Encodes the value of each member in then given obj * into the given node using {@link #encodeFields(mxCodec, Object, Node)} * and {@link #encodeElements(mxCodec, Object, Node)}. * * @param enc Codec that controls the encoding process. * @param obj Object to be encoded. * @param node XML node that contains the encoded object. */ protected void encodeObject(GraphCodec enc, Object obj, Node node) { GraphCodec.setAttribute(node, "id", enc.getId(obj)); encodeFields(enc, obj, node); encodeElements(enc, obj, node); } /** * Encodes the declared fields of the given object into the given node. * * @param enc Codec that controls the encoding process. * @param obj Object whose fields should be encoded. * @param node XML node that contains the encoded object. */ protected void encodeFields(GraphCodec enc, Object obj, Node node) { Class type = obj.getClass(); while (type != null) { Field[] fields = type.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { Field f = fields[i]; if ((f.getModifiers() & Modifier.TRANSIENT) != Modifier.TRANSIENT) { String fieldname = f.getName(); Object value = getFieldValue(obj, fieldname); encodeValue(enc, obj, fieldname, value, node); } } type = type.getSuperclass(); } } /** * Encodes the child objects of arrays, maps and collections. * * @param enc Codec that controls the encoding process. * @param obj Object whose child objects should be encoded. * @param node XML node that contains the encoded object. */ protected void encodeElements(GraphCodec enc, Object obj, Node node) { if (obj.getClass().isArray()) { Object[] tmp = (Object[]) obj; for (int i = 0; i < tmp.length; i++) { encodeValue(enc, obj, null, tmp[i], node); } } else if (obj instanceof Map) { Iterator it = ((Map) obj).entrySet().iterator(); while (it.hasNext()) { Map.Entry e = it.next(); encodeValue(enc, obj, String.valueOf(e.getKey()), e.getValue(), node); } } else if (obj instanceof Collection) { Iterator it = ((Collection) obj).iterator(); while (it.hasNext()) { Object value = it.next(); encodeValue(enc, obj, null, value, node); } } } /** * Converts the given value according to the mappings * and id-refs in this codec and uses * {@link #writeAttribute(GraphCodec, Object, String, Object, Node)} * to write the attribute into the given node. * * @param enc Codec that controls the encoding process. * @param obj Object whose field is going to be encoded. * @param fieldname Name if the field to be encoded. * @param value Value of the property to be encoded. * @param node XML node that contains the encoded object. */ protected void encodeValue(GraphCodec enc, Object obj, String fieldname, Object value, Node node) { if (value != null && !isExcluded(obj, fieldname, value, true)) { if (isReference(obj, fieldname, value, true)) { Object tmp = enc.getId(value); if (tmp == null) { System.err.println("GraphObjectCodec.encode: No ID for " + getName() + "." + fieldname + "=" + value); return; // exit } value = tmp; } Object defaultValue = getFieldValue(template, fieldname); if (fieldname == null || enc.isEncodeDefaults() || defaultValue == null || !defaultValue.equals(value)) { writeAttribute(enc, obj, getAttributeName(fieldname), value, node); } } } /** * Returns true if the given object is a primitive value. * * @param value Object that should be checked. * @return Returns true if the given object is a primitive value. */ protected boolean isPrimitiveValue(Object value) { return value instanceof String || value instanceof Boolean || value instanceof Character || value instanceof Byte || value instanceof Short || value instanceof Integer || value instanceof Long || value instanceof Float || value instanceof Double || value.getClass().isPrimitive(); } /** * Writes the given value into node using writePrimitiveAttribute * or writeComplexAttribute depending on the type of the value. */ protected void writeAttribute(GraphCodec enc, Object obj, String attr, Object value, Node node) { value = convertValueToXml(value); if(value instanceof IProcessProperty){ GraphCodec.setAttribute(node, attr, value.toString()); }else{ if (isPrimitiveValue(value)) { writePrimitiveAttribute(enc, obj, attr, value, node); } else { writeComplexAttribute(enc, obj, attr, value, node); } } } /** * Writes the given value as an attribute of the given node. */ protected void writePrimitiveAttribute(GraphCodec enc, Object obj, String attr, Object value, Node node) { if (attr == null || obj instanceof Map) { Node child = enc.document.createElement("add"); if (attr != null) { GraphCodec.setAttribute(child, "as", attr); } GraphCodec.setAttribute(child, "value", value); node.appendChild(child); } else { GraphCodec.setAttribute(node, attr, value); } } /** * Writes the given value as a child node of the given node. */ protected void writeComplexAttribute(GraphCodec enc, Object obj, String attr, Object value, Node node) { Node child = enc.encode(value); if (child != null) { if (attr != null) { GraphCodec.setAttribute(child, "as", attr); } node.appendChild(child); } else { System.err.println("mxObjectCodec.encode: No node for " + getName() + "." + attr + ": " + value); } } /** * Converts true to "1" and false to "0". All other values are ignored. */ protected Object convertValueToXml(Object value) { if (value instanceof Boolean) { value = ((Boolean) value).booleanValue() ? "1" : "0"; } return value; } /** * Converts XML attribute values to object of the given type. */ protected Object convertValueFromXml(Class type, Object value) { if (value instanceof String) { String tmp = (String) value; if (type.equals(boolean.class) || type == Boolean.class) { if (tmp.equals("1") || tmp.equals("0")) { tmp = (tmp.equals("1")) ? "true" : "false"; } value = Boolean.valueOf(tmp); } else if (type.equals(char.class) || type == Character.class) { value = Character.valueOf(tmp.charAt(0)); } else if (type.equals(byte.class) || type == Byte.class) { value = Byte.valueOf(tmp); } else if (type.equals(short.class) || type == Short.class) { value = Short.valueOf(tmp); } else if (type.equals(int.class) || type == Integer.class) { value = Integer.valueOf(tmp); } else if (type.equals(long.class) || type == Long.class) { value = Long.valueOf(tmp); } else if (type.equals(float.class) || type == Float.class) { value = Float.valueOf(tmp); } else if (type.equals(double.class) || type == Double.class) { value = Double.valueOf(tmp); } } return value; } /** * Returns the XML node attribute name for the given Java field name. That * is, it returns the mapping of the field name. */ protected String getAttributeName(String fieldname) { if (fieldname != null) { Object mapped = mapping.get(fieldname); if (mapped != null) { fieldname = mapped.toString(); } } return fieldname; } /** * Returns the Java field name for the given XML attribute name. That is, it * returns the reverse mapping of the attribute name. * * @param attributename * The attribute name to be mapped. * @return String that represents the mapped field name. */ protected String getFieldName(String attributename) { if (attributename != null) { Object mapped = reverse.get(attributename); if (mapped != null) { attributename = mapped.toString(); } } return attributename; } /** * Returns the field with the specified name. */ protected Field getField(Object obj, String fieldname) { Class type = obj.getClass(); while (type != null) { try { Field field = type.getDeclaredField(fieldname); if (field != null) { return field; } } catch (Exception e) { // ignore } type = type.getSuperclass(); } return null; } /** * Returns the accessor (getter, setter) for the specified field. */ protected Method getAccessor(Object obj, Field field, boolean isGetter) { String name = field.getName(); name = name.substring(0, 1).toUpperCase() + name.substring(1); if (!isGetter) { name = "set" + name; } else if (boolean.class.isAssignableFrom(field.getType())) { name = "is" + name; } else { name = "get" + name; } try { if (isGetter) { return getMethod(obj, name, null); } else { return getMethod(obj, name, new Class[] { field.getType() }); } } catch (Exception e1) { // ignore } return null; } /** * Returns the method with the specified signature. */ protected Method getMethod(Object obj, String methodname, Class[] params) { Class type = obj.getClass(); while (type != null) { try { Method method = type.getDeclaredMethod(methodname, params); if (method != null) { return method; } } catch (Exception e) { // ignore } type = type.getSuperclass(); } return null; } /** * Returns the value of the field with the specified name in the specified * object instance. */ protected Object getFieldValue(Object obj, String fieldname) { Object value = null; if (obj != null && fieldname != null) { Field field = getField(obj, fieldname); try { if (field != null) { value = field.get(obj); } } catch (IllegalAccessException e1) { if (field != null) { try { Method method = getAccessor(obj, field, true); value = method.invoke(obj, (Object[]) null); } catch (Exception e2) { // ignore } } } catch (Exception e) { // ignore } } return value; } /** * Sets the value of the field with the specified name * in the specified object instance. */ protected void setFieldValue(Object obj, String fieldname, Object value) { Field field = null; try { field = getField(obj, fieldname); if (field.getType() == Boolean.class) { value = (value.equals("1") || String.valueOf(value) .equalsIgnoreCase("true")) ? Boolean.TRUE : Boolean.FALSE; } field.set(obj, value); } catch (IllegalAccessException e1) { if (field != null) { try { Method method = getAccessor(obj, field, false); Class type = method.getParameterTypes()[0]; value = convertValueFromXml(type, value); // Converts collection to a typed array before setting if (type.isArray() && value instanceof Collection) { Collection coll = (Collection) value; value = coll.toArray((Object[]) Array.newInstance( type.getComponentType(), coll.size())); } method.invoke(obj, new Object[] { value }); } catch (Exception e2) { System.err.println("setFieldValue: " + e2 + " on " + obj.getClass().getSimpleName() + "." + fieldname + " (" + field.getType().getSimpleName() + ") = " + value + " (" + value.getClass().getSimpleName() + ")"); } } } catch (Exception e) { // ignore } } /** * Hook for subclassers to pre-process the object before encoding. This * returns the input object. The return value of this function is used in * encode to perform the default encoding into the given node. * * @param enc Codec that controls the encoding process. * @param obj Object to be encoded. * @param node XML node to encode the object into. * @return Returns the object to be encoded by the default encoding. */ public Object beforeEncode(GraphCodec enc, Object obj, Node node) { return obj; } /** * Hook for subclassers to post-process the node for the given object after * encoding and return the post-processed node. This implementation returns * the input node. The return value of this method is returned to the * encoder from . * * Parameters: * * @param enc Codec that controls the encoding process. * @param obj Object to be encoded. * @param node XML node that represents the default encoding. * @return Returns the resulting node of the encoding. */ public Node afterEncode(GraphCodec enc, Object obj, Node node) { return node; } /** * Parses the given node into the object or returns a new object * representing the given node. * * @param dec Codec that controls the encoding process. * @param node XML node to be decoded. * @return Returns the resulting object that represents the given XML node. */ public Object decode(GraphCodec dec, Node node) { return decode(dec, node, null); } /** * Parses the given node into the object or returns a new object * representing the given node. * * Dec is a reference to the calling decoder. It is used to decode complex * objects and resolve references. * * If a node has an id attribute then the object cache is checked for the * object. If the object is not yet in the cache then it is constructed * using the constructor of