/** * $Id: mxCodec.java,v 1.1 2012/11/15 13:26:47 gaudenz Exp $ * Copyright (c) 2012, JGraph Ltd */ package com.mxgraph.io; import java.util.Hashtable; import java.util.Map; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.mxgraph.model.mxCell; import com.mxgraph.model.mxCellPath; import com.mxgraph.model.mxICell; import com.mxgraph.util.mxDomUtils; import com.mxgraph.util.mxUtils; /** * XML codec for Java object graphs. In order to resolve forward references * when reading files the XML document that contains the data must be passed * to the constructor. */ public class mxCodec { /** * Holds the owner document of the codec. */ protected Document document; /** * Maps from IDs to objects. */ protected Map<String, Object> objects = new Hashtable<String, Object>(); /** * Specifies if default values should be encoded. Default is false. */ protected boolean encodeDefaults = false; /** * Constructs an XML encoder/decoder with a new owner document. */ public mxCodec() { this(mxDomUtils.createDocument()); } /** * Constructs an XML encoder/decoder for the specified owner document. * * @param document Optional XML document that contains the data. If no document * is specified then a new document is created using mxUtils.createDocument */ public mxCodec(Document document) { if (document == null) { document = mxDomUtils.createDocument(); } this.document = document; } /** * Returns the owner document of the codec. * * @return Returns the owner document. */ public Document getDocument() { return document; } /** * Sets the owner document of the codec. */ public void setDocument(Document value) { document = value; } /** * Returns if default values of member variables should be encoded. */ public boolean isEncodeDefaults() { return encodeDefaults; } /** * Sets if default values of member variables should be encoded. */ public void setEncodeDefaults(boolean encodeDefaults) { this.encodeDefaults = encodeDefaults; } /** * Returns the object lookup table. */ public Map<String, Object> getObjects() { return objects; } /** * Assoiates the given object with the given ID. * * @param id ID for the object to be associated with. * @param object Object to be associated with the ID. * @return Returns the given object. */ public Object putObject(String id, Object object) { return objects.put(id, object); } /** * Returns the decoded object for the element with the specified ID in * {@link #document}. If the object is not known then {@link #lookup(String)} * is used to find an object. If no object is found, then the element with * the respective ID from the document is parsed using {@link #decode(Node)}. * * @param id ID of the object to be returned. * @return Returns the object for the given ID. */ public Object getObject(String id) { Object obj = null; if (id != null) { obj = objects.get(id); if (obj == null) { obj = lookup(id); if (obj == null) { Node node = getElementById(id); if (node != null) { obj = decode(node); } } } } return obj; } /** * Hook for subclassers to implement a custom lookup mechanism for cell IDs. * This implementation always returns null. * * @param id ID of the object to be returned. * @return Returns the object for the given ID. */ public Object lookup(String id) { return null; } /** * Returns the element with the given ID from the document. * * @param id ID of the element to be returned. * @return Returns the element for the given ID. */ public Node getElementById(String id) { return getElementById(id, null); } /** * Returns the element with the given ID from document. The optional attr * argument specifies the name of the ID attribute. Default is "id". The * XPath expression used to find the element is //*[@attr='arg'] where attr * is the name of the ID attribute and arg is the given id. * * Parameters: * * id - String that contains the ID. * attr - Optional string for the attributename. Default is id. */ public Node getElementById(String id, String attr) { if (attr == null) { attr = "id"; } String expr = "//*[@" + attr + "='" + id + "']"; return mxUtils.selectSingleNode(document, expr); } /** * Returns the ID of the specified object. This implementation calls * reference first and if that returns null handles the object as an * mxCell by returning their IDs using mxCell.getId. If no ID exists for * the given cell, then an on-the-fly ID is generated using * mxCellPath.create. * * @param obj Object to return the ID for. * @return Returns the ID for the given object. */ public String getId(Object obj) { String id = null; if (obj != null) { id = reference(obj); if (id == null && obj instanceof mxICell) { id = ((mxICell) obj).getId(); if (id == null) { // Uses an on-the-fly Id id = mxCellPath.create((mxICell) obj); if (id.length() == 0) { id = "root"; } } } } return id; } /** * Hook for subclassers to implement a custom method for retrieving IDs from * objects. This implementation always returns null. * * @param obj Object whose ID should be returned. * @return Returns the ID for the given object. */ public String reference(Object obj) { return null; } /** * Encodes the specified object and returns the resulting XML node. * * @param obj Object to be encoded. * @return Returns an XML node that represents the given object. */ public Node encode(Object obj) { Node node = null; if (obj != null) { String name = mxCodecRegistry.getName(obj); mxObjectCodec enc = mxCodecRegistry.getCodec(name); if (enc != null) { node = enc.encode(this, obj); } else { if (obj instanceof Node) { node = ((Node) obj).cloneNode(true); } else { System.err.println("No codec for " + name); } } } return node; } /** * Decodes the given XML node using {@link #decode(Node, Object)}. * * @param node XML node to be decoded. * @return Returns an object that represents the given node. */ public Object decode(Node node) { return decode(node, null); } /** * Decodes the given XML node. The optional "into" argument specifies an * existing object to be used. If no object is given, then a new * instance is created using the constructor from the codec. * * The function returns the passed in object or the new instance if no * object was given. * * @param node XML node to be decoded. * @param into Optional object to be decodec into. * @return Returns an object that represents the given node. */ public Object decode(Node node, Object into) { Object obj = null; if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { mxObjectCodec codec = mxCodecRegistry.getCodec(node.getNodeName()); try { if (codec != null) { obj = codec.decode(this, node, into); } else { obj = node.cloneNode(true); ((Element) obj).removeAttribute("as"); } } catch (Exception e) { System.err.println("Cannot decode " + node.getNodeName() + ": " + e.getMessage()); e.printStackTrace(); } } return obj; } /** * Encoding of cell hierarchies is built-into the core, but is a * higher-level function that needs to be explicitely used by the * respective object encoders (eg. mxModelCodec, mxChildChangeCodec * and mxRootChangeCodec). This implementation writes the given cell * and its children as a (flat) sequence into the given node. The * children are not encoded if the optional includeChildren is false. * The function is in charge of adding the result into the given node * and has no return value. * * @param cell mxCell to be encoded. * @param node Parent XML node to add the encoded cell into. * @param includeChildren Boolean indicating if the method * should include all descendents. */ public void encodeCell(mxICell cell, Node node, boolean includeChildren) { node.appendChild(encode(cell)); if (includeChildren) { int childCount = cell.getChildCount(); for (int i = 0; i < childCount; i++) { encodeCell(cell.getChildAt(i), node, includeChildren); } } } /** * Decodes cells that have been encoded using inversion, ie. where the * user object is the enclosing node in the XML, and restores the group * and graph structure in the cells. Returns a new <mxCell> instance * that represents the given node. * * @param node XML node that contains the cell data. * @param restoreStructures Boolean indicating whether the graph * structure should be restored by calling insert and insertEdge on the * parent and terminals, respectively. * @return Graph cell that represents the given node. */ public mxICell decodeCell(Node node, boolean restoreStructures) { mxICell cell = null; if (node != null && node.getNodeType() == Node.ELEMENT_NODE) { // Tries to find a codec for the given node name. If that does // not return a codec then the node is the user object (an XML node // that contains the mxCell, aka inversion). mxObjectCodec decoder = mxCodecRegistry .getCodec(node.getNodeName()); // Tries to find the codec for the cell inside the user object. // This assumes all node names inside the user object are either // not registered or they correspond to a class for cells. if (!(decoder instanceof mxCellCodec)) { Node child = node.getFirstChild(); while (child != null && !(decoder instanceof mxCellCodec)) { decoder = mxCodecRegistry.getCodec(child.getNodeName()); child = child.getNextSibling(); } String name = mxCell.class.getSimpleName(); decoder = mxCodecRegistry.getCodec(name); } if (!(decoder instanceof mxCellCodec)) { String name = mxCell.class.getSimpleName(); decoder = mxCodecRegistry.getCodec(name); } cell = (mxICell) decoder.decode(this, node); if (restoreStructures) { insertIntoGraph(cell); } } return cell; } /** * Inserts the given cell into its parent and terminal cells. */ public void insertIntoGraph(mxICell cell) { mxICell parent = cell.getParent(); mxICell source = cell.getTerminal(true); mxICell target = cell.getTerminal(false); // Fixes possible inconsistencies during insert into graph cell.setTerminal(null, false); cell.setTerminal(null, true); cell.setParent(null); if (parent != null) { parent.insert(cell); } if (source != null) { source.insertEdge(cell, true); } if (target != null) { target.insertEdge(cell, false); } } /** * Sets the attribute on the specified node to value. This is a * helper method that makes sure the attribute and value arguments * are not null. * * @param node XML node to set the attribute for. * @param attribute Name of the attribute whose value should be set. * @param value New value of the attribute. */ public static void setAttribute(Node node, String attribute, Object value) { if (node.getNodeType() == Node.ELEMENT_NODE && attribute != null && value != null) { ((Element) node).setAttribute(attribute, String.valueOf(value)); } } }