All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.xmp.impl.XMPNode Maven / Gradle / Ivy

//Copyright (c) 2006, Adobe Systems Incorporated
//All rights reserved.
//
//        Redistribution and use in source and binary forms, with or without
//        modification, are permitted provided that the following conditions are met:
//        1. Redistributions of source code must retain the above copyright
//        notice, this list of conditions and the following disclaimer.
//        2. Redistributions in binary form must reproduce the above copyright
//        notice, this list of conditions and the following disclaimer in the
//        documentation and/or other materials provided with the distribution.
//        3. All advertising materials mentioning features or use of this software
//        must display the following acknowledgement:
//        This product includes software developed by the Adobe Systems Incorporated.
//        4. Neither the name of the Adobe Systems Incorporated nor the
//        names of its contributors may be used to endorse or promote products
//        derived from this software without specific prior written permission.
//
//        THIS SOFTWARE IS PROVIDED BY ADOBE SYSTEMS INCORPORATED ''AS IS'' AND ANY
//        EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
//        WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//        DISCLAIMED. IN NO EVENT SHALL ADOBE SYSTEMS INCORPORATED BE LIABLE FOR ANY
//        DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
//        (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
//        LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
//        ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
//        (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
//        SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
//        http://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html

package com.itextpdf.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.itextpdf.xmp.XMPConst;
import com.itextpdf.xmp.XMPError;
import com.itextpdf.xmp.XMPException;
import com.itextpdf.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:
	 * 
    *
  • Nodes at one level are sorted by name, that is prefix + local name *
  • Starting at the root node the children and qualifier are sorted recursively, * which the following exceptions. *
  • Sorting will not be used for arrays. *
  • Within qualifier "xml:lang" and/or "rdf:type" stay at the top in that order, * all others are sorted. *
*/ 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. * Note: 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); } /** * Note: 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 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); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy