1 //=================================================================================================
2 // ADOBE SYSTEMS INCORPORATED
3 // Copyright 2006 Adobe Systems Incorporated
4 // All Rights Reserved
5 //
6 // NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the terms
7 // of the Adobe license agreement accompanying it.
8 // =================================================================================================
9 
10 package com.adobe.xmp.impl;
11 
12 import java.util.ArrayList;
13 import java.util.Arrays;
14 import java.util.Collections;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.ListIterator;
18 
19 import com.adobe.xmp.XMPConst;
20 import com.adobe.xmp.XMPError;
21 import com.adobe.xmp.XMPException;
22 import com.adobe.xmp.options.PropertyOptions;
23 
24 
25 /**
26  * A node in the internally XMP tree, which can be a schema node, a property node, an array node,
27  * an array item, a struct node or a qualifier node (without '?').
28  *
29  * Possible improvements:
30  *
31  * 1. The kind Node of node might be better represented by a class-hierarchy of different nodes.
32  * 2. The array type should be an enum
33  * 3. isImplicitNode should be removed completely and replaced by return values of fi.
34  * 4. hasLanguage, hasType should be automatically maintained by XMPNode
35  *
36  * @since 21.02.2006
37  */
38 class XMPNode implements Comparable
39 {
40 	/** name of the node, contains different information depending of the node kind */
41 	private String name;
42 	/** value of the node, contains different information depending of the node kind */
43 	private String value;
44 	/** link to the parent node */
45 	private XMPNode parent;
46 	/** list of child nodes, lazy initialized */
47 	private List children = null;
48 	/** list of qualifier of the node, lazy initialized */
49 	private List qualifier = null;
50 	/** options describing the kind of the node */
51 	private PropertyOptions options = null;
52 
53 	// internal processing options
54 
55 	/** flag if the node is implicitly created */
56 	private boolean implicit;
57 	/** flag if the node has aliases */
58 	private boolean hasAliases;
59 	/** flag if the node is an alias */
60 	private boolean alias;
61 	/** flag if the node has an "rdf:value" child node. */
62 	private boolean hasValueChild;
63 
64 
65 
66 	/**
67 	 * Creates an <code>XMPNode</code> with initial values.
68 	 *
69 	 * @param name the name of the node
70 	 * @param value the value of the node
71 	 * @param options the options of the node
72 	 */
XMPNode(String name, String value, PropertyOptions options)73 	public XMPNode(String name, String value, PropertyOptions options)
74 	{
75 		this.name = name;
76 		this.value = value;
77 		this.options = options;
78 	}
79 
80 
81 	/**
82 	 * Constructor for the node without value.
83 	 *
84 	 * @param name the name of the node
85 	 * @param options the options of the node
86 	 */
XMPNode(String name, PropertyOptions options)87 	public XMPNode(String name, PropertyOptions options)
88 	{
89 		this(name, null, options);
90 	}
91 
92 
93 	/**
94 	 * Resets the node.
95 	 */
clear()96 	public void clear()
97 	{
98 		options = null;
99 		name = null;
100 		value = null;
101 		children = null;
102 		qualifier = null;
103 	}
104 
105 
106 	/**
107 	 * @return Returns the parent node.
108 	 */
getParent()109 	public XMPNode getParent()
110 	{
111 		return parent;
112 	}
113 
114 
115 	/**
116 	 * @param index an index [1..size]
117 	 * @return Returns the child with the requested index.
118 	 */
getChild(int index)119 	public XMPNode getChild(int index)
120 	{
121 		return (XMPNode) getChildren().get(index - 1);
122 	}
123 
124 
125 	/**
126 	 * Adds a node as child to this node.
127 	 * @param node an XMPNode
128 	 * @throws XMPException
129 	 */
addChild(XMPNode node)130 	public void addChild(XMPNode node) throws XMPException
131 	{
132 		// check for duplicate properties
133 		assertChildNotExisting(node.getName());
134 		node.setParent(this);
135 		getChildren().add(node);
136 	}
137 
138 
139 	/**
140 	 * Adds a node as child to this node.
141 	 * @param index the index of the node <em>before</em> which the new one is inserted.
142 	 * <em>Note:</em> The node children are indexed from [1..size]!
143 	 * An index of size + 1 appends a node.
144 	 * @param node an XMPNode
145 	 * @throws XMPException
146 	 */
addChild(int index, XMPNode node)147 	public void addChild(int index, XMPNode node) throws XMPException
148 	{
149 		assertChildNotExisting(node.getName());
150 		node.setParent(this);
151 		getChildren().add(index - 1, node);
152 	}
153 
154 
155 	/**
156 	 * Replaces a node with another one.
157 	 * @param index the index of the node that will be replaced.
158 	 * <em>Note:</em> The node children are indexed from [1..size]!
159 	 * @param node the replacement XMPNode
160 	 */
replaceChild(int index, XMPNode node)161 	public void replaceChild(int index, XMPNode node)
162 	{
163 		node.setParent(this);
164 		getChildren().set(index - 1, node);
165 	}
166 
167 
168 	/**
169 	 * Removes a child at the requested index.
170 	 * @param itemIndex the index to remove [1..size]
171 	 */
removeChild(int itemIndex)172 	public void removeChild(int itemIndex)
173 	{
174 		getChildren().remove(itemIndex - 1);
175 		cleanupChildren();
176 	}
177 
178 
179 	/**
180 	 * Removes a child node.
181 	 * If its a schema node and doesn't have any children anymore, its deleted.
182 	 *
183 	 * @param node the child node to delete.
184 	 */
removeChild(XMPNode node)185 	public void removeChild(XMPNode node)
186 	{
187 		getChildren().remove(node);
188 		cleanupChildren();
189 	}
190 
191 
192 	/**
193 	 * Removes the children list if this node has no children anymore;
194 	 * checks if the provided node is a schema node and doesn't have any children anymore,
195 	 * its deleted.
196 	 */
cleanupChildren()197 	protected void cleanupChildren()
198 	{
199 		if (children.isEmpty())
200 		{
201 			children = null;
202 		}
203 	}
204 
205 
206 	/**
207 	 * Removes all children from the node.
208 	 */
removeChildren()209 	public void removeChildren()
210 	{
211 		children = null;
212 	}
213 
214 
215 	/**
216 	 * @return Returns the number of children without neccessarily creating a list.
217 	 */
getChildrenLength()218 	public int getChildrenLength()
219 	{
220 		return children != null ?
221 			children.size() :
222 			0;
223 	}
224 
225 
226 	/**
227 	 * @param expr child node name to look for
228 	 * @return Returns an <code>XMPNode</code> if node has been found, <code>null</code> otherwise.
229 	 */
findChildByName(String expr)230 	public XMPNode findChildByName(String expr)
231 	{
232 		return find(getChildren(), expr);
233 	}
234 
235 
236 	/**
237 	 * @param index an index [1..size]
238 	 * @return Returns the qualifier with the requested index.
239 	 */
getQualifier(int index)240 	public XMPNode getQualifier(int index)
241 	{
242 		return (XMPNode) getQualifier().get(index - 1);
243 	}
244 
245 
246 	/**
247 	 * @return Returns the number of qualifier without neccessarily creating a list.
248 	 */
getQualifierLength()249 	public int getQualifierLength()
250 	{
251 		return qualifier != null ?
252 			qualifier.size() :
253 			0;
254 	}
255 
256 
257 	/**
258 	 * Appends a qualifier to the qualifier list and sets respective options.
259 	 * @param qualNode a qualifier node.
260 	 * @throws XMPException
261 	 */
addQualifier(XMPNode qualNode)262 	public void addQualifier(XMPNode qualNode) throws XMPException
263 	{
264 		assertQualifierNotExisting(qualNode.getName());
265 		qualNode.setParent(this);
266 		qualNode.getOptions().setQualifier(true);
267 		getOptions().setHasQualifiers(true);
268 
269 		// contraints
270 		if (qualNode.isLanguageNode())
271 		{
272 			// "xml:lang" is always first and the option "hasLanguage" is set
273 			options.setHasLanguage(true);
274 			getQualifier().add(0, qualNode);
275 		}
276 		else if (qualNode.isTypeNode())
277 		{
278 			// "rdf:type" must be first or second after "xml:lang" and the option "hasType" is set
279 			options.setHasType(true);
280 			getQualifier().add(
281 				!options.getHasLanguage() ? 0 : 1,
282 				qualNode);
283 		}
284 		else
285 		{
286 			// other qualifiers are appended
287 			getQualifier().add(qualNode);
288 		}
289 	}
290 
291 
292 	/**
293 	 * Removes one qualifier node and fixes the options.
294 	 * @param qualNode qualifier to remove
295 	 */
removeQualifier(XMPNode qualNode)296 	public void removeQualifier(XMPNode qualNode)
297 	{
298 		PropertyOptions opts = getOptions();
299 		if (qualNode.isLanguageNode())
300 		{
301 			// if "xml:lang" is removed, remove hasLanguage-flag too
302 			opts.setHasLanguage(false);
303 		}
304 		else if (qualNode.isTypeNode())
305 		{
306 			// if "rdf:type" is removed, remove hasType-flag too
307 			opts.setHasType(false);
308 		}
309 
310 		getQualifier().remove(qualNode);
311 		if (qualifier.isEmpty())
312 		{
313 			opts.setHasQualifiers(false);
314 			qualifier = null;
315 		}
316 
317 	}
318 
319 
320 	/**
321 	 * Removes all qualifiers from the node and sets the options appropriate.
322 	 */
removeQualifiers()323 	public void removeQualifiers()
324 	{
325 		PropertyOptions opts = getOptions();
326 		// clear qualifier related options
327 		opts.setHasQualifiers(false);
328 		opts.setHasLanguage(false);
329 		opts.setHasType(false);
330 		qualifier = null;
331 	}
332 
333 
334 	/**
335 	 * @param expr qualifier node name to look for
336 	 * @return Returns a qualifier <code>XMPNode</code> if node has been found,
337 	 * <code>null</code> otherwise.
338 	 */
findQualifierByName(String expr)339 	public XMPNode findQualifierByName(String expr)
340 	{
341 		return find(qualifier, expr);
342 	}
343 
344 
345 	/**
346 	 * @return Returns whether the node has children.
347 	 */
hasChildren()348 	public boolean hasChildren()
349 	{
350 		return children != null  &&  children.size() > 0;
351 	}
352 
353 
354 	/**
355 	 * @return Returns an iterator for the children.
356 	 * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case.
357 	 */
iterateChildren()358 	public Iterator iterateChildren()
359 	{
360 		if (children != null)
361 		{
362 			return getChildren().iterator();
363 		}
364 		else
365 		{
366 			return Collections.EMPTY_LIST.listIterator();
367 		}
368 	}
369 
370 
371 	/**
372 	 * @return Returns whether the node has qualifier attached.
373 	 */
hasQualifier()374 	public boolean hasQualifier()
375 	{
376 		return qualifier != null  &&  qualifier.size() > 0;
377 	}
378 
379 
380 	/**
381 	 * @return Returns an iterator for the qualifier.
382 	 * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case.
383 	 */
iterateQualifier()384 	public Iterator iterateQualifier()
385 	{
386 		if (qualifier != null)
387 		{
388 			final Iterator it = getQualifier().iterator();
389 
390 			return new Iterator()
391 			{
392 				public boolean hasNext()
393 				{
394 					return it.hasNext();
395 				}
396 
397 				public Object next()
398 				{
399 					return it.next();
400 				}
401 
402 				public void remove()
403 				{
404 					throw new UnsupportedOperationException(
405 							"remove() is not allowed due to the internal contraints");
406 				}
407 
408 			};
409 		}
410 		else
411 		{
412 			return Collections.EMPTY_LIST.iterator();
413 		}
414 	}
415 
416 
417 	/**
418 	 * Performs a <b>deep clone</b> of the node and the complete subtree.
419 	 *
420 	 * @see java.lang.Object#clone()
421 	 */
422 	public Object clone()
423 	{
424 		PropertyOptions newOptions;
425 		try
426 		{
427 			newOptions = new PropertyOptions(getOptions().getOptions());
428 		}
429 		catch (XMPException e)
430 		{
431 			// cannot happen
432 			newOptions = new PropertyOptions();
433 		}
434 
435 		XMPNode newNode = new XMPNode(name, value, newOptions);
436 		cloneSubtree(newNode);
437 
438 		return newNode;
439 	}
440 
441 
442 	/**
443 	 * Performs a <b>deep clone</b> of the complete subtree (children and
444 	 * qualifier )into and add it to the destination node.
445 	 *
446 	 * @param destination the node to add the cloned subtree
447 	 */
448 	public void cloneSubtree(XMPNode destination)
449 	{
450 		try
451 		{
452 			for (Iterator it = iterateChildren(); it.hasNext();)
453 			{
454 				XMPNode child = (XMPNode) it.next();
455 				destination.addChild((XMPNode) child.clone());
456 			}
457 
458 			for (Iterator it = iterateQualifier(); it.hasNext();)
459 			{
460 				XMPNode qualifier = (XMPNode) it.next();
461 				destination.addQualifier((XMPNode) qualifier.clone());
462 			}
463 		}
464 		catch (XMPException e)
465 		{
466 			// cannot happen (duplicate childs/quals do not exist in this node)
467 			assert false;
468 		}
469 
470 	}
471 
472 
473 	/**
474 	 * Renders this node and the tree unter this node in a human readable form.
475 	 * @param recursive Flag is qualifier and child nodes shall be rendered too
476 	 * @return Returns a multiline string containing the dump.
477 	 */
478 	public String dumpNode(boolean recursive)
479 	{
480 		StringBuffer result = new StringBuffer(512);
481 		this.dumpNode(result, recursive, 0, 0);
482 		return result.toString();
483 	}
484 
485 
486 	/**
487 	 * @see Comparable#compareTo(Object)
488 	 */
489 	public int compareTo(Object xmpNode)
490 	{
491 		if (getOptions().isSchemaNode())
492 		{
493 			return this.value.compareTo(((XMPNode) xmpNode).getValue());
494 		}
495 		else
496 		{
497 			return this.name.compareTo(((XMPNode) xmpNode).getName());
498 		}
499 	}
500 
501 
502 	/**
503 	 * @return Returns the name.
504 	 */
505 	public String getName()
506 	{
507 		return name;
508 	}
509 
510 
511 	/**
512 	 * @param name The name to set.
513 	 */
514 	public void setName(String name)
515 	{
516 		this.name = name;
517 	}
518 
519 
520 	/**
521 	 * @return Returns the value.
522 	 */
523 	public String getValue()
524 	{
525 		return value;
526 	}
527 
528 
529 	/**
530 	 * @param value The value to set.
531 	 */
532 	public void setValue(String value)
533 	{
534 		this.value = value;
535 	}
536 
537 
538 	/**
539 	 * @return Returns the options.
540 	 */
541 	public PropertyOptions getOptions()
542 	{
543 		if (options == null)
544 		{
545 			options = new PropertyOptions();
546 		}
547 		return options;
548 	}
549 
550 
551 	/**
552 	 * Updates the options of the node.
553 	 * @param options the options to set.
554 	 */
555 	public void setOptions(PropertyOptions options)
556 	{
557 		this.options = options;
558 	}
559 
560 
561 	/**
562 	 * @return Returns the implicit flag
563 	 */
564 	public boolean isImplicit()
565 	{
566 		return implicit;
567 	}
568 
569 
570 	/**
571 	 * @param implicit Sets the implicit node flag
572 	 */
573 	public void setImplicit(boolean implicit)
574 	{
575 		this.implicit = implicit;
576 	}
577 
578 
579 	/**
580 	 * @return Returns if the node contains aliases (applies only to schema nodes)
581 	 */
582 	public boolean getHasAliases()
583 	{
584 		return hasAliases;
585 	}
586 
587 
588 	/**
589 	 * @param hasAliases sets the flag that the node contains aliases
590 	 */
591 	public void setHasAliases(boolean hasAliases)
592 	{
593 		this.hasAliases = hasAliases;
594 	}
595 
596 
597 	/**
598 	 * @return Returns if the node contains aliases (applies only to schema nodes)
599 	 */
600 	public boolean isAlias()
601 	{
602 		return alias;
603 	}
604 
605 
606 	/**
607 	 * @param alias sets the flag that the node is an alias
608 	 */
609 	public void setAlias(boolean alias)
610 	{
611 		this.alias = alias;
612 	}
613 
614 
615 	/**
616 	 * @return the hasValueChild
617 	 */
618 	public boolean getHasValueChild()
619 	{
620 		return hasValueChild;
621 	}
622 
623 
624 	/**
625 	 * @param hasValueChild the hasValueChild to set
626 	 */
627 	public void setHasValueChild(boolean hasValueChild)
628 	{
629 		this.hasValueChild = hasValueChild;
630 	}
631 
632 
633 
634 	/**
635 	 * Sorts the complete datamodel according to the following rules:
636 	 * <ul>
637 	 * 		<li>Nodes at one level are sorted by name, that is prefix + local name
638 	 * 		<li>Starting at the root node the children and qualifier are sorted recursively,
639 	 * 			which the following exceptions.
640 	 * 		<li>Sorting will not be used for arrays.
641 	 * 		<li>Within qualifier "xml:lang" and/or "rdf:type" stay at the top in that order,
642 	 * 			all others are sorted.
643 	 * </ul>
644 	 */
645 	public void sort()
646 	{
647 		// sort qualifier
648 		if (hasQualifier())
649 		{
650 			XMPNode[] quals = (XMPNode[]) getQualifier()
651 				.toArray(new XMPNode[getQualifierLength()]);
652 			int sortFrom = 0;
653 			while (
654 					quals.length > sortFrom  &&
655 					(XMPConst.XML_LANG.equals(quals[sortFrom].getName())  ||
656 					 "rdf:type".equals(quals[sortFrom].getName()))
657 				  )
658 			{
659 				quals[sortFrom].sort();
660 				sortFrom++;
661 			}
662 
663 			Arrays.sort(quals, sortFrom, quals.length);
664 			ListIterator it = qualifier.listIterator();
665 			for (int j = 0; j < quals.length; j++)
666 			{
667 				it.next();
668 				it.set(quals[j]);
669 				quals[j].sort();
670 			}
671 		}
672 
673 		// sort children
674 		if (hasChildren())
675 		{
676 			if (!getOptions().isArray())
677 			{
678 				Collections.sort(children);
679 			}
680 			for (Iterator it = iterateChildren(); it.hasNext();)
681 			{
682 				((XMPNode) it.next()).sort();
683 
684 			}
685 		}
686 	}
687 
688 
689 
690 	//------------------------------------------------------------------------------ private methods
691 
692 
693 	/**
694 	 * Dumps this node and its qualifier and children recursively.
695 	 * <em>Note:</em> It creats empty options on every node.
696 	 *
697 	 * @param result the buffer to append the dump.
698 	 * @param recursive Flag is qualifier and child nodes shall be rendered too
699 	 * @param indent the current indent level.
700 	 * @param index the index within the parent node (important for arrays)
701 	 */
702 	private void dumpNode(StringBuffer result, boolean recursive, int indent, int index)
703 	{
704 		// write indent
705 		for (int i = 0; i < indent; i++)
706 		{
707 			result.append('\t');
708 		}
709 
710 		// render Node
711 		if (parent != null)
712 		{
713 			if (getOptions().isQualifier())
714 			{
715 				result.append('?');
716 				result.append(name);
717 			}
718 			else if (getParent().getOptions().isArray())
719 			{
720 				result.append('[');
721 				result.append(index);
722 				result.append(']');
723 			}
724 			else
725 			{
726 				result.append(name);
727 			}
728 		}
729 		else
730 		{
731 			// applies only to the root node
732 			result.append("ROOT NODE");
733 			if (name != null  &&  name.length() > 0)
734 			{
735 				// the "about" attribute
736 				result.append(" (");
737 				result.append(name);
738 				result.append(')');
739 			}
740 		}
741 
742 		if (value != null  &&  value.length() > 0)
743 		{
744 			result.append(" = \"");
745 			result.append(value);
746 			result.append('"');
747 		}
748 
749 		// render options if at least one is set
750 		if (getOptions().containsOneOf(0xffffffff))
751 		{
752 			result.append("\t(");
753 			result.append(getOptions().toString());
754 			result.append(" : ");
755 			result.append(getOptions().getOptionsString());
756 			result.append(')');
757 		}
758 
759 		result.append('\n');
760 
761 		// render qualifier
762 		if (recursive  &&  hasQualifier())
763 		{
764 			XMPNode[] quals = (XMPNode[]) getQualifier()
765 				.toArray(new XMPNode[getQualifierLength()]);
766 			int i = 0;
767 			while (quals.length > i  &&
768 					(XMPConst.XML_LANG.equals(quals[i].getName())  ||
769 					 "rdf:type".equals(quals[i].getName()))
770 				  )
771 			{
772 				i++;
773 			}
774 			Arrays.sort(quals, i, quals.length);
775 			for (i = 0; i < quals.length; i++)
776 			{
777 				XMPNode qualifier = quals[i];
778 				qualifier.dumpNode(result, recursive, indent + 2, i + 1);
779 			}
780 		}
781 
782 		// render children
783 		if (recursive  &&  hasChildren())
784 		{
785 			XMPNode[] children = (XMPNode[]) getChildren()
786 				.toArray(new XMPNode[getChildrenLength()]);
787 			if (!getOptions().isArray())
788 			{
789 				Arrays.sort(children);
790 			}
791 			for (int i = 0; i < children.length; i++)
792 			{
793 				XMPNode child = children[i];
794 				child.dumpNode(result, recursive, indent + 1, i + 1);
795 			}
796 		}
797 	}
798 
799 
800 	/**
801 	 * @return Returns whether this node is a language qualifier.
802 	 */
803 	private boolean isLanguageNode()
804 	{
805 		return XMPConst.XML_LANG.equals(name);
806 	}
807 
808 
809 	/**
810 	 * @return Returns whether this node is a type qualifier.
811 	 */
812 	private boolean isTypeNode()
813 	{
814 		return "rdf:type".equals(name);
815 	}
816 
817 
818 	/**
819 	 * <em>Note:</em> This method should always be called when accessing 'children' to be sure
820 	 * that its initialized.
821 	 * @return Returns list of children that is lazy initialized.
822 	 */
823 	private List getChildren()
824 	{
825 		if (children == null)
826 		{
827 			children = new ArrayList(0);
828 		}
829 		return children;
830 	}
831 
832 
833 	/**
834 	 * @return Returns a read-only copy of child nodes list.
835 	 */
836 	public List getUnmodifiableChildren()
837 	{
838 		return Collections.unmodifiableList(new ArrayList(getChildren()));
839 	}
840 
841 
842 	/**
843 	 * @return Returns list of qualifier that is lazy initialized.
844 	 */
845 	private List getQualifier()
846 	{
847 		if (qualifier == null)
848 		{
849 			qualifier = new ArrayList(0);
850 		}
851 		return qualifier;
852 	}
853 
854 
855 	/**
856 	 * Sets the parent node, this is solely done by <code>addChild(...)</code>
857 	 * and <code>addQualifier()</code>.
858 	 *
859 	 * @param parent
860 	 *            Sets the parent node.
861 	 */
862 	protected void setParent(XMPNode parent)
863 	{
864 		this.parent = parent;
865 	}
866 
867 
868 	/**
869 	 * Internal find.
870 	 * @param list the list to search in
871 	 * @param expr the search expression
872 	 * @return Returns the found node or <code>nulls</code>.
873 	 */
874 	private XMPNode find(List list, String expr)
875 	{
876 
877 		if (list != null)
878 		{
879 			for (Iterator it = list.iterator(); it.hasNext();)
880 			{
881 				XMPNode child = (XMPNode) it.next();
882 				if (child.getName().equals(expr))
883 				{
884 					return child;
885 				}
886 			}
887 		}
888 		return null;
889 	}
890 
891 
892 	/**
893 	 * Checks that a node name is not existing on the same level, except for array items.
894 	 * @param childName the node name to check
895 	 * @throws XMPException Thrown if a node with the same name is existing.
896 	 */
897 	private void assertChildNotExisting(String childName) throws XMPException
898 	{
899 		if (!XMPConst.ARRAY_ITEM_NAME.equals(childName)  &&
900 			findChildByName(childName) != null)
901 		{
902 			throw new XMPException("Duplicate property or field node '" + childName + "'",
903 					XMPError.BADXMP);
904 		}
905 	}
906 
907 
908 	/**
909 	 * Checks that a qualifier name is not existing on the same level.
910 	 * @param qualifierName the new qualifier name
911 	 * @throws XMPException Thrown if a node with the same name is existing.
912 	 */
913 	private void assertQualifierNotExisting(String qualifierName) throws XMPException
914 	{
915 		if (!XMPConst.ARRAY_ITEM_NAME.equals(qualifierName)  &&
916 			findQualifierByName(qualifierName) != null)
917 		{
918 			throw new XMPException("Duplicate '" + qualifierName + "' qualifier", XMPError.BADXMP);
919 		}
920 	}
921 }