//
// OMEXMLNode.java
//
/*
* ome.xml.OMEXMLNode
*
*-----------------------------------------------------------------------------
*
* Copyright (C) 2007-2008 Open Microscopy Environment
* Massachusetts Institute of Technology,
* National Institutes of Health,
* University of Dundee,
* University of Wisconsin-Madison
*
*
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*-----------------------------------------------------------------------------
*/
/*-----------------------------------------------------------------------------
*
* Written by: Curtis Rueden <ctrueden@wisc.edu>
*
*-----------------------------------------------------------------------------
*/
package ome.xml;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Hashtable;
import java.util.Vector;
import org.w3c.dom.*;
// HACK: for scan-deps.pl: The following packages are not actually "optional":
// optional org.apache.log4j, optional org.slf4j.impl
/**
* OMEXMLNode is the superclass of all OME-XML nodes. These nodes are
* similar to, but more sophisticated than, the nodes obtained from a direct
* DOM parse of an OME-XML file. Every OME-XML node is backed by a
* corresponding DOM element from the directly parsed XML, but not all DOM
* elements have a corresponding OME-XML node; some (such as FirstName within
* Experimenter) are implicit within more significant OME-XML nodes (e.g.,
* ExperimenterNode). In general, it is not guaranteed that all DOM elements
* and attributes be exposed through subclasses of OMEXMLNode, though the
* intent is for the OMEXMLNode infrastructure to be as complete as possible.
*
* Subclasses of OMEXMLNode provide OME-specific functionality such as more
* intuitive traversal of OME structures.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/ome-xml/src/ome/xml/OMEXMLNode.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/ome-xml/src/ome/xml/OMEXMLNode.java;hb=HEAD">Gitweb</a></dd></dl>
*/
public abstract class OMEXMLNode {
// -- Constants --
protected static final String[] NODE_PACKAGES = {".ome", ".spw", ".st", ""};
protected static final String LEGACY_VERSION = "2003-FC";
// -- Static fields --
/** Next free ID numbers for generating internal ID attribute values. */
protected static Hashtable nextIds = new Hashtable();
/** Prefix used for generated internal ID values. */
protected static String idPrefix = "openmicroscopy.org";
/**
* Table of existing nodes corresponding to known elements.
* This table allows us to have one node instance per element object, and
* reuse this node on demand, rather than creating a new one every time.
*/
protected static Hashtable nodeHash = new Hashtable();
/**
* Table of existing classes for known node types.
* This table allows us to avoid searching for a Class object corresponding
* to a particular node type string within each schema version's package.
*/
protected static Hashtable classHash = new Hashtable();
/**
* Table of existing constructor signatures corresponding to known
* OMEXMLNode subclasses. This table allows us to avoid searching the
* Class object for the appropriate constructor every time we need it.
*/
protected static Hashtable constructorHash = new Hashtable();
// -- Fields --
/** Associated DOM element for this node. */
protected Element element;
/** Base package of this node. */
protected String basePackage;
// -- Constructor --
/** Constructs an OME-XML node with the given associated DOM element. */
public OMEXMLNode(Element element) {
this.element = element;
if (hasID() && getNodeID() == null) setNodeID(makeID(getElementName()));
// determine base package
//String pack = getClass().getPackage().getName();
// NB: getPackage() returns null within ImageJ for some reason
String className = getClass().getName();
int dot = className.lastIndexOf(".");
if (dot < 0) dot = 0;
String pack = className.substring(0, dot);
// strip off sub-package suffix, if any
for (int i=0; i<NODE_PACKAGES.length; i++) {
if (pack.endsWith(NODE_PACKAGES[i])) {
pack = pack.substring(0, pack.length() - NODE_PACKAGES[i].length());
break;
}
}
basePackage = pack;
}
// -- Static utility methods --
/** Gets the prefix used when generating internal ID values. */
public static String getIDPrefix() { return idPrefix; }
/** Sets the prefix used when generating internal ID values. */
public static void setIDPrefix(String prefix) {
String regex = "(\\S+\\.\\S+)+";
if (!prefix.matches(regex)) {
throw new IllegalArgumentException(
"Prefix does not match regular expression: " + regex);
}
idPrefix = prefix;
}
/** Clears the cached nodes and resets node indices. */
public static void clearCaches() {
nextIds.clear();
classHash.clear();
nodeHash.clear();
constructorHash.clear();
}
// -- OMEXMLNode API methods --
/** Gets the DOM element backing this OME-XML node. */
public Element getDOMElement() { return element; }
/** Gets the name of the DOM element. */
public String getElementName() { return DOMUtil.getName(element); }
/** Gets whether this type of node should have an ID. */
public abstract boolean hasID();
/** Gets the ID for this node, or null if none. */
public String getNodeID() { return getAttribute("ID"); }
/** Sets the ID for this node. */
public void setNodeID(String id) { setAttribute("ID", id); }
/**
* Gets the next available internal ID for use
* within this OME-XML node's tree structure.
*/
public String makeID(String nodeType) {
Integer id = (Integer) nextIds.get(nodeType);
int q = id == null ? 0 : id.intValue();
nextIds.put(nodeType, new Integer(q + 1));
String s;
if (isLegacy()) s = idPrefix + ":" + nodeType + ":" + q;
else s = nodeType + ":" + q;
return s;
}
/** Gets the node's character data. */
public String getCData() { return DOMUtil.getCharacterData(element); }
/** Sets the node's character data. */
public void setCData(String value) {
DOMUtil.setCharacterData(value, element);
}
/** Gets whether the current OME-XML hierarchy is a legacy version. */
public boolean isLegacy() {
return getVersion().equals(LEGACY_VERSION);
}
/**
* Gets the OME-XML schema version ("2003-FC", "2007-06", etc.)
* for the current OME-XML hierarchy.
*/
public String getVersion() {
String name = getClass().getName();
if (name.startsWith("org.openmicroscopy.xml.")) return LEGACY_VERSION;
String prefix = "ome.xml.r";
if (name.startsWith(prefix)) {
int dot = name.indexOf(".", prefix.length());
if (dot >= 0) {
String numbers = name.substring(prefix.length(), dot);
if (numbers.length() == 6) {
return numbers.substring(0, 4) + "-" +
numbers.substring(4, 6).toUpperCase();
}
}
}
return null; // unknown package
}
// -- Internal OMEXMLNode API methods --
/** Gets the number of child elements with the given name. */
protected int getChildCount(String name) {
return getSize(DOMUtil.getChildElements(name, element));
}
/**
* Gets an OME-XML node representing the first child with the given name.
*
* <b>NB: This method has public access only for legacy reasons,
* and direct usage is discouraged.</b>
*/
public OMEXMLNode getChildNode(String name) {
return getNode(DOMUtil.getChildElement(name, element));
}
/**
* Gets an OME-XML node of the specified type
* representing the first child with the given name.
*/
protected OMEXMLNode getChildNode(String nodeType, String name) {
return createNode(nodeType, DOMUtil.getChildElement(name, element));
}
/**
* Gets a list of OME-XML node children with the given name.
*
* <b>NB: This method has public access only for legacy reasons,
* and direct usage is discouraged.</b>
*/
public Vector getChildNodes(String name) {
return getNodes(DOMUtil.getChildElements(name, element));
}
/** Gets the index-th OME-XML node child with the given name. */
protected OMEXMLNode getChildNode(String name, int index) {
return getNode(DOMUtil.getChildElement(name, element, index));
}
/**
* Gets an OME-XML node representing the
* first ancestor element with the given name.
*/
protected OMEXMLNode getAncestorNode(String name) {
return getNode(getAncestorElement(name));
}
/**
* Gets an OME-XML node of the given type representing the first
* element referenced by a child element with the given name.
*
* For example, if this node is an ImageNode,
* getReferencedNode("Pixels", "AcquiredPixelsRef") will return a
* PixelsNode for the Pixels element whose ID matches the one given
* by the AcquiredPixelsRef child element.
*/
protected OMEXMLNode getReferencedNode(String nodeType, String refName) {
Element ref = getChildElement(refName);
if (ref == null) return null;
Element el = findElement(nodeType, DOMUtil.getAttribute("ID", ref));
return getNode(el);
}
/**
* Gets a list of all OME-XML nodes of the given type representing
* the elements referenced by the child elements with the given name.
*
* For example, if this node is an ImageNode,
* getReferencedNodes("Dataset", "DatasetRef") will return a list of
* DatasetNode objects for the Dataset elements whose IDs match the
* ones given by the DatasetRef child elements.
*/
protected Vector getReferencedNodes(String nodeType, String refName) {
Vector refs = getChildElements(refName);
if (refs == null) return null;
Vector els = new Vector();
for (int i=0; i<refs.size(); i++) {
Element ref = (Element) refs.get(i);
Element el = findElement(nodeType, DOMUtil.getAttribute("ID", ref));
els.add(el);
}
return getNodes(els);
}
/**
* Gets an OME-XML node of the given type representing the first
* element referenced by an attribute with the given name.
*
* For example, if this node is an ImageNode,
* getAttrReferencedNode("Pixels", "DefaultPixels") will return a
* PixelsNode for the Pixels element whose ID matches the one given
* by the DefaultPixels attribute.
*/
protected OMEXMLNode getAttrReferencedNode(String nodeType, String attrName) {
Element el = findElement(nodeType, getAttribute(attrName));
return getNode(el);
}
/**
* Sets the OME-XML referenced by the attribute with the given name.
*
* For example, if this node is an ImageNode,
* setAttrReferencedNode(pixelsNode, "DefaultPixels") will set the
* DefaultPixels attribute value to match the given PixelsNode's ID.
*/
protected void setAttrReferencedNode(OMEXMLNode node, String attrName) {
if (node == null || attrName == null) return;
String id = DOMUtil.getAttribute("ID", node.getDOMElement());
if (id != null) setAttribute(attrName, id);
}
/**
* Gets the number of elements of a certain type (with the given name)
* that reference this OME-XML node using a *Ref child element.
*/
protected int getReferringCount(String name) {
return getReferringCount(name, getElementName() + "Ref");
}
/**
* Gets the number of elements of a certain type (with the given name)
* that reference this OME-XML node using a child element with name refName.
*/
protected int getReferringCount(String name, String refName) {
return getSize(findReferringElements(name, refName, getNodeID()));
}
/**
* Gets a list of nodes of a certain type (with the given name)
* that reference this OME-XML node using a *Ref child element.
*
* For example, if this node is a Project node and
* getReferringNodes("Dataset") is called, it will search the DOM structure
* for Datasets with child ProjectRefs element whose IDs match this Project
* node's ID value.
*/
protected Vector getReferringNodes(String name) {
return getReferringNodes(name, getElementName() + "Ref");
}
/**
* Gets a list of nodes of a certain type (with the given name)
* that refer to this OME-XML node using a child element with name refName.
*
* For example, if this node is an Experimenter node and
* getReferringNodes("ExperimenterGroup", "Contact") is called, it will
* search the DOM structure for ExperimenterGroups with child Contact
* elements whose IDs match this Experimenter node's ID value.
*/
protected Vector getReferringNodes(String name, String refName) {
return getNodes(findReferringElements(name, refName, getNodeID()));
}
/**
* Gets the number of elements of a certain type (with the given name)
* that reference this OME-XML node using an attribute.
*/
protected int getAttrReferringCount(String name, String attrName) {
return getSize(DOMUtil.findElementList(name,
attrName, getNodeID(), element.getOwnerDocument()));
}
/**
* Gets a list of nodes of a certain type (with the given name)
* that refer to this OME-XML node using an attribute.
*
* For example, if this node is a PixelsNode and
* getAttrReferringNodes("ChannelComponent", "Pixels") is called, it will
* search the DOM structure for ChannelComponents with Pixels
* attributes whose values match this Pixels node's ID value.
*/
protected Vector getAttrReferringNodes(String name, String attrName) {
return getNodes(DOMUtil.findElementList(name,
attrName, getNodeID(), element.getOwnerDocument()));
}
/**
* Creates a reference element beneath this node.
*
* For example, if this node is a DatasetNode and "project" is a ProjectNode,
* createReference(project) references the Project from this Dataset by
* adding a child XML element called ProjectRef with an ID attribute matching
* the ID of the referenced Project.
*/
protected void createReference(OMEXMLNode node) {
Element ref = DOMUtil.createChild(element, node.getElementName() + "Ref");
DOMUtil.setAttribute("ID", node.getNodeID(), ref);
}
/** Gets the given child node's character data. */
protected String getCData(String name) {
return DOMUtil.getCharacterData(getChildElement(name));
}
/**
* Gets the given child node's character data.
* @see #getCData(String)
*/
protected String getStringCData(String name) {
return getCData(name);
}
/**
* Sets the given child node's character data to the specified value,
* creating the child node if necessary.
*/
protected void setCData(String name, String value) {
DOMUtil.setCharacterData(value, getChildElement(name, true));
}
/**
* Sets the given child node's character data
* to the specified Object's string representation,
* creating the child node if necessary.
*/
protected void setCData(String name, Object value) {
DOMUtil.setCharacterData(value, getChildElement(name, true));
}
/**
* Gets the given child node's character data as a Boolean,
* or null if the value is not a boolean.
*/
protected Boolean getBooleanCData(String name) {
return DOMUtil.getBooleanCharacterData(getChildElement(name));
}
/**
* Gets the given child node's character data as a Double,
* or null if the value is not a double.
*/
protected Double getDoubleCData(String name) {
return DOMUtil.getDoubleCharacterData(getChildElement(name));
}
/**
* Gets the given child node's character data as a Float,
* or null if the value is not a float.
*/
protected Float getFloatCData(String name) {
return DOMUtil.getFloatCharacterData(getChildElement(name));
}
/**
* Gets the given child node's character data as a Integer,
* or null if the value is not an integer.
*/
protected Integer getIntegerCData(String name) {
return DOMUtil.getIntegerCharacterData(getChildElement(name));
}
/**
* Gets the given child node's character data as a Long,
* or null if the value is not a long.
*/
protected Long getLongCData(String name) {
return DOMUtil.getLongCharacterData(getChildElement(name));
}
/** Gets a list of all DOM element attribute names. */
protected String[] getAttributeNames() {
return DOMUtil.getAttributeNames(element);
}
/** Gets a list of all DOM element attribute values. */
protected String[] getAttributeValues() {
return DOMUtil.getAttributeValues(element);
}
/**
* Sets the value of the DOM element's attribute with the given name
* to the specified value.
*
* <b>NB: This method has public access only for legacy reasons,
* and direct usage is discouraged.</b>
*/
public void setAttribute(String name, String value) {
DOMUtil.setAttribute(name, value, element);
}
/**
* Sets the value of the DOM element's attribute with the
* given name to the specified Object's string representation.
*/
protected void setAttribute(String name, Object value) {
DOMUtil.setAttribute(name, value, element);
}
/**
* Gets the value of the DOM element's attribute with the given name.
*
* <b>NB: This method has public access only for legacy reasons,
* and direct usage is discouraged.</b>
*/
public String getAttribute(String name) {
return DOMUtil.getAttribute(name, element);
}
/**
* Gets the value of the DOM element's attribute with the given name
* as a Boolean, or null if the value is not a boolean.
*/
protected Boolean getBooleanAttribute(String name) {
return DOMUtil.getBooleanAttribute(name, element);
}
/**
* Gets the value of the DOM element's attribute with the given name
* as a Double, or null if the value is not a double.
*/
protected Double getDoubleAttribute(String name) {
return DOMUtil.getDoubleAttribute(name, element);
}
/**
* Gets the value of the DOM element's attribute with the given name
* as a Float, or null if the value is not a float.
*/
protected Float getFloatAttribute(String name) {
return DOMUtil.getFloatAttribute(name, element);
}
/**
* Gets the value of the DOM element's attribute with the given name
* as an Integer, or null if the value is not an integer.
*/
protected Integer getIntegerAttribute(String name) {
return DOMUtil.getIntegerAttribute(name, element);
}
/**
* Gets the value of the DOM element's attribute with the given name
* as a Long, or null if the value is not a long.
*/
protected Long getLongAttribute(String name) {
return DOMUtil.getLongAttribute(name, element);
}
/** Gets the value of the DOM element's attribute with the given name. */
protected String getStringAttribute(String name) {
return DOMUtil.getAttribute(name, element);
}
/** Gets the OME-XML node corresponding to the specified DOM element. */
protected OMEXMLNode getNode(Element el) {
if (el == null) return null;
OMEXMLNode node = (OMEXMLNode) nodeHash.get(el);
if (node == null) {
node = createNode(DOMUtil.getName(el), el);
nodeHash.put(el, node);
}
return node;
}
// -- Helper methods --
/**
* Creates an OME-XML node of the given type,
* using the specified DOM element as a source.
*/
private OMEXMLNode createNode(String nodeType, Element el) {
if (nodeType == null || el == null) return null;
Class c = getClass(nodeType);
if (c == null) return new CustomNode(el);
return createNode(c, el);
}
/**
* Creates an OME-XML node of the given type,
* using the specified DOM element as a source.
*/
private OMEXMLNode createNode(Class nodeType, Element el) {
if (nodeType == null || el == null) return null;
// construct a new instance of the given OMEXMLNode subclass
try {
Constructor con = (Constructor) constructorHash.get(nodeType);
if (con == null) {
con = nodeType.getConstructor(new Class[] {Element.class});
constructorHash.put(nodeType, con);
}
return (OMEXMLNode) con.newInstance(new Object[] {el});
}
catch (IllegalAccessException exc) { }
catch (InstantiationException exc) { }
catch (InvocationTargetException exc) { }
catch (NoSuchMethodException exc) { }
return null;
}
/**
* Gets a list of OME-XML nodes of the proper types,
* using the specified list of DOM elements as a source.
*/
private Vector getNodes(Vector els) {
if (els == null) return null;
int size = els.size();
Vector nodes = new Vector(size);
for (int i=0; i<size; i++) {
Object o = (Object) els.elementAt(i);
OMEXMLNode node = null;
if (o instanceof Element) {
Element el = (Element) o;
node = getNode(el);
}
nodes.add(node);
}
return nodes;
}
/** Gets the first child DOM element with the specified name. */
private Element getChildElement(String name) {
return getChildElement(name, false);
}
/**
* Gets the first child DOM element with the specified name,
* creating it if it does not exist and the create flag is set.
*/
private Element getChildElement(String name, boolean create) {
Element el = DOMUtil.getChildElement(name, element);
if (el == null && create) el = DOMUtil.createChild(element, name);
return el;
}
/** Gets a list of child DOM elements with the specified name. */
private Vector getChildElements(String name) {
return DOMUtil.getChildElements(name, element);
}
/** Gets the first ancestor DOM element with the specified name. */
protected Element getAncestorElement(String name) {
return DOMUtil.getAncestorElement(name, element);
}
/** Finds the DOM element with the specified name and ID attribute value. */
private Element findElement(String name, String id) {
if (name == null || id == null) return null;
return DOMUtil.findElement(name, "ID", id, element.getOwnerDocument());
}
/**
* Gets a list of elements of a certain type (with the given name)
* that refer to the element with the specified ID using a child element
* (with name refName).
*/
private Vector findReferringElements(String name, String refName, String id) {
if (name == null || refName == null || id == null) return null;
Vector possible = DOMUtil.findElementList(name, element.getOwnerDocument());
if (possible == null) return null;
Vector v = new Vector();
int psize = possible.size();
for (int i=0; i<psize; i++) {
Element el = (Element) possible.elementAt(i);
Vector refs = DOMUtil.getChildElements(refName, el);
int rsize = refs.size();
boolean match = false;
for (int j=0; j<rsize; j++) {
Element ref = (Element) refs.elementAt(j);
if (id.equals(DOMUtil.getAttribute("ID", ref))) {
match = true;
break;
}
}
if (match) v.add(el);
}
return v;
}
/**
* Gets the node class based on the given name from the class loader.
* @return null if the class could not be loaded.
*/
private Class getClass(String nodeName) {
String hashName = basePackage + ":" + nodeName;
Class c = (Class) classHash.get(hashName);
if (c != null) return c;
// find class among sub-packages
for (int i=0; i<NODE_PACKAGES.length; i++) {
String subPack = basePackage + NODE_PACKAGES[i];
String name = subPack + "." + nodeName + "Node";
try {
c = Class.forName(name);
classHash.put(hashName, c);
return c;
}
catch (ClassNotFoundException exc) { }
catch (NoClassDefFoundError err) { }
catch (RuntimeException exc) {
// HACK: workaround for bug in Apache Axis2
String msg = exc.getMessage();
if (msg != null && msg.indexOf("ClassNotFound") < 0) throw exc;
}
}
return null;
}
/** Gets the size of the vector. Returns 0 if the vector is null. */
private int getSize(Vector v) { return v == null ? 0 : v.size(); }
// -- Object API methods --
/**
* Tests whether two OME-XML nodes are equal. Nodes are considered equal if
* they are instances of the same class with the same backing DOM element.
*/
public boolean equals(Object obj) {
if (obj == null) return false;
if (!obj.getClass().equals(getClass())) return false;
return element.equals(((OMEXMLNode) obj).element);
}
/** Gets a string representation of this node. */
public String toString() { return element.toString(); }
// -- Deprecated methods --
/**
* @deprecated <b>NB: This method exists only for legacy reasons,
* and usage is discouraged.</b>
*/
public Vector getChildNodes() {
return getNodes(DOMUtil.getChildElements(null, element));
}
}