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 package com.adobe.xmp.impl;
     11 
     12 import java.util.GregorianCalendar;
     13 import java.util.Iterator;
     14 
     15 import com.adobe.xmp.XMPConst;
     16 import com.adobe.xmp.XMPDateTime;
     17 import com.adobe.xmp.XMPDateTimeFactory;
     18 import com.adobe.xmp.XMPError;
     19 import com.adobe.xmp.XMPException;
     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.XMPPathSegment;
     24 import com.adobe.xmp.options.AliasOptions;
     25 import com.adobe.xmp.options.PropertyOptions;
     26 
     27 
     28 /**
     29  * Utilities for <code>XMPNode</code>.
     30  *
     31  * @since   Aug 28, 2006
     32  */
     33 public class XMPNodeUtils implements XMPConst
     34 {
     35 	/** */
     36 	static final int CLT_NO_VALUES = 0;
     37 	/** */
     38 	static final int CLT_SPECIFIC_MATCH = 1;
     39 	/** */
     40 	static final int CLT_SINGLE_GENERIC = 2;
     41 	/** */
     42 	static final int CLT_MULTIPLE_GENERIC = 3;
     43 	/** */
     44 	static final int CLT_XDEFAULT = 4;
     45 	/** */
     46 	static final int CLT_FIRST_ITEM = 5;
     47 
     48 
     49 	/**
     50 	 * Private Constructor
     51 	 */
     52 	private XMPNodeUtils()
     53 	{
     54 		// EMPTY
     55 	}
     56 
     57 
     58 	/**
     59 	 * Find or create a schema node if <code>createNodes</code> is false and
     60 	 *
     61 	 * @param tree the root of the xmp tree.
     62 	 * @param namespaceURI a namespace
     63 	 * @param createNodes a flag indicating if the node shall be created if not found.
     64 	 * 		  <em>Note:</em> The namespace must be registered prior to this call.
     65 	 *
     66 	 * @return Returns the schema node if found, <code>null</code> otherwise.
     67 	 * 		   Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b>
     68 	 * 		   returned a valid node.
     69 	 * @throws XMPException An exception is only thrown if an error occurred, not if a
     70 	 *         		node was not found.
     71 	 */
     72 	static XMPNode findSchemaNode(XMPNode tree, String namespaceURI,
     73 			boolean createNodes)
     74 			throws XMPException
     75 	{
     76 		return findSchemaNode(tree, namespaceURI, null, createNodes);
     77 	}
     78 
     79 
     80 	/**
     81 	 * Find or create a schema node if <code>createNodes</code> is true.
     82 	 *
     83 	 * @param tree the root of the xmp tree.
     84 	 * @param namespaceURI a namespace
     85 	 * @param suggestedPrefix If a prefix is suggested, the namespace is allowed to be registered.
     86 	 * @param createNodes a flag indicating if the node shall be created if not found.
     87 	 * 		  <em>Note:</em> The namespace must be registered prior to this call.
     88 	 *
     89 	 * @return Returns the schema node if found, <code>null</code> otherwise.
     90 	 * 		   Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b>
     91 	 * 		   returned a valid node.
     92 	 * @throws XMPException An exception is only thrown if an error occurred, not if a
     93 	 *         		node was not found.
     94 	 */
     95 	static XMPNode findSchemaNode(XMPNode tree, String namespaceURI, String suggestedPrefix,
     96 			boolean createNodes)
     97 			throws XMPException
     98 	{
     99 		assert tree.getParent() == null; // make sure that its the root
    100 		XMPNode schemaNode = tree.findChildByName(namespaceURI);
    101 
    102 		if (schemaNode == null  &&  createNodes)
    103 		{
    104 			schemaNode = new XMPNode(namespaceURI,
    105 				new PropertyOptions()
    106 					.setSchemaNode(true));
    107 			schemaNode.setImplicit(true);
    108 
    109 			// only previously registered schema namespaces are allowed in the XMP tree.
    110 			String prefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(namespaceURI);
    111 			if (prefix == null)
    112 			{
    113 				if (suggestedPrefix != null  &&  suggestedPrefix.length() != 0)
    114 				{
    115 					prefix = XMPMetaFactory.getSchemaRegistry().registerNamespace(namespaceURI,
    116 							suggestedPrefix);
    117 				}
    118 				else
    119 				{
    120 					throw new XMPException("Unregistered schema namespace URI",
    121 							XMPError.BADSCHEMA);
    122 				}
    123 			}
    124 
    125 			schemaNode.setValue(prefix);
    126 
    127 			tree.addChild(schemaNode);
    128 		}
    129 
    130 		return schemaNode;
    131 	}
    132 
    133 
    134 	/**
    135 	 * Find or create a child node under a given parent node. If the parent node is no
    136 	 * Returns the found or created child node.
    137 	 *
    138 	 * @param parent
    139 	 *            the parent node
    140 	 * @param childName
    141 	 *            the node name to find
    142 	 * @param createNodes
    143 	 *            flag, if new nodes shall be created.
    144 	 * @return Returns the found or created node or <code>null</code>.
    145 	 * @throws XMPException Thrown if
    146 	 */
    147 	static XMPNode findChildNode(XMPNode parent, String childName, boolean createNodes)
    148 			throws XMPException
    149 	{
    150 		if (!parent.getOptions().isSchemaNode() && !parent.getOptions().isStruct())
    151 		{
    152 			if (!parent.isImplicit())
    153 			{
    154 				throw new XMPException("Named children only allowed for schemas and structs",
    155 						XMPError.BADXPATH);
    156 			}
    157 			else if (parent.getOptions().isArray())
    158 			{
    159 				throw new XMPException("Named children not allowed for arrays",
    160 						XMPError.BADXPATH);
    161 			}
    162 			else if (createNodes)
    163 			{
    164 				parent.getOptions().setStruct(true);
    165 			}
    166 		}
    167 
    168 		XMPNode childNode = parent.findChildByName(childName);
    169 
    170 		if (childNode == null  &&  createNodes)
    171 		{
    172 			PropertyOptions options = new PropertyOptions();
    173 			childNode = new XMPNode(childName, options);
    174 			childNode.setImplicit(true);
    175 			parent.addChild(childNode);
    176 		}
    177 
    178 		assert childNode != null ||  !createNodes;
    179 
    180 		return childNode;
    181 	}
    182 
    183 
    184 	/**
    185 	 * Follow an expanded path expression to find or create a node.
    186 	 *
    187 	 * @param xmpTree the node to begin the search.
    188 	 * @param xpath the complete xpath
    189 	 * @param createNodes flag if nodes shall be created
    190 	 * 			(when called by <code>setProperty()</code>)
    191 	 * @param leafOptions the options for the created leaf nodes (only when
    192 	 *			<code>createNodes == true</code>).
    193 	 * @return Returns the node if found or created or <code>null</code>.
    194 	 * @throws XMPException An exception is only thrown if an error occurred,
    195 	 * 			not if a node was not found.
    196 	 */
    197 	static XMPNode findNode(XMPNode xmpTree, XMPPath xpath, boolean createNodes,
    198 		PropertyOptions leafOptions) throws XMPException
    199 	{
    200 		// check if xpath is set.
    201 		if (xpath == null  ||  xpath.size() == 0)
    202 		{
    203 			throw new XMPException("Empty XMPPath", XMPError.BADXPATH);
    204 		}
    205 
    206 		// Root of implicitly created subtree to possible delete it later.
    207 		// Valid only if leaf is new.
    208 		XMPNode rootImplicitNode = null;
    209 		XMPNode currNode = null;
    210 
    211 		// resolve schema step
    212 		currNode = findSchemaNode(xmpTree,
    213 			xpath.getSegment(XMPPath.STEP_SCHEMA).getName(), createNodes);
    214 		if (currNode == null)
    215 		{
    216 			return null;
    217 		}
    218 		else if (currNode.isImplicit())
    219 		{
    220 			currNode.setImplicit(false);	// Clear the implicit node bit.
    221 			rootImplicitNode = currNode;	// Save the top most implicit node.
    222 		}
    223 
    224 
    225 		// Now follow the remaining steps of the original XMPPath.
    226 		try
    227 		{
    228 			for (int i = 1; i < xpath.size(); i++)
    229 			{
    230 				currNode = followXPathStep(currNode, xpath.getSegment(i), createNodes);
    231 				if (currNode == null)
    232 				{
    233 					if (createNodes)
    234 					{
    235 						// delete implicitly created nodes
    236 						deleteNode(rootImplicitNode);
    237 					}
    238 					return null;
    239 				}
    240 				else if (currNode.isImplicit())
    241 				{
    242 					// clear the implicit node flag
    243 					currNode.setImplicit(false);
    244 
    245 					// if node is an ALIAS (can be only in root step, auto-create array
    246 					// when the path has been resolved from a not simple alias type
    247 					if (i == 1  &&
    248 						xpath.getSegment(i).isAlias()  &&
    249 						xpath.getSegment(i).getAliasForm() != 0)
    250 					{
    251 						currNode.getOptions().setOption(xpath.getSegment(i).getAliasForm(), true);
    252 					}
    253 					// "CheckImplicitStruct" in C++
    254 					else if (i < xpath.size() - 1  &&
    255 						xpath.getSegment(i).getKind() == XMPPath.STRUCT_FIELD_STEP  &&
    256 						!currNode.getOptions().isCompositeProperty())
    257 					{
    258 						currNode.getOptions().setStruct(true);
    259 					}
    260 
    261 					if (rootImplicitNode == null)
    262 					{
    263 						rootImplicitNode = currNode;	// Save the top most implicit node.
    264 					}
    265 				}
    266 			}
    267 		}
    268 		catch (XMPException e)
    269 		{
    270 			// if new notes have been created prior to the error, delete them
    271 			if (rootImplicitNode != null)
    272 			{
    273 				deleteNode(rootImplicitNode);
    274 			}
    275 			throw e;
    276 		}
    277 
    278 
    279 		if (rootImplicitNode != null)
    280 		{
    281 			// set options only if a node has been successful created
    282 			currNode.getOptions().mergeWith(leafOptions);
    283 			currNode.setOptions(currNode.getOptions());
    284 		}
    285 
    286 		return currNode;
    287 	}
    288 
    289 
    290 	/**
    291 	 * Deletes the the given node and its children from its parent.
    292 	 * Takes care about adjusting the flags.
    293 	 * @param node the top-most node to delete.
    294 	 */
    295 	static void deleteNode(XMPNode node)
    296 	{
    297 		XMPNode parent = node.getParent();
    298 
    299 		if (node.getOptions().isQualifier())
    300 		{
    301 			// root is qualifier
    302 			parent.removeQualifier(node);
    303 		}
    304 		else
    305 		{
    306 			// root is NO qualifier
    307 			parent.removeChild(node);
    308 		}
    309 
    310 		// delete empty Schema nodes
    311 		if (!parent.hasChildren()  &&  parent.getOptions().isSchemaNode())
    312 		{
    313 			parent.getParent().removeChild(parent);
    314 		}
    315 	}
    316 
    317 
    318 	/**
    319 	 * This is setting the value of a leaf node.
    320 	 *
    321 	 * @param node an XMPNode
    322 	 * @param value a value
    323 	 */
    324 	static void setNodeValue(XMPNode node, Object value)
    325 	{
    326 		String strValue = serializeNodeValue(value);
    327 		if (!(node.getOptions().isQualifier()  &&  XML_LANG.equals(node.getName())))
    328 		{
    329 			node.setValue(strValue);
    330 		}
    331 		else
    332 		{
    333 			node.setValue(Utils.normalizeLangValue(strValue));
    334 		}
    335 	}
    336 
    337 
    338 	/**
    339 	 * Verifies the PropertyOptions for consistancy and updates them as needed.
    340 	 * If options are <code>null</code> they are created with default values.
    341 	 *
    342 	 * @param options the <code>PropertyOptions</code>
    343 	 * @param itemValue the node value to set
    344 	 * @return Returns the updated options.
    345 	 * @throws XMPException If the options are not consistant.
    346 	 */
    347 	static PropertyOptions verifySetOptions(PropertyOptions options, Object itemValue)
    348 			throws XMPException
    349 	{
    350 		// create empty and fix existing options
    351 		if (options == null)
    352 		{
    353 			// set default options
    354 			options = new PropertyOptions();
    355 		}
    356 
    357 		if (options.isArrayAltText())
    358 		{
    359 			options.setArrayAlternate(true);
    360 		}
    361 
    362 		if (options.isArrayAlternate())
    363 		{
    364 			options.setArrayOrdered(true);
    365 		}
    366 
    367 		if (options.isArrayOrdered())
    368 		{
    369 			options.setArray(true);
    370 		}
    371 
    372 		if (options.isCompositeProperty() && itemValue != null && itemValue.toString().length() > 0)
    373 		{
    374 			throw new XMPException("Structs and arrays can't have values",
    375 				XMPError.BADOPTIONS);
    376 		}
    377 
    378 		options.assertConsistency(options.getOptions());
    379 
    380 		return options;
    381 	}
    382 
    383 
    384 	/**
    385 	 * Converts the node value to String, apply special conversions for defined
    386 	 * types in XMP.
    387 	 *
    388 	 * @param value
    389 	 *            the node value to set
    390 	 * @return Returns the String representation of the node value.
    391 	 */
    392 	static String serializeNodeValue(Object value)
    393 	{
    394 		String strValue;
    395 		if (value == null)
    396 		{
    397 			strValue = null;
    398 		}
    399 		else if (value instanceof Boolean)
    400 		{
    401 			strValue = XMPUtils.convertFromBoolean(((Boolean) value).booleanValue());
    402 		}
    403 		else if (value instanceof Integer)
    404 		{
    405 			strValue = XMPUtils.convertFromInteger(((Integer) value).intValue());
    406 		}
    407 		else if (value instanceof Long)
    408 		{
    409 			strValue = XMPUtils.convertFromLong(((Long) value).longValue());
    410 		}
    411 		else if (value instanceof Double)
    412 		{
    413 			strValue = XMPUtils.convertFromDouble(((Double) value).doubleValue());
    414 		}
    415 		else if (value instanceof XMPDateTime)
    416 		{
    417 			strValue = XMPUtils.convertFromDate((XMPDateTime) value);
    418 		}
    419 		else if (value instanceof GregorianCalendar)
    420 		{
    421 			XMPDateTime dt = XMPDateTimeFactory.createFromCalendar((GregorianCalendar) value);
    422 			strValue = XMPUtils.convertFromDate(dt);
    423 		}
    424 		else if (value instanceof byte[])
    425 		{
    426 			strValue = XMPUtils.encodeBase64((byte[]) value);
    427 		}
    428 		else
    429 		{
    430 			strValue = value.toString();
    431 		}
    432 
    433 		return strValue != null ? Utils.removeControlChars(strValue) : null;
    434 	}
    435 
    436 
    437 	/**
    438 	 * After processing by ExpandXPath, a step can be of these forms:
    439 	 * <ul>
    440 	 * 	<li>qualName - A top level property or struct field.
    441 	 * <li>[index] - An element of an array.
    442 	 * <li>[last()] - The last element of an array.
    443 	 * <li>[qualName="value"] - An element in an array of structs, chosen by a field value.
    444 	 * <li>[?qualName="value"] - An element in an array, chosen by a qualifier value.
    445 	 * <li>?qualName - A general qualifier.
    446 	 * </ul>
    447 	 * Find the appropriate child node, resolving aliases, and optionally creating nodes.
    448 	 *
    449 	 * @param parentNode the node to start to start from
    450 	 * @param nextStep the xpath segment
    451 	 * @param createNodes
    452 	 * @return returns the found or created XMPPath node
    453 	 * @throws XMPException
    454 	 */
    455 	private static XMPNode followXPathStep(
    456 				XMPNode parentNode,
    457 				XMPPathSegment nextStep,
    458 				boolean createNodes) throws XMPException
    459 	{
    460 		XMPNode nextNode = null;
    461 		int index = 0;
    462 		int stepKind = nextStep.getKind();
    463 
    464 		if (stepKind == XMPPath.STRUCT_FIELD_STEP)
    465 		{
    466 			nextNode = findChildNode(parentNode, nextStep.getName(), createNodes);
    467 		}
    468 		else if (stepKind == XMPPath.QUALIFIER_STEP)
    469 		{
    470 			nextNode = findQualifierNode(
    471 				parentNode, nextStep.getName().substring(1), createNodes);
    472 		}
    473 		else
    474 		{
    475 			// This is an array indexing step. First get the index, then get the node.
    476 
    477 			if (!parentNode.getOptions().isArray())
    478 			{
    479 				throw new XMPException("Indexing applied to non-array", XMPError.BADXPATH);
    480 			}
    481 
    482 			if (stepKind == XMPPath.ARRAY_INDEX_STEP)
    483 			{
    484 				index = findIndexedItem(parentNode, nextStep.getName(), createNodes);
    485 			}
    486 			else if (stepKind == XMPPath.ARRAY_LAST_STEP)
    487 			{
    488 				index = parentNode.getChildrenLength();
    489 			}
    490 			else if (stepKind == XMPPath.FIELD_SELECTOR_STEP)
    491 			{
    492 				String[] result = Utils.splitNameAndValue(nextStep.getName());
    493 				String fieldName = result[0];
    494 				String fieldValue = result[1];
    495 				index = lookupFieldSelector(parentNode, fieldName, fieldValue);
    496 			}
    497 			else if (stepKind == XMPPath.QUAL_SELECTOR_STEP)
    498 			{
    499 				String[] result = Utils.splitNameAndValue(nextStep.getName());
    500 				String qualName = result[0];
    501 				String qualValue = result[1];
    502 				index = lookupQualSelector(
    503 					parentNode, qualName, qualValue, nextStep.getAliasForm());
    504 			}
    505 			else
    506 			{
    507 				throw new XMPException("Unknown array indexing step in FollowXPathStep",
    508 						XMPError.INTERNALFAILURE);
    509 			}
    510 
    511 			if (1 <= index  &&  index <=  parentNode.getChildrenLength())
    512 			{
    513 				nextNode = parentNode.getChild(index);
    514 			}
    515 		}
    516 
    517 		return nextNode;
    518 	}
    519 
    520 
    521 	/**
    522 	 * Find or create a qualifier node under a given parent node. Returns a pointer to the
    523 	 * qualifier node, and optionally an iterator for the node's position in
    524 	 * the parent's vector of qualifiers. The iterator is unchanged if no qualifier node (null)
    525 	 * is returned.
    526 	 * <em>Note:</em> On entry, the qualName parameter must not have the leading '?' from the
    527 	 * XMPPath step.
    528 	 *
    529 	 * @param parent the parent XMPNode
    530 	 * @param qualName the qualifier name
    531 	 * @param createNodes flag if nodes shall be created
    532 	 * @return Returns the qualifier node if found or created, <code>null</code> otherwise.
    533 	 * @throws XMPException
    534 	 */
    535 	private static XMPNode findQualifierNode(XMPNode parent, String qualName, boolean createNodes)
    536 			throws XMPException
    537 	{
    538 		assert !qualName.startsWith("?");
    539 
    540 		XMPNode qualNode = parent.findQualifierByName(qualName);
    541 
    542 		if (qualNode == null  &&  createNodes)
    543 		{
    544 			qualNode = new XMPNode(qualName, null);
    545 			qualNode.setImplicit(true);
    546 
    547 			parent.addQualifier(qualNode);
    548 		}
    549 
    550 		return qualNode;
    551 	}
    552 
    553 
    554 	/**
    555 	 * @param arrayNode an array node
    556 	 * @param segment the segment containing the array index
    557 	 * @param createNodes flag if new nodes are allowed to be created.
    558 	 * @return Returns the index or index = -1 if not found
    559 	 * @throws XMPException Throws Exceptions
    560 	 */
    561 	private static int findIndexedItem(XMPNode arrayNode, String segment, boolean createNodes)
    562 			throws XMPException
    563 	{
    564 		int index = 0;
    565 
    566 		try
    567 		{
    568 			segment = segment.substring(1, segment.length() - 1);
    569 			index = Integer.parseInt(segment);
    570 			if (index < 1)
    571 			{
    572 				throw new XMPException("Array index must be larger than zero",
    573 						XMPError.BADXPATH);
    574 			}
    575 		}
    576 		catch (NumberFormatException e)
    577 		{
    578 			throw new XMPException("Array index not digits.", XMPError.BADXPATH);
    579 		}
    580 
    581 		if (createNodes  &&  index == arrayNode.getChildrenLength() + 1)
    582 		{
    583 			// Append a new last + 1 node.
    584 			XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, null);
    585 			newItem.setImplicit(true);
    586 			arrayNode.addChild(newItem);
    587 		}
    588 
    589 		return index;
    590 	}
    591 
    592 
    593 	/**
    594 	 * Searches for a field selector in a node:
    595 	 * [fieldName="value] - an element in an array of structs, chosen by a field value.
    596 	 * No implicit nodes are created by field selectors.
    597 	 *
    598 	 * @param arrayNode
    599 	 * @param fieldName
    600 	 * @param fieldValue
    601 	 * @return Returns the index of the field if found, otherwise -1.
    602 	 * @throws XMPException
    603 	 */
    604 	private static int lookupFieldSelector(XMPNode arrayNode, String fieldName, String fieldValue)
    605 		throws XMPException
    606 	{
    607 		int result = -1;
    608 
    609 		for (int index = 1; index <= arrayNode.getChildrenLength()  &&  result < 0; index++)
    610 		{
    611 			XMPNode currItem = arrayNode.getChild(index);
    612 
    613 			if (!currItem.getOptions().isStruct())
    614 			{
    615 				throw new XMPException("Field selector must be used on array of struct",
    616 						XMPError.BADXPATH);
    617 			}
    618 
    619 			for (int f = 1; f <= currItem.getChildrenLength(); f++)
    620 			{
    621 				XMPNode currField = currItem.getChild(f);
    622 				if (!fieldName.equals(currField.getName()))
    623 				{
    624 					continue;
    625 				}
    626 				if (fieldValue.equals(currField.getValue()))
    627 				{
    628 					result = index;
    629 					break;
    630 				}
    631 			}
    632 		}
    633 
    634 		return result;
    635 	}
    636 
    637 
    638 	/**
    639 	 * Searches for a qualifier selector in a node:
    640 	 * [?qualName="value"] - an element in an array, chosen by a qualifier value.
    641 	 * No implicit nodes are created for qualifier selectors,
    642 	 * except for an alias to an x-default item.
    643 	 *
    644 	 * @param arrayNode an array node
    645 	 * @param qualName the qualifier name
    646 	 * @param qualValue the qualifier value
    647 	 * @param aliasForm in case the qual selector results from an alias,
    648 	 * 		  an x-default node is created if there has not been one.
    649 	 * @return Returns the index of th
    650 	 * @throws XMPException
    651 	 */
    652 	private static int lookupQualSelector(XMPNode arrayNode, String qualName,
    653 		String qualValue, int aliasForm) throws XMPException
    654 	{
    655 		if (XML_LANG.equals(qualName))
    656 		{
    657 			qualValue = Utils.normalizeLangValue(qualValue);
    658 			int index = XMPNodeUtils.lookupLanguageItem(arrayNode, qualValue);
    659 			if (index < 0  &&  (aliasForm & AliasOptions.PROP_ARRAY_ALT_TEXT) > 0)
    660 			{
    661 				XMPNode langNode = new XMPNode(ARRAY_ITEM_NAME, null);
    662 				XMPNode xdefault = new XMPNode(XML_LANG, X_DEFAULT, null);
    663 				langNode.addQualifier(xdefault);
    664 				arrayNode.addChild(1, langNode);
    665 				return 1;
    666 			}
    667 			else
    668 			{
    669 				return index;
    670 			}
    671 		}
    672 		else
    673 		{
    674 			for (int index = 1; index < arrayNode.getChildrenLength(); index++)
    675 			{
    676 				XMPNode currItem = arrayNode.getChild(index);
    677 
    678 				for (Iterator it = currItem.iterateQualifier(); it.hasNext();)
    679 				{
    680 					XMPNode qualifier = (XMPNode) it.next();
    681 					if (qualName.equals(qualifier.getName())  &&
    682 						qualValue.equals(qualifier.getValue()))
    683 					{
    684 						return index;
    685 					}
    686 				}
    687 			}
    688 			return -1;
    689 		}
    690 	}
    691 
    692 
    693 	/**
    694 	 * Make sure the x-default item is first. Touch up &quot;single value&quot;
    695 	 * arrays that have a default plus one real language. This case should have
    696 	 * the same value for both items. Older Adobe apps were hardwired to only
    697 	 * use the &quot;x-default&quot; item, so we copy that value to the other
    698 	 * item.
    699 	 *
    700 	 * @param arrayNode
    701 	 *            an alt text array node
    702 	 */
    703 	static void normalizeLangArray(XMPNode arrayNode)
    704 	{
    705 		if (!arrayNode.getOptions().isArrayAltText())
    706 		{
    707 			return;
    708 		}
    709 
    710 		// check if node with x-default qual is first place
    711 		for (int i = 2; i <= arrayNode.getChildrenLength(); i++)
    712 		{
    713 			XMPNode child = arrayNode.getChild(i);
    714 			if (child.hasQualifier() && X_DEFAULT.equals(child.getQualifier(1).getValue()))
    715 			{
    716 				// move node to first place
    717 				try
    718 				{
    719 					arrayNode.removeChild(i);
    720 					arrayNode.addChild(1, child);
    721 				}
    722 				catch (XMPException e)
    723 				{
    724 					// cannot occur, because same child is removed before
    725 					assert false;
    726 				}
    727 
    728 				if (i == 2)
    729 				{
    730 					arrayNode.getChild(2).setValue(child.getValue());
    731 				}
    732 				break;
    733 			}
    734 		}
    735 	}
    736 
    737 
    738 	/**
    739 	 * See if an array is an alt-text array. If so, make sure the x-default item
    740 	 * is first.
    741 	 *
    742 	 * @param arrayNode
    743 	 *            the array node to check if its an alt-text array
    744 	 */
    745 	static void detectAltText(XMPNode arrayNode)
    746 	{
    747 		if (arrayNode.getOptions().isArrayAlternate() && arrayNode.hasChildren())
    748 		{
    749 			boolean isAltText = false;
    750 			for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
    751 			{
    752 				XMPNode child = (XMPNode) it.next();
    753 				if (child.getOptions().getHasLanguage())
    754 				{
    755 					isAltText = true;
    756 					break;
    757 				}
    758 			}
    759 
    760 			if (isAltText)
    761 			{
    762 				arrayNode.getOptions().setArrayAltText(true);
    763 				normalizeLangArray(arrayNode);
    764 			}
    765 		}
    766 	}
    767 
    768 
    769 	/**
    770 	 * Appends a language item to an alt text array.
    771 	 *
    772 	 * @param arrayNode the language array
    773 	 * @param itemLang the language of the item
    774 	 * @param itemValue the content of the item
    775 	 * @throws XMPException Thrown if a duplicate property is added
    776 	 */
    777 	static void appendLangItem(XMPNode arrayNode, String itemLang, String itemValue)
    778 			throws XMPException
    779 	{
    780 		XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null);
    781 		XMPNode langQual = new XMPNode(XML_LANG, itemLang, null);
    782 		newItem.addQualifier(langQual);
    783 
    784 		if (!X_DEFAULT.equals(langQual.getValue()))
    785 		{
    786 			arrayNode.addChild(newItem);
    787 		}
    788 		else
    789 		{
    790 			arrayNode.addChild(1, newItem);
    791 		}
    792 	}
    793 
    794 
    795 	/**
    796 	 * <ol>
    797 	 * <li>Look for an exact match with the specific language.
    798 	 * <li>If a generic language is given, look for partial matches.
    799 	 * <li>Look for an "x-default"-item.
    800 	 * <li>Choose the first item.
    801 	 * </ol>
    802 	 *
    803 	 * @param arrayNode
    804 	 *            the alt text array node
    805 	 * @param genericLang
    806 	 *            the generic language
    807 	 * @param specificLang
    808 	 *            the specific language
    809 	 * @return Returns the kind of match as an Integer and the found node in an
    810 	 *         array.
    811 	 *
    812 	 * @throws XMPException
    813 	 */
    814 	static Object[] chooseLocalizedText(XMPNode arrayNode, String genericLang, String specificLang)
    815 			throws XMPException
    816 	{
    817 		// See if the array has the right form. Allow empty alt arrays,
    818 		// that is what parsing returns.
    819 		if (!arrayNode.getOptions().isArrayAltText())
    820 		{
    821 			throw new XMPException("Localized text array is not alt-text", XMPError.BADXPATH);
    822 		}
    823 		else if (!arrayNode.hasChildren())
    824 		{
    825 			return new Object[] { new Integer(XMPNodeUtils.CLT_NO_VALUES), null };
    826 		}
    827 
    828 		int foundGenericMatches = 0;
    829 		XMPNode resultNode = null;
    830 		XMPNode xDefault = null;
    831 
    832 		// Look for the first partial match with the generic language.
    833 		for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
    834 		{
    835 			XMPNode currItem = (XMPNode) it.next();
    836 
    837 			// perform some checks on the current item
    838 			if (currItem.getOptions().isCompositeProperty())
    839 			{
    840 				throw new XMPException("Alt-text array item is not simple", XMPError.BADXPATH);
    841 			}
    842 			else if (!currItem.hasQualifier()
    843 					|| !XML_LANG.equals(currItem.getQualifier(1).getName()))
    844 			{
    845 				throw new XMPException("Alt-text array item has no language qualifier",
    846 						XMPError.BADXPATH);
    847 			}
    848 
    849 			String currLang = currItem.getQualifier(1).getValue();
    850 
    851 			// Look for an exact match with the specific language.
    852 			if (specificLang.equals(currLang))
    853 			{
    854 				return new Object[] { new Integer(XMPNodeUtils.CLT_SPECIFIC_MATCH), currItem };
    855 			}
    856 			else if (genericLang != null && currLang.startsWith(genericLang))
    857 			{
    858 				if (resultNode == null)
    859 				{
    860 					resultNode = currItem;
    861 				}
    862 				// ! Don't return/break, need to look for other matches.
    863 				foundGenericMatches++;
    864 			}
    865 			else if (X_DEFAULT.equals(currLang))
    866 			{
    867 				xDefault = currItem;
    868 			}
    869 		}
    870 
    871 		// evaluate loop
    872 		if (foundGenericMatches == 1)
    873 		{
    874 			return new Object[] { new Integer(XMPNodeUtils.CLT_SINGLE_GENERIC), resultNode };
    875 		}
    876 		else if (foundGenericMatches > 1)
    877 		{
    878 			return new Object[] { new Integer(XMPNodeUtils.CLT_MULTIPLE_GENERIC), resultNode };
    879 		}
    880 		else if (xDefault != null)
    881 		{
    882 			return new Object[] { new Integer(XMPNodeUtils.CLT_XDEFAULT), xDefault };
    883 		}
    884 		else
    885 		{
    886 			// Everything failed, choose the first item.
    887 			return new Object[] { new Integer(XMPNodeUtils.CLT_FIRST_ITEM), arrayNode.getChild(1) };
    888 		}
    889 	}
    890 
    891 
    892 	/**
    893 	 * Looks for the appropriate language item in a text alternative array.item
    894 	 *
    895 	 * @param arrayNode
    896 	 *            an array node
    897 	 * @param language
    898 	 *            the requested language
    899 	 * @return Returns the index if the language has been found, -1 otherwise.
    900 	 * @throws XMPException
    901 	 */
    902 	static int lookupLanguageItem(XMPNode arrayNode, String language) throws XMPException
    903 	{
    904 		if (!arrayNode.getOptions().isArray())
    905 		{
    906 			throw new XMPException("Language item must be used on array", XMPError.BADXPATH);
    907 		}
    908 
    909 		for (int index = 1; index <= arrayNode.getChildrenLength(); index++)
    910 		{
    911 			XMPNode child = arrayNode.getChild(index);
    912 			if (!child.hasQualifier() || !XML_LANG.equals(child.getQualifier(1).getName()))
    913 			{
    914 				continue;
    915 			}
    916 			else if (language.equals(child.getQualifier(1).getValue()))
    917 			{
    918 				return index;
    919 			}
    920 		}
    921 
    922 		return -1;
    923 	}
    924 }
    925