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
10package com.adobe.xmp.impl;
11
12import java.util.GregorianCalendar;
13import java.util.Iterator;
14
15import com.adobe.xmp.XMPConst;
16import com.adobe.xmp.XMPDateTime;
17import com.adobe.xmp.XMPDateTimeFactory;
18import com.adobe.xmp.XMPError;
19import com.adobe.xmp.XMPException;
20import com.adobe.xmp.XMPMetaFactory;
21import com.adobe.xmp.XMPUtils;
22import com.adobe.xmp.impl.xpath.XMPPath;
23import com.adobe.xmp.impl.xpath.XMPPathSegment;
24import com.adobe.xmp.options.AliasOptions;
25import com.adobe.xmp.options.PropertyOptions;
26
27
28/**
29 * Utilities for <code>XMPNode</code>.
30 *
31 * @since   Aug 28, 2006
32 */
33public 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