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
11
12package com.adobe.xmp.impl;
13
14import java.util.Iterator;
15
16import com.adobe.xmp.XMPConst;
17import com.adobe.xmp.XMPError;
18import com.adobe.xmp.XMPException;
19import com.adobe.xmp.XMPMeta;
20import com.adobe.xmp.XMPMetaFactory;
21import com.adobe.xmp.XMPUtils;
22import com.adobe.xmp.impl.xpath.XMPPath;
23import com.adobe.xmp.impl.xpath.XMPPathParser;
24import com.adobe.xmp.options.PropertyOptions;
25import com.adobe.xmp.properties.XMPAliasInfo;
26
27
28
29/**
30 * @since 11.08.2006
31 */
32public class XMPUtilsImpl implements XMPConst
33{
34	/** */
35	private static final int UCK_NORMAL = 0;
36	/** */
37	private static final int UCK_SPACE = 1;
38	/** */
39	private static final int UCK_COMMA = 2;
40	/** */
41	private static final int UCK_SEMICOLON = 3;
42	/** */
43	private static final int UCK_QUOTE = 4;
44	/** */
45	private static final int UCK_CONTROL = 5;
46
47
48	/**
49	 * Private constructor, as
50	 */
51	private XMPUtilsImpl()
52	{
53		// EMPTY
54	}
55
56
57	/**
58	 * @see XMPUtils#catenateArrayItems(XMPMeta, String, String, String, String,
59	 *      boolean)
60	 *
61	 * @param xmp
62	 *            The XMP object containing the array to be catenated.
63	 * @param schemaNS
64	 *            The schema namespace URI for the array. Must not be null or
65	 *            the empty string.
66	 * @param arrayName
67	 *            The name of the array. May be a general path expression, must
68	 *            not be null or the empty string. Each item in the array must
69	 *            be a simple string value.
70	 * @param separator
71	 *            The string to be used to separate the items in the catenated
72	 *            string. Defaults to "; ", ASCII semicolon and space
73	 *            (U+003B, U+0020).
74	 * @param quotes
75	 *            The characters to be used as quotes around array items that
76	 *            contain a separator. Defaults to '"'
77	 * @param allowCommas
78	 *            Option flag to control the catenation.
79	 * @return Returns the string containing the catenated array items.
80	 * @throws XMPException
81	 *             Forwards the Exceptions from the metadata processing
82	 */
83	public static String catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName,
84			String separator, String quotes, boolean allowCommas) throws XMPException
85	{
86		ParameterAsserts.assertSchemaNS(schemaNS);
87		ParameterAsserts.assertArrayName(arrayName);
88		ParameterAsserts.assertImplementation(xmp);
89		if (separator == null  ||  separator.length() == 0)
90		{
91			separator = "; ";
92		}
93		if (quotes == null  ||  quotes.length() == 0)
94		{
95			quotes = "\"";
96		}
97
98		XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp;
99		XMPNode arrayNode = null;
100		XMPNode currItem = null;
101
102		// Return an empty result if the array does not exist,
103		// hurl if it isn't the right form.
104		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
105		arrayNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), arrayPath, false, null);
106		if (arrayNode == null)
107		{
108			return "";
109		}
110		else if (!arrayNode.getOptions().isArray() || arrayNode.getOptions().isArrayAlternate())
111		{
112			throw new XMPException("Named property must be non-alternate array", XMPError.BADPARAM);
113		}
114
115		// Make sure the separator is OK.
116		checkSeparator(separator);
117		// Make sure the open and close quotes are a legitimate pair.
118		char openQuote = quotes.charAt(0);
119		char closeQuote = checkQuotes(quotes, openQuote);
120
121		// Build the result, quoting the array items, adding separators.
122		// Hurl if any item isn't simple.
123
124		StringBuffer catinatedString = new StringBuffer();
125
126		for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
127		{
128			currItem = (XMPNode) it.next();
129			if (currItem.getOptions().isCompositeProperty())
130			{
131				throw new XMPException("Array items must be simple", XMPError.BADPARAM);
132			}
133			String str = applyQuotes(currItem.getValue(), openQuote, closeQuote, allowCommas);
134
135			catinatedString.append(str);
136			if (it.hasNext())
137			{
138				catinatedString.append(separator);
139			}
140		}
141
142		return catinatedString.toString();
143	}
144
145
146	/**
147	 * see {@link XMPUtils#separateArrayItems(XMPMeta, String, String, String,
148	 * PropertyOptions, boolean)}
149	 *
150	 * @param xmp
151	 *            The XMP object containing the array to be updated.
152	 * @param schemaNS
153	 *            The schema namespace URI for the array. Must not be null or
154	 *            the empty string.
155	 * @param arrayName
156	 *            The name of the array. May be a general path expression, must
157	 *            not be null or the empty string. Each item in the array must
158	 *            be a simple string value.
159	 * @param catedStr
160	 *            The string to be separated into the array items.
161	 * @param arrayOptions
162	 *            Option flags to control the separation.
163	 * @param preserveCommas
164	 *            Flag if commas shall be preserved
165	 *
166	 * @throws XMPException
167	 *             Forwards the Exceptions from the metadata processing
168	 */
169	public static void separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName,
170			String catedStr, PropertyOptions arrayOptions, boolean preserveCommas)
171			throws XMPException
172	{
173		ParameterAsserts.assertSchemaNS(schemaNS);
174		ParameterAsserts.assertArrayName(arrayName);
175		if (catedStr == null)
176		{
177			throw new XMPException("Parameter must not be null", XMPError.BADPARAM);
178		}
179		ParameterAsserts.assertImplementation(xmp);
180		XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp;
181
182		// Keep a zero value, has special meaning below.
183		XMPNode arrayNode = separateFindCreateArray(schemaNS, arrayName, arrayOptions, xmpImpl);
184
185		// Extract the item values one at a time, until the whole input string is done.
186		String itemValue;
187		int itemStart, itemEnd;
188		int nextKind = UCK_NORMAL, charKind = UCK_NORMAL;
189		char ch = 0, nextChar = 0;
190
191		itemEnd = 0;
192		int endPos = catedStr.length();
193		while (itemEnd < endPos)
194		{
195			// Skip any leading spaces and separation characters. Always skip commas here.
196			// They can be kept when within a value, but not when alone between values.
197			for (itemStart = itemEnd; itemStart < endPos; itemStart++)
198			{
199				ch = catedStr.charAt(itemStart);
200				charKind = classifyCharacter(ch);
201				if (charKind == UCK_NORMAL || charKind == UCK_QUOTE)
202				{
203					break;
204				}
205			}
206			if (itemStart >= endPos)
207			{
208				break;
209			}
210
211			if (charKind != UCK_QUOTE)
212			{
213				// This is not a quoted value. Scan for the end, create an array
214				// item from the substring.
215				for (itemEnd = itemStart; itemEnd < endPos; itemEnd++)
216				{
217					ch = catedStr.charAt(itemEnd);
218					charKind = classifyCharacter(ch);
219
220					if (charKind == UCK_NORMAL || charKind == UCK_QUOTE  ||
221						(charKind == UCK_COMMA && preserveCommas))
222					{
223						continue;
224					}
225					else if (charKind != UCK_SPACE)
226					{
227						break;
228					}
229					else if ((itemEnd + 1) < endPos)
230					{
231						ch = catedStr.charAt(itemEnd + 1);
232						nextKind = classifyCharacter(ch);
233						if (nextKind == UCK_NORMAL  ||  nextKind == UCK_QUOTE  ||
234							(nextKind == UCK_COMMA && preserveCommas))
235						{
236							continue;
237						}
238					}
239
240					// Anything left?
241					break; // Have multiple spaces, or a space followed by a
242							// separator.
243				}
244				itemValue = catedStr.substring(itemStart, itemEnd);
245			}
246			else
247			{
248				// Accumulate quoted values into a local string, undoubling
249				// internal quotes that
250				// match the surrounding quotes. Do not undouble "unmatching"
251				// quotes.
252
253				char openQuote = ch;
254				char closeQuote = getClosingQuote(openQuote);
255
256				itemStart++; // Skip the opening quote;
257				itemValue = "";
258
259				for (itemEnd = itemStart; itemEnd < endPos; itemEnd++)
260				{
261					ch = catedStr.charAt(itemEnd);
262					charKind = classifyCharacter(ch);
263
264					if (charKind != UCK_QUOTE || !isSurroundingQuote(ch, openQuote, closeQuote))
265					{
266						// This is not a matching quote, just append it to the
267						// item value.
268						itemValue += ch;
269					}
270					else
271					{
272						// This is a "matching" quote. Is it doubled, or the
273						// final closing quote?
274						// Tolerate various edge cases like undoubled opening
275						// (non-closing) quotes,
276						// or end of input.
277
278						if ((itemEnd + 1) < endPos)
279						{
280							nextChar = catedStr.charAt(itemEnd + 1);
281							nextKind = classifyCharacter(nextChar);
282						}
283						else
284						{
285							nextKind = UCK_SEMICOLON;
286							nextChar = 0x3B;
287						}
288
289						if (ch == nextChar)
290						{
291							// This is doubled, copy it and skip the double.
292							itemValue += ch;
293							// Loop will add in charSize.
294							itemEnd++;
295						}
296						else if (!isClosingingQuote(ch, openQuote, closeQuote))
297						{
298							// This is an undoubled, non-closing quote, copy it.
299							itemValue += ch;
300						}
301						else
302						{
303							// This is an undoubled closing quote, skip it and
304							// exit the loop.
305							itemEnd++;
306							break;
307						}
308					}
309				}
310			}
311
312			// Add the separated item to the array.
313			// Keep a matching old value in case it had separators.
314			int foundIndex = -1;
315			for (int oldChild = 1; oldChild <= arrayNode.getChildrenLength(); oldChild++)
316			{
317				if (itemValue.equals(arrayNode.getChild(oldChild).getValue()))
318				{
319					foundIndex = oldChild;
320					break;
321				}
322			}
323
324			XMPNode newItem = null;
325			if (foundIndex < 0)
326			{
327				newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null);
328				arrayNode.addChild(newItem);
329			}
330		}
331	}
332
333
334	/**
335	 * Utility to find or create the array used by <code>separateArrayItems()</code>.
336	 * @param schemaNS a the namespace fo the array
337	 * @param arrayName the name of the array
338	 * @param arrayOptions the options for the array if newly created
339	 * @param xmp the xmp object
340	 * @return Returns the array node.
341	 * @throws XMPException Forwards exceptions
342	 */
343	private static XMPNode separateFindCreateArray(String schemaNS, String arrayName,
344			PropertyOptions arrayOptions, XMPMetaImpl xmp) throws XMPException
345	{
346		arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null);
347		if (!arrayOptions.isOnlyArrayOptions())
348		{
349			throw new XMPException("Options can only provide array form", XMPError.BADOPTIONS);
350		}
351
352		// Find the array node, make sure it is OK. Move the current children
353		// aside, to be readded later if kept.
354		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
355		XMPNode arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, false, null);
356		if (arrayNode != null)
357		{
358			// The array exists, make sure the form is compatible. Zero
359			// arrayForm means take what exists.
360			PropertyOptions arrayForm = arrayNode.getOptions();
361			if (!arrayForm.isArray() || arrayForm.isArrayAlternate())
362			{
363				throw new XMPException("Named property must be non-alternate array",
364					XMPError.BADXPATH);
365			}
366			if (arrayOptions.equalArrayTypes(arrayForm))
367			{
368				throw new XMPException("Mismatch of specified and existing array form",
369						XMPError.BADXPATH); // *** Right error?
370			}
371		}
372		else
373		{
374			// The array does not exist, try to create it.
375			// don't modify the options handed into the method
376			arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, true, arrayOptions
377					.setArray(true));
378			if (arrayNode == null)
379			{
380				throw new XMPException("Failed to create named array", XMPError.BADXPATH);
381			}
382		}
383		return arrayNode;
384	}
385
386
387	/**
388	 * @see XMPUtils#removeProperties(XMPMeta, String, String, boolean, boolean)
389	 *
390	 * @param xmp
391	 *            The XMP object containing the properties to be removed.
392	 *
393	 * @param schemaNS
394	 *            Optional schema namespace URI for the properties to be
395	 *            removed.
396	 *
397	 * @param propName
398	 *            Optional path expression for the property to be removed.
399	 *
400	 * @param doAllProperties
401	 *            Option flag to control the deletion: do internal properties in
402	 *            addition to external properties.
403	 * @param includeAliases
404	 *            Option flag to control the deletion: Include aliases in the
405	 *            "named schema" case above.
406	 * @throws XMPException If metadata processing fails
407	 */
408	public static void removeProperties(XMPMeta xmp, String schemaNS, String propName,
409			boolean doAllProperties, boolean includeAliases) throws XMPException
410	{
411		ParameterAsserts.assertImplementation(xmp);
412		XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp;
413
414		if (propName != null && propName.length() > 0)
415		{
416			// Remove just the one indicated property. This might be an alias,
417			// the named schema might not actually exist. So don't lookup the
418			// schema node.
419
420			if (schemaNS == null || schemaNS.length() == 0)
421			{
422				throw new XMPException("Property name requires schema namespace",
423					XMPError.BADPARAM);
424			}
425
426			XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
427
428			XMPNode propNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), expPath, false, null);
429			if (propNode != null)
430			{
431				if (doAllProperties
432						|| !Utils.isInternalProperty(expPath.getSegment(XMPPath.STEP_SCHEMA)
433								.getName(), expPath.getSegment(XMPPath.STEP_ROOT_PROP).getName()))
434				{
435					XMPNode parent = propNode.getParent();
436					parent.removeChild(propNode);
437					if (parent.getOptions().isSchemaNode()  &&  !parent.hasChildren())
438					{
439						// remove empty schema node
440						parent.getParent().removeChild(parent);
441					}
442
443				}
444			}
445		}
446		else if (schemaNS != null && schemaNS.length() > 0)
447		{
448
449			// Remove all properties from the named schema. Optionally include
450			// aliases, in which case
451			// there might not be an actual schema node.
452
453			// XMP_NodePtrPos schemaPos;
454			XMPNode schemaNode = XMPNodeUtils.findSchemaNode(xmpImpl.getRoot(), schemaNS, false);
455			if (schemaNode != null)
456			{
457				if (removeSchemaChildren(schemaNode, doAllProperties))
458				{
459					xmpImpl.getRoot().removeChild(schemaNode);
460				}
461			}
462
463			if (includeAliases)
464			{
465				// We're removing the aliases also. Look them up by their
466				// namespace prefix.
467				// But that takes more code and the extra speed isn't worth it.
468				// Lookup the XMP node
469				// from the alias, to make sure the actual exists.
470
471				XMPAliasInfo[] aliases = XMPMetaFactory.getSchemaRegistry().findAliases(schemaNS);
472				for (int i = 0; i < aliases.length; i++)
473				{
474					XMPAliasInfo info = aliases[i];
475					XMPPath path = XMPPathParser.expandXPath(info.getNamespace(), info
476							.getPropName());
477					XMPNode actualProp = XMPNodeUtils
478							.findNode(xmpImpl.getRoot(), path, false, null);
479					if (actualProp != null)
480					{
481						XMPNode parent = actualProp.getParent();
482						parent.removeChild(actualProp);
483					}
484				}
485			}
486		}
487		else
488		{
489			// Remove all appropriate properties from all schema. In this case
490			// we don't have to be
491			// concerned with aliases, they are handled implicitly from the
492			// actual properties.
493			for (Iterator it = xmpImpl.getRoot().iterateChildren(); it.hasNext();)
494			{
495				XMPNode schema = (XMPNode) it.next();
496				if (removeSchemaChildren(schema, doAllProperties))
497				{
498					it.remove();
499				}
500			}
501		}
502	}
503
504
505	/**
506	 * @see XMPUtils#appendProperties(XMPMeta, XMPMeta, boolean, boolean)
507	 * @param source The source XMP object.
508	 * @param destination The destination XMP object.
509	 * @param doAllProperties Do internal properties in addition to external properties.
510	 * @param replaceOldValues Replace the values of existing properties.
511	 * @param deleteEmptyValues Delete destination values if source property is empty.
512	 * @throws XMPException Forwards the Exceptions from the metadata processing
513	 */
514	public static void appendProperties(XMPMeta source, XMPMeta destination,
515			boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues)
516		throws XMPException
517	{
518		ParameterAsserts.assertImplementation(source);
519		ParameterAsserts.assertImplementation(destination);
520
521		XMPMetaImpl src = (XMPMetaImpl) source;
522		XMPMetaImpl dest = (XMPMetaImpl) destination;
523
524		for (Iterator it = src.getRoot().iterateChildren(); it.hasNext();)
525		{
526			XMPNode sourceSchema = (XMPNode) it.next();
527
528			// Make sure we have a destination schema node
529			XMPNode destSchema = XMPNodeUtils.findSchemaNode(dest.getRoot(),
530					sourceSchema.getName(), false);
531			boolean createdSchema = false;
532			if (destSchema == null)
533			{
534				destSchema = new XMPNode(sourceSchema.getName(), sourceSchema.getValue(),
535						new PropertyOptions().setSchemaNode(true));
536				dest.getRoot().addChild(destSchema);
537				createdSchema = true;
538			}
539
540			// Process the source schema's children.
541			for (Iterator ic = sourceSchema.iterateChildren(); ic.hasNext();)
542			{
543				XMPNode sourceProp = (XMPNode) ic.next();
544				if (doAllProperties
545						|| !Utils.isInternalProperty(sourceSchema.getName(), sourceProp.getName()))
546				{
547					appendSubtree(
548						dest, sourceProp, destSchema, replaceOldValues, deleteEmptyValues);
549				}
550			}
551
552			if (!destSchema.hasChildren()  &&  (createdSchema  ||  deleteEmptyValues))
553			{
554				// Don't create an empty schema / remove empty schema.
555				dest.getRoot().removeChild(destSchema);
556			}
557		}
558	}
559
560
561	/**
562	 * Remove all schema children according to the flag
563	 * <code>doAllProperties</code>. Empty schemas are automatically remove
564	 * by <code>XMPNode</code>
565	 *
566	 * @param schemaNode
567	 *            a schema node
568	 * @param doAllProperties
569	 *            flag if all properties or only externals shall be removed.
570	 * @return Returns true if the schema is empty after the operation.
571	 */
572	private static boolean removeSchemaChildren(XMPNode schemaNode, boolean doAllProperties)
573	{
574		for (Iterator it = schemaNode.iterateChildren(); it.hasNext();)
575		{
576			XMPNode currProp = (XMPNode) it.next();
577			if (doAllProperties
578					|| !Utils.isInternalProperty(schemaNode.getName(), currProp.getName()))
579			{
580				it.remove();
581			}
582		}
583
584		return !schemaNode.hasChildren();
585	}
586
587
588	/**
589	 * @see XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean)
590	 * @param destXMP The destination XMP object.
591	 * @param sourceNode the source node
592	 * @param destParent the parent of the destination node
593	 * @param replaceOldValues Replace the values of existing properties.
594	 * @param deleteEmptyValues flag if properties with empty values should be deleted
595	 * 		   in the destination object.
596	 * @throws XMPException
597	 */
598	private static void appendSubtree(XMPMetaImpl destXMP, XMPNode sourceNode, XMPNode destParent,
599			boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException
600	{
601		XMPNode destNode = XMPNodeUtils.findChildNode(destParent, sourceNode.getName(), false);
602
603		boolean valueIsEmpty = false;
604		if (deleteEmptyValues)
605		{
606			valueIsEmpty = sourceNode.getOptions().isSimple() ?
607				sourceNode.getValue() == null  ||  sourceNode.getValue().length() == 0 :
608				!sourceNode.hasChildren();
609		}
610
611		if (deleteEmptyValues  &&  valueIsEmpty)
612		{
613			if (destNode != null)
614			{
615				destParent.removeChild(destNode);
616			}
617		}
618		else if (destNode == null)
619		{
620			// The one easy case, the destination does not exist.
621			destParent.addChild((XMPNode) sourceNode.clone());
622		}
623		else if (replaceOldValues)
624		{
625			// The destination exists and should be replaced.
626			destXMP.setNode(destNode, sourceNode.getValue(), sourceNode.getOptions(), true);
627			destParent.removeChild(destNode);
628			destNode = (XMPNode) sourceNode.clone();
629			destParent.addChild(destNode);
630		}
631		else
632		{
633			// The destination exists and is not totally replaced. Structs and
634			// arrays are merged.
635
636			PropertyOptions sourceForm = sourceNode.getOptions();
637			PropertyOptions destForm = destNode.getOptions();
638			if (sourceForm != destForm)
639			{
640				return;
641			}
642			if (sourceForm.isStruct())
643			{
644				// To merge a struct process the fields recursively. E.g. add simple missing fields.
645				// The recursive call to AppendSubtree will handle deletion for fields with empty
646				// values.
647				for (Iterator it = sourceNode.iterateChildren(); it.hasNext();)
648				{
649					XMPNode sourceField = (XMPNode) it.next();
650					appendSubtree(destXMP, sourceField, destNode,
651						replaceOldValues, deleteEmptyValues);
652					if (deleteEmptyValues  &&  !destNode.hasChildren())
653					{
654						destParent.removeChild(destNode);
655					}
656				}
657			}
658			else if (sourceForm.isArrayAltText())
659			{
660				// Merge AltText arrays by the "xml:lang" qualifiers. Make sure x-default is first.
661				// Make a special check for deletion of empty values. Meaningful in AltText arrays
662				// because the "xml:lang" qualifier provides unambiguous source/dest correspondence.
663				for (Iterator it = sourceNode.iterateChildren(); it.hasNext();)
664				{
665					XMPNode sourceItem = (XMPNode) it.next();
666					if (!sourceItem.hasQualifier()
667							|| !XMPConst.XML_LANG.equals(sourceItem.getQualifier(1).getName()))
668					{
669						continue;
670					}
671
672					int destIndex = XMPNodeUtils.lookupLanguageItem(destNode,
673							sourceItem.getQualifier(1).getValue());
674					if (deleteEmptyValues  &&
675							(sourceItem.getValue() == null  ||
676							 sourceItem.getValue().length() == 0))
677					{
678						if (destIndex != -1)
679						{
680							destNode.removeChild(destIndex);
681							if (!destNode.hasChildren())
682							{
683								destParent.removeChild(destNode);
684							}
685						}
686					}
687					else if (destIndex == -1)
688					{
689						// Not replacing, keep the existing item.
690						if (!XMPConst.X_DEFAULT.equals(sourceItem.getQualifier(1).getValue())
691								|| !destNode.hasChildren())
692						{
693							sourceItem.cloneSubtree(destNode);
694						}
695						else
696						{
697							XMPNode destItem = new XMPNode(
698								sourceItem.getName(),
699								sourceItem.getValue(),
700								sourceItem.getOptions());
701							sourceItem.cloneSubtree(destItem);
702							destNode.addChild(1, destItem);
703						}
704					}
705				}
706			}
707			else if (sourceForm.isArray())
708			{
709				// Merge other arrays by item values. Don't worry about order or duplicates. Source
710				// items with empty values do not cause deletion, that conflicts horribly with
711				// merging.
712
713				for (Iterator is = sourceNode.iterateChildren(); is.hasNext();)
714				{
715					XMPNode sourceItem = (XMPNode) is.next();
716
717					boolean match = false;
718					for (Iterator id = destNode.iterateChildren(); id.hasNext();)
719					{
720						XMPNode destItem = (XMPNode) id.next();
721						if (itemValuesMatch(sourceItem, destItem))
722						{
723							match = true;
724						}
725					}
726					if (!match)
727					{
728						destNode = (XMPNode) sourceItem.clone();
729						destParent.addChild(destNode);
730					}
731				}
732			}
733		}
734	}
735
736
737	/**
738	 * Compares two nodes including its children and qualifier.
739	 * @param leftNode an <code>XMPNode</code>
740	 * @param rightNode an <code>XMPNode</code>
741	 * @return Returns true if the nodes are equal, false otherwise.
742	 * @throws XMPException Forwards exceptions to the calling method.
743	 */
744	private static boolean itemValuesMatch(XMPNode leftNode, XMPNode rightNode) throws XMPException
745	{
746		PropertyOptions leftForm = leftNode.getOptions();
747		PropertyOptions rightForm = rightNode.getOptions();
748
749		if (leftForm.equals(rightForm))
750		{
751			return false;
752		}
753
754		if (leftForm.getOptions() == 0)
755		{
756			// Simple nodes, check the values and xml:lang qualifiers.
757			if (!leftNode.getValue().equals(rightNode.getValue()))
758			{
759				return false;
760			}
761			if (leftNode.getOptions().getHasLanguage() != rightNode.getOptions().getHasLanguage())
762			{
763				return false;
764			}
765			if (leftNode.getOptions().getHasLanguage()
766					&& !leftNode.getQualifier(1).getValue().equals(
767							rightNode.getQualifier(1).getValue()))
768			{
769				return false;
770			}
771		}
772		else if (leftForm.isStruct())
773		{
774			// Struct nodes, see if all fields match, ignoring order.
775
776			if (leftNode.getChildrenLength() != rightNode.getChildrenLength())
777			{
778				return false;
779			}
780
781			for (Iterator it = leftNode.iterateChildren(); it.hasNext();)
782			{
783				XMPNode leftField = (XMPNode) it.next();
784				XMPNode rightField = XMPNodeUtils.findChildNode(rightNode, leftField.getName(),
785						false);
786				if (rightField == null || !itemValuesMatch(leftField, rightField))
787				{
788					return false;
789				}
790			}
791		}
792		else
793		{
794			// Array nodes, see if the "leftNode" values are present in the
795			// "rightNode", ignoring order, duplicates,
796			// and extra values in the rightNode-> The rightNode is the
797			// destination for AppendProperties.
798
799			assert leftForm.isArray();
800
801			for (Iterator il = leftNode.iterateChildren(); il.hasNext();)
802			{
803				XMPNode leftItem = (XMPNode) il.next();
804
805				boolean match = false;
806				for (Iterator ir = rightNode.iterateChildren(); ir.hasNext();)
807				{
808					XMPNode rightItem = (XMPNode) ir.next();
809					if (itemValuesMatch(leftItem, rightItem))
810					{
811						match = true;
812						break;
813					}
814				}
815				if (!match)
816				{
817					return false;
818				}
819			}
820		}
821		return true; // All of the checks passed.
822	}
823
824
825	/**
826	 * Make sure the separator is OK. It must be one semicolon surrounded by
827	 * zero or more spaces. Any of the recognized semicolons or spaces are
828	 * allowed.
829	 *
830	 * @param separator
831	 * @throws XMPException
832	 */
833	private static void checkSeparator(String separator) throws XMPException
834	{
835		boolean haveSemicolon = false;
836		for (int i = 0; i < separator.length(); i++)
837		{
838			int charKind = classifyCharacter(separator.charAt(i));
839			if (charKind == UCK_SEMICOLON)
840			{
841				if (haveSemicolon)
842				{
843					throw new XMPException("Separator can have only one semicolon",
844						XMPError.BADPARAM);
845				}
846				haveSemicolon = true;
847			}
848			else if (charKind != UCK_SPACE)
849			{
850				throw new XMPException("Separator can have only spaces and one semicolon",
851						XMPError.BADPARAM);
852			}
853		}
854		if (!haveSemicolon)
855		{
856			throw new XMPException("Separator must have one semicolon", XMPError.BADPARAM);
857		}
858	}
859
860
861	/**
862	 * Make sure the open and close quotes are a legitimate pair and return the
863	 * correct closing quote or an exception.
864	 *
865	 * @param quotes
866	 *            opened and closing quote in a string
867	 * @param openQuote
868	 *            the open quote
869	 * @return Returns a corresponding closing quote.
870	 * @throws XMPException
871	 */
872	private static char checkQuotes(String quotes, char openQuote) throws XMPException
873	{
874		char closeQuote;
875
876		int charKind = classifyCharacter(openQuote);
877		if (charKind != UCK_QUOTE)
878		{
879			throw new XMPException("Invalid quoting character", XMPError.BADPARAM);
880		}
881
882		if (quotes.length() == 1)
883		{
884			closeQuote = openQuote;
885		}
886		else
887		{
888			closeQuote = quotes.charAt(1);
889			charKind = classifyCharacter(closeQuote);
890			if (charKind != UCK_QUOTE)
891			{
892				throw new XMPException("Invalid quoting character", XMPError.BADPARAM);
893			}
894		}
895
896		if (closeQuote != getClosingQuote(openQuote))
897		{
898			throw new XMPException("Mismatched quote pair", XMPError.BADPARAM);
899		}
900		return closeQuote;
901	}
902
903
904	/**
905	 * Classifies the character into normal chars, spaces, semicola, quotes,
906	 * control chars.
907	 *
908	 * @param ch
909	 *            a char
910	 * @return Return the character kind.
911	 */
912	private static int classifyCharacter(char ch)
913	{
914		if (SPACES.indexOf(ch) >= 0 || (0x2000 <= ch && ch <= 0x200B))
915		{
916			return UCK_SPACE;
917		}
918		else if (COMMAS.indexOf(ch) >= 0)
919		{
920			return UCK_COMMA;
921		}
922		else if (SEMICOLA.indexOf(ch) >= 0)
923		{
924			return UCK_SEMICOLON;
925		}
926		else if (QUOTES.indexOf(ch) >= 0 || (0x3008 <= ch && ch <= 0x300F)
927				|| (0x2018 <= ch && ch <= 0x201F))
928		{
929			return UCK_QUOTE;
930		}
931		else if (ch < 0x0020 || CONTROLS.indexOf(ch) >= 0)
932		{
933			return UCK_CONTROL;
934		}
935		else
936		{
937			// Assume typical case.
938			return UCK_NORMAL;
939		}
940	}
941
942
943	/**
944	 * @param openQuote
945	 *            the open quote char
946	 * @return Returns the matching closing quote for an open quote.
947	 */
948	private static char getClosingQuote(char openQuote)
949	{
950		switch (openQuote)
951		{
952		case 0x0022:
953			return 0x0022; // ! U+0022 is both opening and closing.
954		case 0x005B:
955			return 0x005D;
956		case 0x00AB:
957			return 0x00BB; // ! U+00AB and U+00BB are reversible.
958		case 0x00BB:
959			return 0x00AB;
960		case 0x2015:
961			return 0x2015; // ! U+2015 is both opening and closing.
962		case 0x2018:
963			return 0x2019;
964		case 0x201A:
965			return 0x201B;
966		case 0x201C:
967			return 0x201D;
968		case 0x201E:
969			return 0x201F;
970		case 0x2039:
971			return 0x203A; // ! U+2039 and U+203A are reversible.
972		case 0x203A:
973			return 0x2039;
974		case 0x3008:
975			return 0x3009;
976		case 0x300A:
977			return 0x300B;
978		case 0x300C:
979			return 0x300D;
980		case 0x300E:
981			return 0x300F;
982		case 0x301D:
983			return 0x301F; // ! U+301E also closes U+301D.
984		default:
985			return 0;
986		}
987	}
988
989
990	/**
991	 * Add quotes to the item.
992	 *
993	 * @param item
994	 *            the array item
995	 * @param openQuote
996	 *            the open quote character
997	 * @param closeQuote
998	 *            the closing quote character
999	 * @param allowCommas
1000	 *            flag if commas are allowed
1001	 * @return Returns the value in quotes.
1002	 */
1003	private static String applyQuotes(String item, char openQuote, char closeQuote,
1004			boolean allowCommas)
1005	{
1006		if (item == null)
1007		{
1008			item = "";
1009		}
1010
1011		boolean prevSpace = false;
1012		int charOffset;
1013		int charKind;
1014
1015		// See if there are any separators in the value. Stop at the first
1016		// occurrance. This is a bit
1017		// tricky in order to make typical typing work conveniently. The purpose
1018		// of applying quotes
1019		// is to preserve the values when splitting them back apart. That is
1020		// CatenateContainerItems
1021		// and SeparateContainerItems must round trip properly. For the most
1022		// part we only look for
1023		// separators here. Internal quotes, as in -- Irving "Bud" Jones --
1024		// won't cause problems in
1025		// the separation. An initial quote will though, it will make the value
1026		// look quoted.
1027
1028		int i;
1029		for (i = 0; i < item.length(); i++)
1030		{
1031			char ch = item.charAt(i);
1032			charKind = classifyCharacter(ch);
1033			if (i == 0 && charKind == UCK_QUOTE)
1034			{
1035				break;
1036			}
1037
1038			if (charKind == UCK_SPACE)
1039			{
1040				// Multiple spaces are a separator.
1041				if (prevSpace)
1042				{
1043					break;
1044				}
1045				prevSpace = true;
1046			}
1047			else
1048			{
1049				prevSpace = false;
1050				if ((charKind == UCK_SEMICOLON || charKind == UCK_CONTROL)
1051						|| (charKind == UCK_COMMA && !allowCommas))
1052				{
1053					break;
1054				}
1055			}
1056		}
1057
1058
1059		if (i < item.length())
1060		{
1061			// Create a quoted copy, doubling any internal quotes that match the
1062			// outer ones. Internal quotes did not stop the "needs quoting"
1063			// search, but they do need
1064			// doubling. So we have to rescan the front of the string for
1065			// quotes. Handle the special
1066			// case of U+301D being closed by either U+301E or U+301F.
1067
1068			StringBuffer newItem = new StringBuffer(item.length() + 2);
1069			int splitPoint;
1070			for (splitPoint = 0; splitPoint <= i; splitPoint++)
1071			{
1072				if (classifyCharacter(item.charAt(i)) == UCK_QUOTE)
1073				{
1074					break;
1075				}
1076			}
1077
1078			// Copy the leading "normal" portion.
1079			newItem.append(openQuote).append(item.substring(0, splitPoint));
1080
1081			for (charOffset = splitPoint; charOffset < item.length(); charOffset++)
1082			{
1083				newItem.append(item.charAt(charOffset));
1084				if (classifyCharacter(item.charAt(charOffset)) == UCK_QUOTE
1085						&& isSurroundingQuote(item.charAt(charOffset), openQuote, closeQuote))
1086				{
1087					newItem.append(item.charAt(charOffset));
1088				}
1089			}
1090
1091			newItem.append(closeQuote);
1092
1093			item = newItem.toString();
1094		}
1095
1096		return item;
1097	}
1098
1099
1100	/**
1101	 * @param ch a character
1102	 * @param openQuote the opening quote char
1103	 * @param closeQuote the closing quote char
1104	 * @return Return it the character is a surrounding quote.
1105	 */
1106	private static boolean isSurroundingQuote(char ch, char openQuote, char closeQuote)
1107	{
1108		return ch == openQuote || isClosingingQuote(ch, openQuote, closeQuote);
1109	}
1110
1111
1112	/**
1113	 * @param ch a character
1114	 * @param openQuote the opening quote char
1115	 * @param closeQuote the closing quote char
1116	 * @return Returns true if the character is a closing quote.
1117	 */
1118	private static boolean isClosingingQuote(char ch, char openQuote, char closeQuote)
1119	{
1120		return ch == closeQuote || (openQuote == 0x301D && ch == 0x301E || ch == 0x301F);
1121	}
1122
1123
1124
1125	/**
1126	 * U+0022 ASCII space<br>
1127	 * U+3000, ideographic space<br>
1128	 * U+303F, ideographic half fill space<br>
1129	 * U+2000..U+200B, en quad through zero width space
1130	 */
1131	private static final String SPACES = "\u0020\u3000\u303F";
1132	/**
1133	 * U+002C, ASCII comma<br>
1134	 * U+FF0C, full width comma<br>
1135	 * U+FF64, half width ideographic comma<br>
1136	 * U+FE50, small comma<br>
1137	 * U+FE51, small ideographic comma<br>
1138	 * U+3001, ideographic comma<br>
1139	 * U+060C, Arabic comma<br>
1140	 * U+055D, Armenian comma
1141	 */
1142	private static final String COMMAS = "\u002C\uFF0C\uFF64\uFE50\uFE51\u3001\u060C\u055D";
1143	/**
1144	 * U+003B, ASCII semicolon<br>
1145	 * U+FF1B, full width semicolon<br>
1146	 * U+FE54, small semicolon<br>
1147	 * U+061B, Arabic semicolon<br>
1148	 * U+037E, Greek "semicolon" (really a question mark)
1149	 */
1150	private static final String SEMICOLA = "\u003B\uFF1B\uFE54\u061B\u037E";
1151	/**
1152	 * U+0022 ASCII quote<br>
1153	 * ASCII '[' (0x5B) and ']' (0x5D) are used as quotes in Chinese and
1154	 * Korean.<br>
1155	 * U+00AB and U+00BB, guillemet quotes<br>
1156	 * U+3008..U+300F, various quotes.<br>
1157	 * U+301D..U+301F, double prime quotes.<br>
1158	 * U+2015, dash quote.<br>
1159	 * U+2018..U+201F, various quotes.<br>
1160	 * U+2039 and U+203A, guillemet quotes.
1161	 */
1162	private static final String QUOTES =
1163		"\"\u005B\u005D\u00AB\u00BB\u301D\u301E\u301F\u2015\u2039\u203A";
1164	/**
1165	 * U+0000..U+001F ASCII controls<br>
1166	 * U+2028, line separator.<br>
1167	 * U+2029, paragraph separator.
1168	 */
1169	private static final String CONTROLS = "\u2028\u2029";
1170}
1171