/** * $Id: mxObjectCodec.java,v 1.1 2012/11/15 13:26:47 gaudenz Exp $ * Copyright (c) 2006, Gaudenz Alder */ package com.mxgraph.io; 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.HashMap; 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.util.mxUtils; /** * Generic codec for Java objects. See below for a detailed description of * the encoding/decoding scheme. * * Note: Since booleans are numbers in JavaScript, all boolean values are * encoded into 1 for true and 0 for false. */ @SuppressWarnings("unchecked") public class mxObjectCodec { /** * Immutable empty set. */ private static Set<String> EMPTY_SET = new HashSet<String>(); /** * 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<String> exclude; /** * Array containing the variable names that should be turned into or * converted from references. See <mxCodec.getId> and <mxCodec.getObject>. */ protected Set<String> idrefs; /** * Maps from from fieldnames to XML attribute names. */ protected Map<String, String> mapping; /** * Maps from from XML attribute names to fieldnames. */ protected Map<String, String> reverse; /** * Caches accessors for the given method names. */ protected Map<String, Method> accessors; /** * Caches fields for faster access. */ protected Map<Class, Map<String, Field>> fields; /** * Constructs a new codec for the specified template object. */ public mxObjectCodec(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 mxObjectCodec(Object template, String[] exclude, String[] idrefs, Map<String, String> mapping) { this.template = template; if (exclude != null) { this.exclude = new HashSet<String>(); 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<String>(); for (int i = 0; i < idrefs.length; i++) { this.idrefs.add(idrefs[i]); } } else { this.idrefs = EMPTY_SET; } if (mapping == null) { mapping = new Hashtable<String, String>(); } this.mapping = mapping; reverse = new Hashtable<String, String>(); Iterator<Map.Entry<String, String>> it = mapping.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, String> 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: * <code> * mxCodecRegistry.register(new mxCellCodec() * { * public String getName() * { * return "anotherName"; * } * }); * </code> */ 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(); // Skips text nodes while (node != null && !(node instanceof Element)) { node = node.getNextSibling(); } if (node != null && node instanceof Element && ((Element) node).hasAttribute("as")) { obj = new Hashtable<Object, Object>(); } } } 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: * * <ul> * <li>If the variable name is in {@link #exclude} then it is ignored.</li> * <li>If the variable name is in {@link #idrefs} then * {@link mxCodec#getId(Object)} is used to replace the object with its ID. * </li> * <li>The variable name is mapped using {@link #mapping}.</li> * <li>If obj is an array and the variable name is numeric (ie. an index) then it * is not encoded.</li> * <li>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.</li> * <li>Else, if {@link com.mxgraph.io.mxCodec#isEncodeDefaults()} is true or * the value differs from the template value, then ... * <ul> * <li>... if obj is not an array, then the value is mapped to an * attribute.</li> * <li>... 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. * </li> * </ul> * </li> * </ul> * * 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(mxCodec 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(mxCodec enc, Object obj, Node node) { mxCodec.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(mxCodec enc, Object obj, Node node) { // LATER: Use PropertyDescriptors in Introspector.getBeanInfo(clazz) // see http://forum.jgraph.com/questions/1424 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(mxCodec 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<Map.Entry> 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(mxCodec, 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(mxCodec 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("mxObjectCodec.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(mxCodec enc, Object obj, String attr, Object value, Node node) { value = convertValueToXml(value); 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(mxCodec enc, Object obj, String attr, Object value, Node node) { if (attr == null || obj instanceof Map) { Node child = enc.document.createElement("add"); if (attr != null) { mxCodec.setAttribute(child, "as", attr); } mxCodec.setAttribute(child, "value", value); node.appendChild(child); } else { mxCodec.setAttribute(node, attr, value); } } /** * Writes the given value as a child node of the given node. */ protected void writeComplexAttribute(mxCodec enc, Object obj, String attr, Object value, Node node) { Node child = enc.encode(value); if (child != null) { if (attr != null) { mxCodec.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(); // Creates the fields cache if (fields == null) { fields = new HashMap<Class, Map<String, Field>>(); } // Creates the fields cache entry for the given type Map<String, Field> map = fields.get(type); if (map == null) { map = new HashMap<String, Field>(); fields.put(type, map); } // Tries to get cached field Field field = map.get(fieldname); if (field != null) { return field; } while (type != null) { try { field = type.getDeclaredField(fieldname); if (field != null) { // Adds field to fields cache map.put(fieldname, field); 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; } Method method = (accessors != null) ? accessors.get(name) : null; if (method == null) { try { if (isGetter) { method = getMethod(obj, name, null); } else { method = getMethod(obj, name, new Class[] { field.getType() }); } } catch (Exception e1) { // ignore } // Adds accessor to cache if (method != null) { if (accessors == null) { accessors = new Hashtable<String, Method>(); } accessors.put(name, method); } } return method; } /** * 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) { if (Modifier.isPublic(field.getModifiers())) { value = field.get(obj); } else { value = getFieldValueWithAccessor(obj, field); } } } catch (IllegalAccessException e1) { value = getFieldValueWithAccessor(obj, field); } catch (Exception e) { // ignore } } return value; } /** * Returns the value of the field using the accessor for the field if one exists. */ protected Object getFieldValueWithAccessor(Object obj, Field field) { Object value = null; if (field != null) { try { Method method = getAccessor(obj, field, true); if (method != null) { value = method.invoke(obj, (Object[]) null); } } catch (Exception e2) { // 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 != null) { if (field.getType() == Boolean.class) { value = (value.equals("1") || String.valueOf(value) .equalsIgnoreCase("true")) ? Boolean.TRUE : Boolean.FALSE; } if (Modifier.isPublic(field.getModifiers())) { field.set(obj, value); } else { setFieldValueWithAccessor(obj, field, value); } } } catch (IllegalAccessException e1) { setFieldValueWithAccessor(obj, field, value); } catch (Exception e) { // ignore } } /** * Sets the value of the given field using the accessor if one exists. */ protected void setFieldValueWithAccessor(Object obj, Field field, Object value) { if (field != null) { try { Method method = getAccessor(obj, field, false); if (method != null) { 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() + "." + field.getName() + " (" + field.getType().getSimpleName() + ") = " + value + " (" + value.getClass().getSimpleName() + ")"); } } } /** * 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(mxCodec 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 <encode>. * * 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(mxCodec 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(mxCodec 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 <template> and cached in <mxCodec.objects>. * * This implementation decodes all attributes and childs of a node according * to the following rules: * - If the variable name is in <exclude> or if the attribute name is "id" * or "as" then it is ignored. - If the variable name is in <idrefs> then * <mxCodec.getObject> is used to replace the reference with an object. - * The variable name is mapped using a reverse <mapping>. - If the value has * a child node, then the codec is used to create a child object with the * variable name taken from the "as" attribute. - If the object is an array * and the variable name is empty then the value or child object is appended * to the array. - If an add child has no value or the object is not an * array then the child text content is evaluated using <mxUtils.eval>. * * If no object exists for an ID in <idrefs> a warning is issued in * System.err. * * @param dec Codec that controls the encoding process. * @param node XML node to be decoded. * @param into Optional object to encode the node into. * @return Returns the resulting object that represents the given XML node * or the object given to the method as the into parameter. */ public Object decode(mxCodec dec, Node node, Object into) { Object obj = null; if (node instanceof Element) { String id = ((Element) node).getAttribute("id"); obj = dec.objects.get(id); if (obj == null) { obj = into; if (obj == null) { obj = cloneTemplate(node); } if (id != null && id.length() > 0) { dec.putObject(id, obj); } } node = beforeDecode(dec, node, obj); decodeNode(dec, node, obj); obj = afterDecode(dec, node, obj); } return obj; } /** * Calls decodeAttributes and decodeChildren for the given node. */ protected void decodeNode(mxCodec dec, Node node, Object obj) { if (node != null) { decodeAttributes(dec, node, obj); decodeChildren(dec, node, obj); } } /** * Decodes all attributes of the given node using decodeAttribute. */ protected void decodeAttributes(mxCodec dec, Node node, Object obj) { NamedNodeMap attrs = node.getAttributes(); if (attrs != null) { for (int i = 0; i < attrs.getLength(); i++) { Node attr = attrs.item(i); decodeAttribute(dec, attr, obj); } } } /** * Reads the given attribute into the specified object. */ protected void decodeAttribute(mxCodec dec, Node attr, Object obj) { String name = attr.getNodeName(); if (!name.equalsIgnoreCase("as") && !name.equalsIgnoreCase("id")) { Object value = attr.getNodeValue(); String fieldname = getFieldName(name); if (isReference(obj, fieldname, value, false)) { Object tmp = dec.getObject(String.valueOf(value)); if (tmp == null) { System.err.println("mxObjectCodec.decode: No object for " + getName() + "." + fieldname + "=" + value); return; // exit } value = tmp; } if (!isExcluded(obj, fieldname, value, false)) { setFieldValue(obj, fieldname, value); } } } /** * Decodec all children of the given node using decodeChild. */ protected void decodeChildren(mxCodec dec, Node node, Object obj) { Node child = node.getFirstChild(); while (child != null) { if (child.getNodeType() == Node.ELEMENT_NODE && !processInclude(dec, child, obj)) { decodeChild(dec, child, obj); } child = child.getNextSibling(); } } /** * Reads the specified child into the given object. */ protected void decodeChild(mxCodec dec, Node child, Object obj) { String fieldname = getFieldName(((Element) child).getAttribute("as")); if (fieldname == null || !isExcluded(obj, fieldname, child, false)) { Object template = getFieldTemplate(obj, fieldname, child); Object value = null; if (child.getNodeName().equals("add")) { value = ((Element) child).getAttribute("value"); if (value == null) { value = child.getTextContent(); } } else { value = dec.decode(child, template); // System.out.println("Decoded " + child.getNodeName() + "." // + fieldname + "=" + value); } addObjectValue(obj, fieldname, value, template); } } /** * Returns the template instance for the given field. This returns the * value of the field, null if the value is an array or an empty collection * if the value is a collection. The value is then used to populate the * field for a new instance. For strongly typed languages it may be * required to override this to return the correct collection instance * based on the encoded child. */ protected Object getFieldTemplate(Object obj, String fieldname, Node child) { Object template = getFieldValue(obj, fieldname); // Arrays are replaced completely if (template != null && template.getClass().isArray()) { template = null; } // Collections are cleared else if (template instanceof Collection) { ((Collection<?>) template).clear(); } return template; } /** * Sets the decoded child node as a value of the given object. If the * object is a map, then the value is added with the given fieldname as a * key. If the fieldname is not empty, then setFieldValue is called or * else, if the object is a collection, the value is added to the * collection. For strongly typed languages it may be required to * override this with the correct code to add an entry to an object. */ protected void addObjectValue(Object obj, String fieldname, Object value, Object template) { if (value != null && !value.equals(template)) { if (fieldname != null && obj instanceof Map) { ((Map) obj).put(fieldname, value); } else if (fieldname != null && fieldname.length() > 0) { setFieldValue(obj, fieldname, value); } // Arrays are treated as collections and // converted in setFieldValue else if (obj instanceof Collection) { ((Collection) obj).add(value); } } } /** * Returns true if the given node is an include directive and executes the * include by decoding the XML document. Returns false if the given node is * not an include directive. * * @param dec Codec that controls the encoding/decoding process. * @param node XML node to be checked. * @param into Optional object to pass-thru to the codec. * @return Returns true if the given node was processed as an include. */ public boolean processInclude(mxCodec dec, Node node, Object into) { if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equalsIgnoreCase("include")) { String name = ((Element) node).getAttribute("name"); if (name != null) { try { Node xml = mxUtils.loadDocument( mxObjectCodec.class.getResource(name).toString()) .getDocumentElement(); if (xml != null) { dec.decode(xml, into); } } catch (Exception e) { System.err.println("Cannot process include: " + name); } } return true; } return false; } /** * Hook for subclassers to pre-process the node for the specified object * and return the node to be used for further processing by * {@link #decode(mxCodec, Node)}. The object is created based on the * template in the calling method and is never null. * * This implementation returns the input node. The return value of this * function is used in {@link #decode(mxCodec, Node)} to perform the * default decoding into the given object. * * @param dec Codec that controls the decoding process. * @param node XML node to be decoded. * @param obj Object to encode the node into. * @return Returns the node used for the default decoding. */ public Node beforeDecode(mxCodec dec, Node node, Object obj) { return node; } /** * Hook for subclassers to post-process the object after decoding. This * implementation returns the given object without any changes. The return * value of this method is returned to the decoder from * {@link #decode(mxCodec, Node)}. * * @param dec Codec that controls the decoding process. * @param node XML node to be decoded. * @param obj Object that represents the default decoding. * @return Returns the result of the decoding process. */ public Object afterDecode(mxCodec dec, Node node, Object obj) { return obj; } }