//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 <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); } } }