summaryrefslogtreecommitdiff
path: root/XMPCore/src/com/adobe/xmp/impl/XMPNode.java
diff options
context:
space:
mode:
Diffstat (limited to 'XMPCore/src/com/adobe/xmp/impl/XMPNode.java')
-rw-r--r--XMPCore/src/com/adobe/xmp/impl/XMPNode.java921
1 files changed, 921 insertions, 0 deletions
diff --git a/XMPCore/src/com/adobe/xmp/impl/XMPNode.java b/XMPCore/src/com/adobe/xmp/impl/XMPNode.java
new file mode 100644
index 0000000..9170efe
--- /dev/null
+++ b/XMPCore/src/com/adobe/xmp/impl/XMPNode.java
@@ -0,0 +1,921 @@
+//=================================================================================================
+// ADOBE SYSTEMS INCORPORATED
+// Copyright 2006 Adobe Systems Incorporated
+// All Rights Reserved
+//
+// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms
+// of the Adobe license agreement accompanying it.
+// =================================================================================================
+
+package com.adobe.xmp.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import com.adobe.xmp.XMPConst;
+import com.adobe.xmp.XMPError;
+import com.adobe.xmp.XMPException;
+import com.adobe.xmp.options.PropertyOptions;
+
+
+/**
+ * A node in the internally XMP tree, which can be a schema node, a property node, an array node,
+ * an array item, a struct node or a qualifier node (without '?').
+ *
+ * Possible improvements:
+ *
+ * 1. The kind Node of node might be better represented by a class-hierarchy of different nodes.
+ * 2. The array type should be an enum
+ * 3. isImplicitNode should be removed completely and replaced by return values of fi.
+ * 4. hasLanguage, hasType should be automatically maintained by XMPNode
+ *
+ * @since 21.02.2006
+ */
+class XMPNode implements Comparable
+{
+ /** name of the node, contains different information depending of the node kind */
+ private String name;
+ /** value of the node, contains different information depending of the node kind */
+ private String value;
+ /** link to the parent node */
+ private XMPNode parent;
+ /** list of child nodes, lazy initialized */
+ private List children = null;
+ /** list of qualifier of the node, lazy initialized */
+ private List qualifier = null;
+ /** options describing the kind of the node */
+ private PropertyOptions options = null;
+
+ // internal processing options
+
+ /** flag if the node is implicitly created */
+ private boolean implicit;
+ /** flag if the node has aliases */
+ private boolean hasAliases;
+ /** flag if the node is an alias */
+ private boolean alias;
+ /** flag if the node has an "rdf:value" child node. */
+ private boolean hasValueChild;
+
+
+
+ /**
+ * Creates an <code>XMPNode</code> with initial values.
+ *
+ * @param name the name of the node
+ * @param value the value of the node
+ * @param options the options of the node
+ */
+ public XMPNode(String name, String value, PropertyOptions options)
+ {
+ this.name = name;
+ this.value = value;
+ this.options = options;
+ }
+
+
+ /**
+ * Constructor for the node without value.
+ *
+ * @param name the name of the node
+ * @param options the options of the node
+ */
+ public XMPNode(String name, PropertyOptions options)
+ {
+ this(name, null, options);
+ }
+
+
+ /**
+ * Resets the node.
+ */
+ public void clear()
+ {
+ options = null;
+ name = null;
+ value = null;
+ children = null;
+ qualifier = null;
+ }
+
+
+ /**
+ * @return Returns the parent node.
+ */
+ public XMPNode getParent()
+ {
+ return parent;
+ }
+
+
+ /**
+ * @param index an index [1..size]
+ * @return Returns the child with the requested index.
+ */
+ public XMPNode getChild(int index)
+ {
+ return (XMPNode) getChildren().get(index - 1);
+ }
+
+
+ /**
+ * Adds a node as child to this node.
+ * @param node an XMPNode
+ * @throws XMPException
+ */
+ public void addChild(XMPNode node) throws XMPException
+ {
+ // check for duplicate properties
+ assertChildNotExisting(node.getName());
+ node.setParent(this);
+ getChildren().add(node);
+ }
+
+
+ /**
+ * Adds a node as child to this node.
+ * @param index the index of the node <em>before</em> which the new one is inserted.
+ * <em>Note:</em> The node children are indexed from [1..size]!
+ * An index of size + 1 appends a node.
+ * @param node an XMPNode
+ * @throws XMPException
+ */
+ public void addChild(int index, XMPNode node) throws XMPException
+ {
+ assertChildNotExisting(node.getName());
+ node.setParent(this);
+ getChildren().add(index - 1, node);
+ }
+
+
+ /**
+ * Replaces a node with another one.
+ * @param index the index of the node that will be replaced.
+ * <em>Note:</em> The node children are indexed from [1..size]!
+ * @param node the replacement XMPNode
+ */
+ public void replaceChild(int index, XMPNode node)
+ {
+ node.setParent(this);
+ getChildren().set(index - 1, node);
+ }
+
+
+ /**
+ * Removes a child at the requested index.
+ * @param itemIndex the index to remove [1..size]
+ */
+ public void removeChild(int itemIndex)
+ {
+ getChildren().remove(itemIndex - 1);
+ cleanupChildren();
+ }
+
+
+ /**
+ * Removes a child node.
+ * If its a schema node and doesn't have any children anymore, its deleted.
+ *
+ * @param node the child node to delete.
+ */
+ public void removeChild(XMPNode node)
+ {
+ getChildren().remove(node);
+ cleanupChildren();
+ }
+
+
+ /**
+ * Removes the children list if this node has no children anymore;
+ * checks if the provided node is a schema node and doesn't have any children anymore,
+ * its deleted.
+ */
+ protected void cleanupChildren()
+ {
+ if (children.isEmpty())
+ {
+ children = null;
+ }
+ }
+
+
+ /**
+ * Removes all children from the node.
+ */
+ public void removeChildren()
+ {
+ children = null;
+ }
+
+
+ /**
+ * @return Returns the number of children without neccessarily creating a list.
+ */
+ public int getChildrenLength()
+ {
+ return children != null ?
+ children.size() :
+ 0;
+ }
+
+
+ /**
+ * @param expr child node name to look for
+ * @return Returns an <code>XMPNode</code> if node has been found, <code>null</code> otherwise.
+ */
+ public XMPNode findChildByName(String expr)
+ {
+ return find(getChildren(), expr);
+ }
+
+
+ /**
+ * @param index an index [1..size]
+ * @return Returns the qualifier with the requested index.
+ */
+ public XMPNode getQualifier(int index)
+ {
+ return (XMPNode) getQualifier().get(index - 1);
+ }
+
+
+ /**
+ * @return Returns the number of qualifier without neccessarily creating a list.
+ */
+ public int getQualifierLength()
+ {
+ return qualifier != null ?
+ qualifier.size() :
+ 0;
+ }
+
+
+ /**
+ * Appends a qualifier to the qualifier list and sets respective options.
+ * @param qualNode a qualifier node.
+ * @throws XMPException
+ */
+ public void addQualifier(XMPNode qualNode) throws XMPException
+ {
+ assertQualifierNotExisting(qualNode.getName());
+ qualNode.setParent(this);
+ qualNode.getOptions().setQualifier(true);
+ getOptions().setHasQualifiers(true);
+
+ // contraints
+ if (qualNode.isLanguageNode())
+ {
+ // "xml:lang" is always first and the option "hasLanguage" is set
+ options.setHasLanguage(true);
+ getQualifier().add(0, qualNode);
+ }
+ else if (qualNode.isTypeNode())
+ {
+ // "rdf:type" must be first or second after "xml:lang" and the option "hasType" is set
+ options.setHasType(true);
+ getQualifier().add(
+ !options.getHasLanguage() ? 0 : 1,
+ qualNode);
+ }
+ else
+ {
+ // other qualifiers are appended
+ getQualifier().add(qualNode);
+ }
+ }
+
+
+ /**
+ * Removes one qualifier node and fixes the options.
+ * @param qualNode qualifier to remove
+ */
+ public void removeQualifier(XMPNode qualNode)
+ {
+ PropertyOptions opts = getOptions();
+ if (qualNode.isLanguageNode())
+ {
+ // if "xml:lang" is removed, remove hasLanguage-flag too
+ opts.setHasLanguage(false);
+ }
+ else if (qualNode.isTypeNode())
+ {
+ // if "rdf:type" is removed, remove hasType-flag too
+ opts.setHasType(false);
+ }
+
+ getQualifier().remove(qualNode);
+ if (qualifier.isEmpty())
+ {
+ opts.setHasQualifiers(false);
+ qualifier = null;
+ }
+
+ }
+
+
+ /**
+ * Removes all qualifiers from the node and sets the options appropriate.
+ */
+ public void removeQualifiers()
+ {
+ PropertyOptions opts = getOptions();
+ // clear qualifier related options
+ opts.setHasQualifiers(false);
+ opts.setHasLanguage(false);
+ opts.setHasType(false);
+ qualifier = null;
+ }
+
+
+ /**
+ * @param expr qualifier node name to look for
+ * @return Returns a qualifier <code>XMPNode</code> if node has been found,
+ * <code>null</code> otherwise.
+ */
+ public XMPNode findQualifierByName(String expr)
+ {
+ return find(qualifier, expr);
+ }
+
+
+ /**
+ * @return Returns whether the node has children.
+ */
+ public boolean hasChildren()
+ {
+ return children != null && children.size() > 0;
+ }
+
+
+ /**
+ * @return Returns an iterator for the children.
+ * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case.
+ */
+ public Iterator iterateChildren()
+ {
+ if (children != null)
+ {
+ return getChildren().iterator();
+ }
+ else
+ {
+ return Collections.EMPTY_LIST.listIterator();
+ }
+ }
+
+
+ /**
+ * @return Returns whether the node has qualifier attached.
+ */
+ public boolean hasQualifier()
+ {
+ return qualifier != null && qualifier.size() > 0;
+ }
+
+
+ /**
+ * @return Returns an iterator for the qualifier.
+ * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case.
+ */
+ public Iterator iterateQualifier()
+ {
+ if (qualifier != null)
+ {
+ final Iterator it = getQualifier().iterator();
+
+ return new Iterator()
+ {
+ public boolean hasNext()
+ {
+ return it.hasNext();
+ }
+
+ public Object next()
+ {
+ return it.next();
+ }
+
+ public void remove()
+ {
+ throw new UnsupportedOperationException(
+ "remove() is not allowed due to the internal contraints");
+ }
+
+ };
+ }
+ else
+ {
+ return Collections.EMPTY_LIST.iterator();
+ }
+ }
+
+
+ /**
+ * Performs a <b>deep clone</b> of the node and the complete subtree.
+ *
+ * @see java.lang.Object#clone()
+ */
+ public Object clone()
+ {
+ PropertyOptions newOptions;
+ try
+ {
+ newOptions = new PropertyOptions(getOptions().getOptions());
+ }
+ catch (XMPException e)
+ {
+ // cannot happen
+ newOptions = new PropertyOptions();
+ }
+
+ XMPNode newNode = new XMPNode(name, value, newOptions);
+ cloneSubtree(newNode);
+
+ return newNode;
+ }
+
+
+ /**
+ * Performs a <b>deep clone</b> of the complete subtree (children and
+ * qualifier )into and add it to the destination node.
+ *
+ * @param destination the node to add the cloned subtree
+ */
+ public void cloneSubtree(XMPNode destination)
+ {
+ try
+ {
+ for (Iterator it = iterateChildren(); it.hasNext();)
+ {
+ XMPNode child = (XMPNode) it.next();
+ destination.addChild((XMPNode) child.clone());
+ }
+
+ for (Iterator it = iterateQualifier(); it.hasNext();)
+ {
+ XMPNode qualifier = (XMPNode) it.next();
+ destination.addQualifier((XMPNode) qualifier.clone());
+ }
+ }
+ catch (XMPException e)
+ {
+ // cannot happen (duplicate childs/quals do not exist in this node)
+ assert false;
+ }
+
+ }
+
+
+ /**
+ * Renders this node and the tree unter this node in a human readable form.
+ * @param recursive Flag is qualifier and child nodes shall be rendered too
+ * @return Returns a multiline string containing the dump.
+ */
+ public String dumpNode(boolean recursive)
+ {
+ StringBuffer result = new StringBuffer(512);
+ this.dumpNode(result, recursive, 0, 0);
+ return result.toString();
+ }
+
+
+ /**
+ * @see Comparable#compareTo(Object)
+ */
+ public int compareTo(Object xmpNode)
+ {
+ if (getOptions().isSchemaNode())
+ {
+ return this.value.compareTo(((XMPNode) xmpNode).getValue());
+ }
+ else
+ {
+ return this.name.compareTo(((XMPNode) xmpNode).getName());
+ }
+ }
+
+
+ /**
+ * @return Returns the name.
+ */
+ public String getName()
+ {
+ return name;
+ }
+
+
+ /**
+ * @param name The name to set.
+ */
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+
+ /**
+ * @return Returns the value.
+ */
+ public String getValue()
+ {
+ return value;
+ }
+
+
+ /**
+ * @param value The value to set.
+ */
+ public void setValue(String value)
+ {
+ this.value = value;
+ }
+
+
+ /**
+ * @return Returns the options.
+ */
+ public PropertyOptions getOptions()
+ {
+ if (options == null)
+ {
+ options = new PropertyOptions();
+ }
+ return options;
+ }
+
+
+ /**
+ * Updates the options of the node.
+ * @param options the options to set.
+ */
+ public void setOptions(PropertyOptions options)
+ {
+ this.options = options;
+ }
+
+
+ /**
+ * @return Returns the implicit flag
+ */
+ public boolean isImplicit()
+ {
+ return implicit;
+ }
+
+
+ /**
+ * @param implicit Sets the implicit node flag
+ */
+ public void setImplicit(boolean implicit)
+ {
+ this.implicit = implicit;
+ }
+
+
+ /**
+ * @return Returns if the node contains aliases (applies only to schema nodes)
+ */
+ public boolean getHasAliases()
+ {
+ return hasAliases;
+ }
+
+
+ /**
+ * @param hasAliases sets the flag that the node contains aliases
+ */
+ public void setHasAliases(boolean hasAliases)
+ {
+ this.hasAliases = hasAliases;
+ }
+
+
+ /**
+ * @return Returns if the node contains aliases (applies only to schema nodes)
+ */
+ public boolean isAlias()
+ {
+ return alias;
+ }
+
+
+ /**
+ * @param alias sets the flag that the node is an alias
+ */
+ public void setAlias(boolean alias)
+ {
+ this.alias = alias;
+ }
+
+
+ /**
+ * @return the hasValueChild
+ */
+ public boolean getHasValueChild()
+ {
+ return hasValueChild;
+ }
+
+
+ /**
+ * @param hasValueChild the hasValueChild to set
+ */
+ public void setHasValueChild(boolean hasValueChild)
+ {
+ this.hasValueChild = hasValueChild;
+ }
+
+
+
+ /**
+ * Sorts the complete datamodel according to the following rules:
+ * <ul>
+ * <li>Nodes at one level are sorted by name, that is prefix + local name
+ * <li>Starting at the root node the children and qualifier are sorted recursively,
+ * which the following exceptions.
+ * <li>Sorting will not be used for arrays.
+ * <li>Within qualifier "xml:lang" and/or "rdf:type" stay at the top in that order,
+ * all others are sorted.
+ * </ul>
+ */
+ public void sort()
+ {
+ // sort qualifier
+ if (hasQualifier())
+ {
+ XMPNode[] quals = (XMPNode[]) getQualifier()
+ .toArray(new XMPNode[getQualifierLength()]);
+ int sortFrom = 0;
+ while (
+ quals.length > sortFrom &&
+ (XMPConst.XML_LANG.equals(quals[sortFrom].getName()) ||
+ "rdf:type".equals(quals[sortFrom].getName()))
+ )
+ {
+ quals[sortFrom].sort();
+ sortFrom++;
+ }
+
+ Arrays.sort(quals, sortFrom, quals.length);
+ ListIterator it = qualifier.listIterator();
+ for (int j = 0; j < quals.length; j++)
+ {
+ it.next();
+ it.set(quals[j]);
+ quals[j].sort();
+ }
+ }
+
+ // sort children
+ if (hasChildren())
+ {
+ if (!getOptions().isArray())
+ {
+ Collections.sort(children);
+ }
+ for (Iterator it = iterateChildren(); it.hasNext();)
+ {
+ ((XMPNode) it.next()).sort();
+
+ }
+ }
+ }
+
+
+
+ //------------------------------------------------------------------------------ private methods
+
+
+ /**
+ * Dumps this node and its qualifier and children recursively.
+ * <em>Note:</em> It creats empty options on every node.
+ *
+ * @param result the buffer to append the dump.
+ * @param recursive Flag is qualifier and child nodes shall be rendered too
+ * @param indent the current indent level.
+ * @param index the index within the parent node (important for arrays)
+ */
+ private void dumpNode(StringBuffer result, boolean recursive, int indent, int index)
+ {
+ // write indent
+ for (int i = 0; i < indent; i++)
+ {
+ result.append('\t');
+ }
+
+ // render Node
+ if (parent != null)
+ {
+ if (getOptions().isQualifier())
+ {
+ result.append('?');
+ result.append(name);
+ }
+ else if (getParent().getOptions().isArray())
+ {
+ result.append('[');
+ result.append(index);
+ result.append(']');
+ }
+ else
+ {
+ result.append(name);
+ }
+ }
+ else
+ {
+ // applies only to the root node
+ result.append("ROOT NODE");
+ if (name != null && name.length() > 0)
+ {
+ // the "about" attribute
+ result.append(" (");
+ result.append(name);
+ result.append(')');
+ }
+ }
+
+ if (value != null && value.length() > 0)
+ {
+ result.append(" = \"");
+ result.append(value);
+ result.append('"');
+ }
+
+ // render options if at least one is set
+ if (getOptions().containsOneOf(0xffffffff))
+ {
+ result.append("\t(");
+ result.append(getOptions().toString());
+ result.append(" : ");
+ result.append(getOptions().getOptionsString());
+ result.append(')');
+ }
+
+ result.append('\n');
+
+ // render qualifier
+ if (recursive && hasQualifier())
+ {
+ XMPNode[] quals = (XMPNode[]) getQualifier()
+ .toArray(new XMPNode[getQualifierLength()]);
+ int i = 0;
+ while (quals.length > i &&
+ (XMPConst.XML_LANG.equals(quals[i].getName()) ||
+ "rdf:type".equals(quals[i].getName()))
+ )
+ {
+ i++;
+ }
+ Arrays.sort(quals, i, quals.length);
+ for (i = 0; i < quals.length; i++)
+ {
+ XMPNode qualifier = quals[i];
+ qualifier.dumpNode(result, recursive, indent + 2, i + 1);
+ }
+ }
+
+ // render children
+ if (recursive && hasChildren())
+ {
+ XMPNode[] children = (XMPNode[]) getChildren()
+ .toArray(new XMPNode[getChildrenLength()]);
+ if (!getOptions().isArray())
+ {
+ Arrays.sort(children);
+ }
+ for (int i = 0; i < children.length; i++)
+ {
+ XMPNode child = children[i];
+ child.dumpNode(result, recursive, indent + 1, i + 1);
+ }
+ }
+ }
+
+
+ /**
+ * @return Returns whether this node is a language qualifier.
+ */
+ private boolean isLanguageNode()
+ {
+ return XMPConst.XML_LANG.equals(name);
+ }
+
+
+ /**
+ * @return Returns whether this node is a type qualifier.
+ */
+ private boolean isTypeNode()
+ {
+ return "rdf:type".equals(name);
+ }
+
+
+ /**
+ * <em>Note:</em> This method should always be called when accessing 'children' to be sure
+ * that its initialized.
+ * @return Returns list of children that is lazy initialized.
+ */
+ private List getChildren()
+ {
+ if (children == null)
+ {
+ children = new ArrayList(0);
+ }
+ return children;
+ }
+
+
+ /**
+ * @return Returns a read-only copy of child nodes list.
+ */
+ public List getUnmodifiableChildren()
+ {
+ return Collections.unmodifiableList(new ArrayList(getChildren()));
+ }
+
+
+ /**
+ * @return Returns list of qualifier that is lazy initialized.
+ */
+ private List getQualifier()
+ {
+ if (qualifier == null)
+ {
+ qualifier = new ArrayList(0);
+ }
+ return qualifier;
+ }
+
+
+ /**
+ * Sets the parent node, this is solely done by <code>addChild(...)</code>
+ * and <code>addQualifier()</code>.
+ *
+ * @param parent
+ * Sets the parent node.
+ */
+ protected void setParent(XMPNode parent)
+ {
+ this.parent = parent;
+ }
+
+
+ /**
+ * Internal find.
+ * @param list the list to search in
+ * @param expr the search expression
+ * @return Returns the found node or <code>nulls</code>.
+ */
+ private XMPNode find(List list, String expr)
+ {
+
+ if (list != null)
+ {
+ for (Iterator it = list.iterator(); it.hasNext();)
+ {
+ XMPNode child = (XMPNode) it.next();
+ if (child.getName().equals(expr))
+ {
+ return child;
+ }
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Checks that a node name is not existing on the same level, except for array items.
+ * @param childName the node name to check
+ * @throws XMPException Thrown if a node with the same name is existing.
+ */
+ private void assertChildNotExisting(String childName) throws XMPException
+ {
+ if (!XMPConst.ARRAY_ITEM_NAME.equals(childName) &&
+ findChildByName(childName) != null)
+ {
+ throw new XMPException("Duplicate property or field node '" + childName + "'",
+ XMPError.BADXMP);
+ }
+ }
+
+
+ /**
+ * Checks that a qualifier name is not existing on the same level.
+ * @param qualifierName the new qualifier name
+ * @throws XMPException Thrown if a node with the same name is existing.
+ */
+ private void assertQualifierNotExisting(String qualifierName) throws XMPException
+ {
+ if (!XMPConst.ARRAY_ITEM_NAME.equals(qualifierName) &&
+ findQualifierByName(qualifierName) != null)
+ {
+ throw new XMPException("Duplicate '" + qualifierName + "' qualifier", XMPError.BADXMP);
+ }
+ }
+} \ No newline at end of file