1 // =================================================================================================
2 // ADOBE SYSTEMS INCORPORATED
3 // Copyright 2006-2007 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 	 */
XMPNodeUtils()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 	 */
findSchemaNode(XMPNode tree, String namespaceURI, boolean createNodes)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 	 */
findSchemaNode(XMPNode tree, String namespaceURI, String suggestedPrefix, boolean createNodes)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 	 */
findChildNode(XMPNode parent, String childName, boolean createNodes)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 	 */
findNode(XMPNode xmpTree, XMPPath xpath, boolean createNodes, PropertyOptions leafOptions)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 	 */
deleteNode(XMPNode node)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 	 */
setNodeValue(XMPNode node, Object value)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 	 */
verifySetOptions(PropertyOptions options, Object itemValue)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 	 */
serializeNodeValue(Object value)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 	 */
followXPathStep( XMPNode parentNode, XMPPathSegment nextStep, boolean createNodes)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 	 */
findQualifierNode(XMPNode parent, String qualName, boolean createNodes)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 	 */
findIndexedItem(XMPNode arrayNode, String segment, boolean createNodes)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 	 */
lookupFieldSelector(XMPNode arrayNode, String fieldName, String fieldValue)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 	 */
lookupQualSelector(XMPNode arrayNode, String qualName, String qualValue, int aliasForm)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 	 */
normalizeLangArray(XMPNode arrayNode)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 	 */
detectAltText(XMPNode arrayNode)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 	 */
appendLangItem(XMPNode arrayNode, String itemLang, String itemValue)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 	 */
chooseLocalizedText(XMPNode arrayNode, String genericLang, String specificLang)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 	 */
lookupLanguageItem(XMPNode arrayNode, String language)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