Home | History | Annotate | Download | only in impl
      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 	 */
     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