//=================================================================================================
// 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 XMPNode
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 before which the new one is inserted.
* Note: 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.
* Note: 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 XMPNode
if node has been found, null
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 XMPNode
if node has been found,
* null
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.
* Note: 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.
* Note: 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 deep clone 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 deep clone 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:
*
addChild(...)
* and addQualifier()
.
*
* @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 nulls
.
*/
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);
}
}
}