All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mxgraph.io.mxObjectCodec Maven / Gradle / Ivy

Go to download

JGraphX Swing Component - Java Graph Visualization Library This is a binary & source redistribution of the original, unmodified JGraphX library originating from: "https://github.com/jgraph/jgraphx/archive/v3.4.1.3.zip". The purpose of this redistribution is to make the library available to other Maven projects.

There is a newer version: 3.4.1.3
Show newest version
/**
 * $Id: mxObjectCodec.java,v 1.41 2012/08/21 10:55:11 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 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;

	/**
	 * Caches accessors for the given method names.
	 */
	protected Map accessors;

	/**
	 * Caches fields for faster access.
	 */
	protected Map> 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 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();

				// 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();
				}
			}
		}
		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 mxCodec#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.mxCodec#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(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 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>(); } // Creates the fields cache entry for the given type Map map = fields.get(type); if (map == null) { map = new HashMap(); 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(); } 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 . * * 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