content: //=================================================================================================
  // 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.internal.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.internal.xmp.XMPConst;
  import com.adobe.internal.xmp.XMPError;
  import com.adobe.internal.xmp.XMPException;
  import com.adobe.internal.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
   *
   * @author Stefan Makswit
   * @version $Revision$
   * @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.
  	 * it will clone those nodes also which has empty child and empty value.
  	 * @see java.lang.Object#clone(), XMPNode#clone(boolean skipEmpty)
  	 */
  	
  	public Object clone(){
  		return this.clone(false);
  	}
  
  
  	/**
  	 * Performs a <b>deep clone</b> of the node and the complete subtree.
  	 * if <code>skipEmpty</code> is true, it will not clone node which has empty child and empty value.
  	 * @param skipEmpty If true, it will not clone those nodes with empty value and empty childern
  	 * @see java.lang.Object#clone()
  	 */
  	public Object clone(boolean skipEmpty)
  	{
  		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, skipEmpty);
  		if(skipEmpty && (newNode.getValue() == null || newNode.getValue().length() == 0)
  				&&(! newNode.hasChildren())){
  			newNode = null;
  		}
  
  		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
  	 * @param skipEmpty If true, it will not clone nodes with empty values and empty childern
  	 */
  	public void cloneSubtree(XMPNode destination, boolean skipEmpty)
  	{
  		try
  		{
  			for (Iterator it = iterateChildren(); it.hasNext();)
  			{
  				XMPNode child = (XMPNode) it.next();
  				if(skipEmpty && (child.getValue() == null || child.getValue().length() == 0)
  						&& (!child.hasChildren()))
  					continue;
  				XMPNode childNode = (XMPNode) child.clone(skipEmpty);
  				if(childNode == null)
  					continue;
  				destination.addChild(childNode);
  			}
  
  			for (Iterator it = iterateQualifier(); it.hasNext();)
  			{
  				XMPNode qualifier = (XMPNode) it.next();
  				if(skipEmpty && (qualifier.getValue() == null || qualifier.getValue().length() == 0)
  						&& (!qualifier.hasChildren()))
  					continue;
  				XMPNode qualNode = (XMPNode) qualifier.clone(skipEmpty);
  				if(qualNode == null)
  					continue;
  				destination.addQualifier(qualNode);
  			}
  		}
  		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.
  	 * FfF: sort schemas and properties on each level if and only if it would increase performance
  	 *
  	 * @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);
  		}
  	}
  }