• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
12 package com.adobe.xmp.impl;
13 
14 import java.util.Iterator;
15 
16 import com.adobe.xmp.XMPConst;
17 import com.adobe.xmp.XMPError;
18 import com.adobe.xmp.XMPException;
19 import com.adobe.xmp.XMPMeta;
20 import com.adobe.xmp.XMPMetaFactory;
21 import com.adobe.xmp.XMPUtils;
22 import com.adobe.xmp.impl.xpath.XMPPath;
23 import com.adobe.xmp.impl.xpath.XMPPathParser;
24 import com.adobe.xmp.options.PropertyOptions;
25 import com.adobe.xmp.properties.XMPAliasInfo;
26 
27 
28 
29 /**
30  * @since 11.08.2006
31  */
32 public 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 	 */
XMPUtilsImpl()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 	 */
catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, String separator, String quotes, boolean allowCommas)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 	 */
separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, String catedStr, PropertyOptions arrayOptions, boolean preserveCommas)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 	 */
separateFindCreateArray(String schemaNS, String arrayName, PropertyOptions arrayOptions, XMPMetaImpl xmp)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 	 */
removeProperties(XMPMeta xmp, String schemaNS, String propName, boolean doAllProperties, boolean includeAliases)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 	 */
appendProperties(XMPMeta source, XMPMeta destination, boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues)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 	 */
removeSchemaChildren(XMPNode schemaNode, boolean doAllProperties)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 	 */
appendSubtree(XMPMetaImpl destXMP, XMPNode sourceNode, XMPNode destParent, boolean replaceOldValues, boolean deleteEmptyValues)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 	 */
itemValuesMatch(XMPNode leftNode, XMPNode rightNode)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 	 */
checkSeparator(String separator)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 	 */
checkQuotes(String quotes, char openQuote)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 	 */
classifyCharacter(char ch)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 	 */
getClosingQuote(char openQuote)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 	 */
applyQuotes(String item, char openQuote, char closeQuote, boolean allowCommas)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 	 */
isSurroundingQuote(char ch, char openQuote, char closeQuote)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 	 */
isClosingingQuote(char ch, char openQuote, char closeQuote)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