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.Calendar;
13 import java.util.Iterator;
14 
15 import com.adobe.xmp.XMPConst;
16 import com.adobe.xmp.XMPDateTime;
17 import com.adobe.xmp.XMPError;
18 import com.adobe.xmp.XMPException;
19 import com.adobe.xmp.XMPIterator;
20 import com.adobe.xmp.XMPMeta;
21 import com.adobe.xmp.XMPPathFactory;
22 import com.adobe.xmp.XMPUtils;
23 import com.adobe.xmp.impl.xpath.XMPPath;
24 import com.adobe.xmp.impl.xpath.XMPPathParser;
25 import com.adobe.xmp.options.IteratorOptions;
26 import com.adobe.xmp.options.PropertyOptions;
27 import com.adobe.xmp.properties.XMPProperty;
28 
29 
30 /**
31  * Implementation for {@link XMPMeta}.
32  *
33  * @since 17.02.2006
34  */
35 public class XMPMetaImpl implements XMPMeta, XMPConst
36 {
37 	/** Property values are Strings by default */
38 	private static final int VALUE_STRING = 0;
39 	/** */
40 	private static final int VALUE_BOOLEAN = 1;
41 	/** */
42 	private static final int VALUE_INTEGER = 2;
43 	/** */
44 	private static final int VALUE_LONG = 3;
45 	/** */
46 	private static final int VALUE_DOUBLE = 4;
47 	/** */
48 	private static final int VALUE_DATE = 5;
49 	/** */
50 	private static final int VALUE_CALENDAR = 6;
51 	/** */
52 	private static final int VALUE_BASE64 = 7;
53 
54 	/** root of the metadata tree */
55 	private XMPNode tree;
56 	/** the xpacket processing instructions content */
57 	private String packetHeader = null;
58 
59 
60 	/**
61 	 * Constructor for an empty metadata object.
62 	 */
XMPMetaImpl()63 	public XMPMetaImpl()
64 	{
65 		// create root node
66 		tree = new XMPNode(null, null, null);
67 	}
68 
69 
70 	/**
71 	 * Constructor for a cloned metadata tree.
72 	 *
73 	 * @param tree
74 	 *            an prefilled metadata tree which fulfills all
75 	 *            <code>XMPNode</code> contracts.
76 	 */
XMPMetaImpl(XMPNode tree)77 	public XMPMetaImpl(XMPNode tree)
78 	{
79 		this.tree = tree;
80 	}
81 
82 
83 	/**
84 	 * @see XMPMeta#appendArrayItem(String, String, PropertyOptions, String,
85 	 *      PropertyOptions)
86 	 */
appendArrayItem(String schemaNS, String arrayName, PropertyOptions arrayOptions, String itemValue, PropertyOptions itemOptions)87 	public void appendArrayItem(String schemaNS, String arrayName, PropertyOptions arrayOptions,
88 			String itemValue, PropertyOptions itemOptions) throws XMPException
89 	{
90 		ParameterAsserts.assertSchemaNS(schemaNS);
91 		ParameterAsserts.assertArrayName(arrayName);
92 
93 		if (arrayOptions == null)
94 		{
95 			arrayOptions = new PropertyOptions();
96 		}
97 		if (!arrayOptions.isOnlyArrayOptions())
98 		{
99 			throw new XMPException("Only array form flags allowed for arrayOptions",
100 					XMPError.BADOPTIONS);
101 		}
102 
103 		// Check if array options are set correctly.
104 		arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null);
105 
106 
107 		// Locate or create the array. If it already exists, make sure the array
108 		// form from the options
109 		// parameter is compatible with the current state.
110 		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
111 
112 
113 		// Just lookup, don't try to create.
114 		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);
115 
116 		if (arrayNode != null)
117 		{
118 			// The array exists, make sure the form is compatible. Zero
119 			// arrayForm means take what exists.
120 			if (!arrayNode.getOptions().isArray())
121 			{
122 				throw new XMPException("The named property is not an array", XMPError.BADXPATH);
123 			}
124 			// if (arrayOptions != null && !arrayOptions.equalArrayTypes(arrayNode.getOptions()))
125 			// {
126 			// throw new XMPException("Mismatch of existing and specified array form", BADOPTIONS);
127 			// }
128 		}
129 		else
130 		{
131 			// The array does not exist, try to create it.
132 			if (arrayOptions.isArray())
133 			{
134 				arrayNode = XMPNodeUtils.findNode(tree, arrayPath, true, arrayOptions);
135 				if (arrayNode == null)
136 				{
137 					throw new XMPException("Failure creating array node", XMPError.BADXPATH);
138 				}
139 			}
140 			else
141 			{
142 				// array options missing
143 				throw new XMPException("Explicit arrayOptions required to create new array",
144 						XMPError.BADOPTIONS);
145 			}
146 		}
147 
148 		doSetArrayItem(arrayNode, ARRAY_LAST_ITEM, itemValue, itemOptions, true);
149 	}
150 
151 
152 	/**
153 	 * @see XMPMeta#appendArrayItem(String, String, String)
154 	 */
appendArrayItem(String schemaNS, String arrayName, String itemValue)155 	public void appendArrayItem(String schemaNS, String arrayName, String itemValue)
156 			throws XMPException
157 	{
158 		appendArrayItem(schemaNS, arrayName, null, itemValue, null);
159 	}
160 
161 
162 	/**
163 	 * @throws XMPException
164 	 * @see XMPMeta#countArrayItems(String, String)
165 	 */
countArrayItems(String schemaNS, String arrayName)166 	public int countArrayItems(String schemaNS, String arrayName) throws XMPException
167 	{
168 		ParameterAsserts.assertSchemaNS(schemaNS);
169 		ParameterAsserts.assertArrayName(arrayName);
170 
171 		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
172 		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);
173 
174 		if (arrayNode == null)
175 		{
176 			return 0;
177 		}
178 
179 		if (arrayNode.getOptions().isArray())
180 		{
181 			return arrayNode.getChildrenLength();
182 		}
183 		else
184 		{
185 			throw new XMPException("The named property is not an array", XMPError.BADXPATH);
186 		}
187 	}
188 
189 
190 	/**
191 	 * @see XMPMeta#deleteArrayItem(String, String, int)
192 	 */
deleteArrayItem(String schemaNS, String arrayName, int itemIndex)193 	public void deleteArrayItem(String schemaNS, String arrayName, int itemIndex)
194 	{
195 		try
196 		{
197 			ParameterAsserts.assertSchemaNS(schemaNS);
198 			ParameterAsserts.assertArrayName(arrayName);
199 
200 			String itemPath = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex);
201 			deleteProperty(schemaNS, itemPath);
202 		}
203 		catch (XMPException e)
204 		{
205 			// EMPTY, exceptions are ignored within delete
206 		}
207 	}
208 
209 
210 	/**
211 	 * @see XMPMeta#deleteProperty(String, String)
212 	 */
deleteProperty(String schemaNS, String propName)213 	public void deleteProperty(String schemaNS, String propName)
214 	{
215 		try
216 		{
217 			ParameterAsserts.assertSchemaNS(schemaNS);
218 			ParameterAsserts.assertPropName(propName);
219 
220 			XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
221 
222 			XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);
223 			if (propNode != null)
224 			{
225 				XMPNodeUtils.deleteNode(propNode);
226 			}
227 		}
228 		catch (XMPException e)
229 		{
230 			// EMPTY, exceptions are ignored within delete
231 		}
232 	}
233 
234 
235 	/**
236 	 * @see XMPMeta#deleteQualifier(String, String, String, String)
237 	 */
deleteQualifier(String schemaNS, String propName, String qualNS, String qualName)238 	public void deleteQualifier(String schemaNS, String propName, String qualNS, String qualName)
239 	{
240 		try
241 		{
242 			// Note: qualNS and qualName are checked inside composeQualfierPath
243 			ParameterAsserts.assertSchemaNS(schemaNS);
244 			ParameterAsserts.assertPropName(propName);
245 
246 			String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName);
247 			deleteProperty(schemaNS, qualPath);
248 		}
249 		catch (XMPException e)
250 		{
251 			// EMPTY, exceptions within delete are ignored
252 		}
253 	}
254 
255 
256 	/**
257 	 * @see XMPMeta#deleteStructField(String, String, String, String)
258 	 */
deleteStructField(String schemaNS, String structName, String fieldNS, String fieldName)259 	public void deleteStructField(String schemaNS, String structName, String fieldNS,
260 			String fieldName)
261 	{
262 		try
263 		{
264 			// fieldNS and fieldName are checked inside composeStructFieldPath
265 			ParameterAsserts.assertSchemaNS(schemaNS);
266 			ParameterAsserts.assertStructName(structName);
267 
268 			String fieldPath = structName
269 					+ XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
270 			deleteProperty(schemaNS, fieldPath);
271 		}
272 		catch (XMPException e)
273 		{
274 			// EMPTY, exceptions within delete are ignored
275 		}
276 	}
277 
278 
279 	/**
280 	 * @see XMPMeta#doesPropertyExist(String, String)
281 	 */
doesPropertyExist(String schemaNS, String propName)282 	public boolean doesPropertyExist(String schemaNS, String propName)
283 	{
284 		try
285 		{
286 			ParameterAsserts.assertSchemaNS(schemaNS);
287 			ParameterAsserts.assertPropName(propName);
288 
289 			XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
290 			final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);
291 			return propNode != null;
292 		}
293 		catch (XMPException e)
294 		{
295 			return false;
296 		}
297 	}
298 
299 
300 	/**
301 	 * @see XMPMeta#doesArrayItemExist(String, String, int)
302 	 */
doesArrayItemExist(String schemaNS, String arrayName, int itemIndex)303 	public boolean doesArrayItemExist(String schemaNS, String arrayName, int itemIndex)
304 	{
305 		try
306 		{
307 			ParameterAsserts.assertSchemaNS(schemaNS);
308 			ParameterAsserts.assertArrayName(arrayName);
309 
310 			String path = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex);
311 			return doesPropertyExist(schemaNS, path);
312 		}
313 		catch (XMPException e)
314 		{
315 			return false;
316 		}
317 	}
318 
319 
320 	/**
321 	 * @see XMPMeta#doesStructFieldExist(String, String, String, String)
322 	 */
doesStructFieldExist(String schemaNS, String structName, String fieldNS, String fieldName)323 	public boolean doesStructFieldExist(String schemaNS, String structName, String fieldNS,
324 			String fieldName)
325 	{
326 		try
327 		{
328 			// fieldNS and fieldName are checked inside composeStructFieldPath()
329 			ParameterAsserts.assertSchemaNS(schemaNS);
330 			ParameterAsserts.assertStructName(structName);
331 
332 			String path = XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
333 			return doesPropertyExist(schemaNS, structName + path);
334 		}
335 		catch (XMPException e)
336 		{
337 			return false;
338 		}
339 	}
340 
341 
342 	/**
343 	 * @see XMPMeta#doesQualifierExist(String, String, String, String)
344 	 */
doesQualifierExist(String schemaNS, String propName, String qualNS, String qualName)345 	public boolean doesQualifierExist(String schemaNS, String propName, String qualNS,
346 			String qualName)
347 	{
348 		try
349 		{
350 			// qualNS and qualName are checked inside composeQualifierPath()
351 			ParameterAsserts.assertSchemaNS(schemaNS);
352 			ParameterAsserts.assertPropName(propName);
353 
354 			String path = XMPPathFactory.composeQualifierPath(qualNS, qualName);
355 			return doesPropertyExist(schemaNS, propName + path);
356 		}
357 		catch (XMPException e)
358 		{
359 			return false;
360 		}
361 	}
362 
363 
364 	/**
365 	 * @see XMPMeta#getArrayItem(String, String, int)
366 	 */
getArrayItem(String schemaNS, String arrayName, int itemIndex)367 	public XMPProperty getArrayItem(String schemaNS, String arrayName, int itemIndex)
368 			throws XMPException
369 	{
370 		ParameterAsserts.assertSchemaNS(schemaNS);
371 		ParameterAsserts.assertArrayName(arrayName);
372 
373 		String itemPath = XMPPathFactory.composeArrayItemPath(arrayName, itemIndex);
374 		return getProperty(schemaNS, itemPath);
375 	}
376 
377 
378 	/**
379 	 * @throws XMPException
380 	 * @see XMPMeta#getLocalizedText(String, String, String, String)
381 	 */
getLocalizedText(String schemaNS, String altTextName, String genericLang, String specificLang)382 	public XMPProperty getLocalizedText(String schemaNS, String altTextName, String genericLang,
383 			String specificLang) throws XMPException
384 	{
385 		ParameterAsserts.assertSchemaNS(schemaNS);
386 		ParameterAsserts.assertArrayName(altTextName);
387 		ParameterAsserts.assertSpecificLang(specificLang);
388 
389 		genericLang = genericLang != null ? Utils.normalizeLangValue(genericLang) : null;
390 		specificLang = Utils.normalizeLangValue(specificLang);
391 
392 		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, altTextName);
393 		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);
394 		if (arrayNode == null)
395 		{
396 			return null;
397 		}
398 
399 		Object[] result = XMPNodeUtils.chooseLocalizedText(arrayNode, genericLang, specificLang);
400 		int match = ((Integer) result[0]).intValue();
401 		final XMPNode itemNode = (XMPNode) result[1];
402 
403 		if (match != XMPNodeUtils.CLT_NO_VALUES)
404 		{
405 			return new XMPProperty()
406 			{
407 				public Object getValue()
408 				{
409 					return itemNode.getValue();
410 				}
411 
412 
413 				public PropertyOptions getOptions()
414 				{
415 					return itemNode.getOptions();
416 				}
417 
418 
419 				public String getLanguage()
420 				{
421 					return itemNode.getQualifier(1).getValue();
422 				}
423 
424 
425 				public String toString()
426 				{
427 					return itemNode.getValue().toString();
428 				}
429 			};
430 		}
431 		else
432 		{
433 			return null;
434 		}
435 	}
436 
437 
438 	/**
439 	 * @see XMPMeta#setLocalizedText(String, String, String, String, String,
440 	 *      PropertyOptions)
441 	 */
442 	public void setLocalizedText(String schemaNS, String altTextName, String genericLang,
443 			String specificLang, String itemValue, PropertyOptions options) throws XMPException
444 	{
445 		ParameterAsserts.assertSchemaNS(schemaNS);
446 		ParameterAsserts.assertArrayName(altTextName);
447 		ParameterAsserts.assertSpecificLang(specificLang);
448 
449 		genericLang = genericLang != null ? Utils.normalizeLangValue(genericLang) : null;
450 		specificLang = Utils.normalizeLangValue(specificLang);
451 
452 		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, altTextName);
453 
454 		// Find the array node and set the options if it was just created.
455 		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, true, new PropertyOptions(
456 				PropertyOptions.ARRAY | PropertyOptions.ARRAY_ORDERED
457 						| PropertyOptions.ARRAY_ALTERNATE | PropertyOptions.ARRAY_ALT_TEXT));
458 
459 		if (arrayNode == null)
460 		{
461 			throw new XMPException("Failed to find or create array node", XMPError.BADXPATH);
462 		}
463 		else if (!arrayNode.getOptions().isArrayAltText())
464 		{
465 			if (!arrayNode.hasChildren() && arrayNode.getOptions().isArrayAlternate())
466 			{
467 				arrayNode.getOptions().setArrayAltText(true);
468 			}
469 			else
470 			{
471 				throw new XMPException(
472 					"Specified property is no alt-text array", XMPError.BADXPATH);
473 			}
474 		}
475 
476 		// Make sure the x-default item, if any, is first.
477 		boolean haveXDefault = false;
478 		XMPNode xdItem = null;
479 
480 		for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
481 		{
482 			XMPNode currItem = (XMPNode) it.next();
483 			if (!currItem.hasQualifier()
484 					|| !XMPConst.XML_LANG.equals(currItem.getQualifier(1).getName()))
485 			{
486 				throw new XMPException("Language qualifier must be first", XMPError.BADXPATH);
487 			}
488 			else if (XMPConst.X_DEFAULT.equals(currItem.getQualifier(1).getValue()))
489 			{
490 				xdItem = currItem;
491 				haveXDefault = true;
492 				break;
493 			}
494 		}
495 
496 		// Moves x-default to the beginning of the array
497 		if (xdItem != null  &&  arrayNode.getChildrenLength() > 1)
498 		{
499 			arrayNode.removeChild(xdItem);
500 			arrayNode.addChild(1, xdItem);
501 		}
502 
503 		// Find the appropriate item.
504 		// chooseLocalizedText will make sure the array is a language
505 		// alternative.
506 		Object[] result = XMPNodeUtils.chooseLocalizedText(arrayNode, genericLang, specificLang);
507 		int match = ((Integer) result[0]).intValue();
508 		XMPNode itemNode = (XMPNode) result[1];
509 
510 		boolean specificXDefault = XMPConst.X_DEFAULT.equals(specificLang);
511 
512 		switch (match)
513 		{
514 		case XMPNodeUtils.CLT_NO_VALUES:
515 
516 			// Create the array items for the specificLang and x-default, with
517 			// x-default first.
518 			XMPNodeUtils.appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue);
519 			haveXDefault = true;
520 			if (!specificXDefault)
521 			{
522 				XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
523 			}
524 			break;
525 
526 		case XMPNodeUtils.CLT_SPECIFIC_MATCH:
527 
528 			if (!specificXDefault)
529 			{
530 				// Update the specific item, update x-default if it matches the
531 				// old value.
532 				if (haveXDefault && xdItem != itemNode && xdItem != null
533 						&& xdItem.getValue().equals(itemNode.getValue()))
534 				{
535 					xdItem.setValue(itemValue);
536 				}
537 				// ! Do this after the x-default check!
538 				itemNode.setValue(itemValue);
539 			}
540 			else
541 			{
542 				// Update all items whose values match the old x-default value.
543 				assert  haveXDefault  &&  xdItem == itemNode;
544 				for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
545 				{
546 					XMPNode currItem = (XMPNode) it.next();
547 					if (currItem == xdItem
548 							|| !currItem.getValue().equals(
549 									xdItem != null ? xdItem.getValue() : null))
550 					{
551 						continue;
552 					}
553 					currItem.setValue(itemValue);
554 				}
555 				// And finally do the x-default item.
556 				if (xdItem != null)
557 				{
558 					xdItem.setValue(itemValue);
559 				}
560 			}
561 			break;
562 
563 		case XMPNodeUtils.CLT_SINGLE_GENERIC:
564 
565 			// Update the generic item, update x-default if it matches the old
566 			// value.
567 			if (haveXDefault && xdItem != itemNode && xdItem != null
568 					&& xdItem.getValue().equals(itemNode.getValue()))
569 			{
570 				xdItem.setValue(itemValue);
571 			}
572 			itemNode.setValue(itemValue); // ! Do this after
573 			// the x-default
574 			// check!
575 			break;
576 
577 		case XMPNodeUtils.CLT_MULTIPLE_GENERIC:
578 
579 			// Create the specific language, ignore x-default.
580 			XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
581 			if (specificXDefault)
582 			{
583 				haveXDefault = true;
584 			}
585 			break;
586 
587 		case XMPNodeUtils.CLT_XDEFAULT:
588 
589 			// Create the specific language, update x-default if it was the only
590 			// item.
591 			if (xdItem != null  &&  arrayNode.getChildrenLength() == 1)
592 			{
593 				xdItem.setValue(itemValue);
594 			}
595 			XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
596 			break;
597 
598 		case XMPNodeUtils.CLT_FIRST_ITEM:
599 
600 			// Create the specific language, don't add an x-default item.
601 			XMPNodeUtils.appendLangItem(arrayNode, specificLang, itemValue);
602 			if (specificXDefault)
603 			{
604 				haveXDefault = true;
605 			}
606 			break;
607 
608 		default:
609 			// does not happen under normal circumstances
610 			throw new XMPException("Unexpected result from ChooseLocalizedText",
611 					XMPError.INTERNALFAILURE);
612 
613 		}
614 
615 		// Add an x-default at the front if needed.
616 		if (!haveXDefault && arrayNode.getChildrenLength() == 1)
617 		{
618 			XMPNodeUtils.appendLangItem(arrayNode, XMPConst.X_DEFAULT, itemValue);
619 		}
620 	}
621 
622 
623 	/**
624 	 * @see XMPMeta#setLocalizedText(String, String, String, String, String)
625 	 */
626 	public void setLocalizedText(String schemaNS, String altTextName, String genericLang,
627 			String specificLang, String itemValue) throws XMPException
628 	{
629 		setLocalizedText(schemaNS, altTextName, genericLang, specificLang, itemValue, null);
630 	}
631 
632 
633 	/**
634 	 * @throws XMPException
635 	 * @see XMPMeta#getProperty(String, String)
636 	 */
637 	public XMPProperty getProperty(String schemaNS, String propName) throws XMPException
638 	{
639 		return getProperty(schemaNS, propName, VALUE_STRING);
640 	}
641 
642 
643 	/**
644 	 * Returns a property, but the result value can be requested. It can be one
645 	 * of {@link XMPMetaImpl#VALUE_STRING}, {@link XMPMetaImpl#VALUE_BOOLEAN},
646 	 * {@link XMPMetaImpl#VALUE_INTEGER}, {@link XMPMetaImpl#VALUE_LONG},
647 	 * {@link XMPMetaImpl#VALUE_DOUBLE}, {@link XMPMetaImpl#VALUE_DATE},
648 	 * {@link XMPMetaImpl#VALUE_CALENDAR}, {@link XMPMetaImpl#VALUE_BASE64}.
649 	 *
650 	 * @see XMPMeta#getProperty(String, String)
651 	 * @param schemaNS
652 	 *            a schema namespace
653 	 * @param propName
654 	 *            a property name or path
655 	 * @param valueType
656 	 *            the type of the value, see VALUE_...
657 	 * @return Returns an <code>XMPProperty</code>
658 	 * @throws XMPException
659 	 *             Collects any exception that occurs.
660 	 */
661 	protected XMPProperty getProperty(String schemaNS, String propName, int valueType)
662 			throws XMPException
663 	{
664 		ParameterAsserts.assertSchemaNS(schemaNS);
665 		ParameterAsserts.assertPropName(propName);
666 
667 		final XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
668 		final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);
669 
670 		if (propNode != null)
671 		{
672 			if (valueType != VALUE_STRING && propNode.getOptions().isCompositeProperty())
673 			{
674 				throw new XMPException("Property must be simple when a value type is requested",
675 						XMPError.BADXPATH);
676 			}
677 
678 			final Object value = evaluateNodeValue(valueType, propNode);
679 
680 			return new XMPProperty()
681 			{
682 				public Object getValue()
683 				{
684 					return value;
685 				}
686 
687 
688 				public PropertyOptions getOptions()
689 				{
690 					return propNode.getOptions();
691 				}
692 
693 
694 				public String getLanguage()
695 				{
696 					return null;
697 				}
698 
699 
700 				public String toString()
701 				{
702 					return value.toString();
703 				}
704 			};
705 		}
706 		else
707 		{
708 			return null;
709 		}
710 	}
711 
712 
713 	/**
714 	 * Returns a property, but the result value can be requested.
715 	 *
716 	 * @see XMPMeta#getProperty(String, String)
717 	 * @param schemaNS
718 	 *            a schema namespace
719 	 * @param propName
720 	 *            a property name or path
721 	 * @param valueType
722 	 *            the type of the value, see VALUE_...
723 	 * @return Returns the node value as an object according to the
724 	 *         <code>valueType</code>.
725 	 * @throws XMPException
726 	 *             Collects any exception that occurs.
727 	 */
728 	protected Object getPropertyObject(String schemaNS, String propName, int valueType)
729 			throws XMPException
730 	{
731 		ParameterAsserts.assertSchemaNS(schemaNS);
732 		ParameterAsserts.assertPropName(propName);
733 
734 		final XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
735 		final XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, false, null);
736 
737 		if (propNode != null)
738 		{
739 			if (valueType != VALUE_STRING && propNode.getOptions().isCompositeProperty())
740 			{
741 				throw new XMPException("Property must be simple when a value type is requested",
742 						XMPError.BADXPATH);
743 			}
744 
745 			return evaluateNodeValue(valueType, propNode);
746 		}
747 		else
748 		{
749 			return null;
750 		}
751 	}
752 
753 
754 	/**
755 	 * @see XMPMeta#getPropertyBoolean(String, String)
756 	 */
757 	public Boolean getPropertyBoolean(String schemaNS, String propName) throws XMPException
758 	{
759 		return (Boolean) getPropertyObject(schemaNS, propName, VALUE_BOOLEAN);
760 	}
761 
762 
763 	/**
764 	 * @throws XMPException
765 	 * @see XMPMeta#setPropertyBoolean(String, String, boolean, PropertyOptions)
766 	 */
767 	public void setPropertyBoolean(String schemaNS, String propName, boolean propValue,
768 			PropertyOptions options) throws XMPException
769 	{
770 		setProperty(schemaNS, propName, propValue ? TRUESTR : FALSESTR, options);
771 	}
772 
773 
774 	/**
775 	 * @see XMPMeta#setPropertyBoolean(String, String, boolean)
776 	 */
777 	public void setPropertyBoolean(String schemaNS, String propName, boolean propValue)
778 			throws XMPException
779 	{
780 		setProperty(schemaNS, propName, propValue ? TRUESTR : FALSESTR, null);
781 	}
782 
783 
784 	/**
785 	 * @see XMPMeta#getPropertyInteger(String, String)
786 	 */
787 	public Integer getPropertyInteger(String schemaNS, String propName) throws XMPException
788 	{
789 		return (Integer) getPropertyObject(schemaNS, propName, VALUE_INTEGER);
790 	}
791 
792 
793 	/**
794 	 * @see XMPMeta#setPropertyInteger(String, String, int, PropertyOptions)
795 	 */
796 	public void setPropertyInteger(String schemaNS, String propName, int propValue,
797 			PropertyOptions options) throws XMPException
798 	{
799 		setProperty(schemaNS, propName, new Integer(propValue), options);
800 	}
801 
802 
803 	/**
804 	 * @see XMPMeta#setPropertyInteger(String, String, int)
805 	 */
806 	public void setPropertyInteger(String schemaNS, String propName, int propValue)
807 			throws XMPException
808 	{
809 		setProperty(schemaNS, propName, new Integer(propValue), null);
810 	}
811 
812 
813 	/**
814 	 * @see XMPMeta#getPropertyLong(String, String)
815 	 */
816 	public Long getPropertyLong(String schemaNS, String propName) throws XMPException
817 	{
818 		return (Long) getPropertyObject(schemaNS, propName, VALUE_LONG);
819 	}
820 
821 
822 	/**
823 	 * @see XMPMeta#setPropertyLong(String, String, long, PropertyOptions)
824 	 */
825 	public void setPropertyLong(String schemaNS, String propName, long propValue,
826 			PropertyOptions options) throws XMPException
827 	{
828 		setProperty(schemaNS, propName, new Long(propValue), options);
829 	}
830 
831 
832 	/**
833 	 * @see XMPMeta#setPropertyLong(String, String, long)
834 	 */
835 	public void setPropertyLong(String schemaNS, String propName, long propValue)
836 			throws XMPException
837 	{
838 		setProperty(schemaNS, propName, new Long(propValue), null);
839 	}
840 
841 
842 	/**
843 	 * @see XMPMeta#getPropertyDouble(String, String)
844 	 */
845 	public Double getPropertyDouble(String schemaNS, String propName) throws XMPException
846 	{
847 		return (Double) getPropertyObject(schemaNS, propName, VALUE_DOUBLE);
848 	}
849 
850 
851 	/**
852 	 * @see XMPMeta#setPropertyDouble(String, String, double, PropertyOptions)
853 	 */
854 	public void setPropertyDouble(String schemaNS, String propName, double propValue,
855 			PropertyOptions options) throws XMPException
856 	{
857 		setProperty(schemaNS, propName, new Double(propValue), options);
858 	}
859 
860 
861 	/**
862 	 * @see XMPMeta#setPropertyDouble(String, String, double)
863 	 */
864 	public void setPropertyDouble(String schemaNS, String propName, double propValue)
865 			throws XMPException
866 	{
867 		setProperty(schemaNS, propName, new Double(propValue), null);
868 	}
869 
870 
871 	/**
872 	 * @see XMPMeta#getPropertyDate(String, String)
873 	 */
874 	public XMPDateTime getPropertyDate(String schemaNS, String propName) throws XMPException
875 	{
876 		return (XMPDateTime) getPropertyObject(schemaNS, propName, VALUE_DATE);
877 	}
878 
879 
880 	/**
881 	 * @see XMPMeta#setPropertyDate(String, String, XMPDateTime,
882 	 *      PropertyOptions)
883 	 */
884 	public void setPropertyDate(String schemaNS, String propName, XMPDateTime propValue,
885 			PropertyOptions options) throws XMPException
886 	{
887 		setProperty(schemaNS, propName, propValue, options);
888 	}
889 
890 
891 	/**
892 	 * @see XMPMeta#setPropertyDate(String, String, XMPDateTime)
893 	 */
894 	public void setPropertyDate(String schemaNS, String propName, XMPDateTime propValue)
895 			throws XMPException
896 	{
897 		setProperty(schemaNS, propName, propValue, null);
898 	}
899 
900 
901 	/**
902 	 * @see XMPMeta#getPropertyCalendar(String, String)
903 	 */
904 	public Calendar getPropertyCalendar(String schemaNS, String propName) throws XMPException
905 	{
906 		return (Calendar) getPropertyObject(schemaNS, propName, VALUE_CALENDAR);
907 	}
908 
909 
910 	/**
911 	 * @see XMPMeta#setPropertyCalendar(String, String, Calendar,
912 	 *      PropertyOptions)
913 	 */
914 	public void setPropertyCalendar(String schemaNS, String propName, Calendar propValue,
915 			PropertyOptions options) throws XMPException
916 	{
917 		setProperty(schemaNS, propName, propValue, options);
918 	}
919 
920 
921 	/**
922 	 * @see XMPMeta#setPropertyCalendar(String, String, Calendar)
923 	 */
924 	public void setPropertyCalendar(String schemaNS, String propName, Calendar propValue)
925 			throws XMPException
926 	{
927 		setProperty(schemaNS, propName, propValue, null);
928 	}
929 
930 
931 	/**
932 	 * @see XMPMeta#getPropertyBase64(String, String)
933 	 */
934 	public byte[] getPropertyBase64(String schemaNS, String propName) throws XMPException
935 	{
936 		return (byte[]) getPropertyObject(schemaNS, propName, VALUE_BASE64);
937 	}
938 
939 
940 	/**
941 	 * @see XMPMeta#getPropertyString(String, String)
942 	 */
943 	public String getPropertyString(String schemaNS, String propName) throws XMPException
944 	{
945 		return (String) getPropertyObject(schemaNS, propName, VALUE_STRING);
946 	}
947 
948 
949 	/**
950 	 * @see XMPMeta#setPropertyBase64(String, String, byte[], PropertyOptions)
951 	 */
952 	public void setPropertyBase64(String schemaNS, String propName, byte[] propValue,
953 			PropertyOptions options) throws XMPException
954 	{
955 		setProperty(schemaNS, propName, propValue, options);
956 	}
957 
958 
959 	/**
960 	 * @see XMPMeta#setPropertyBase64(String, String, byte[])
961 	 */
962 	public void setPropertyBase64(String schemaNS, String propName, byte[] propValue)
963 			throws XMPException
964 	{
965 		setProperty(schemaNS, propName, propValue, null);
966 	}
967 
968 
969 	/**
970 	 * @throws XMPException
971 	 * @see XMPMeta#getQualifier(String, String, String, String)
972 	 */
973 	public XMPProperty getQualifier(String schemaNS, String propName, String qualNS,
974 		String qualName) throws XMPException
975 	{
976 		// qualNS and qualName are checked inside composeQualfierPath
977 		ParameterAsserts.assertSchemaNS(schemaNS);
978 		ParameterAsserts.assertPropName(propName);
979 
980 		String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName);
981 		return getProperty(schemaNS, qualPath);
982 	}
983 
984 
985 	/**
986 	 * @see XMPMeta#getStructField(String, String, String, String)
987 	 */
988 	public XMPProperty getStructField(String schemaNS, String structName, String fieldNS,
989 			String fieldName) throws XMPException
990 	{
991 		// fieldNS and fieldName are checked inside composeStructFieldPath
992 		ParameterAsserts.assertSchemaNS(schemaNS);
993 		ParameterAsserts.assertStructName(structName);
994 
995 		String fieldPath = structName + XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
996 		return getProperty(schemaNS, fieldPath);
997 	}
998 
999 
1000 	/**
1001 	 * @throws XMPException
1002 	 * @see XMPMeta#iterator()
1003 	 */
1004 	public XMPIterator iterator() throws XMPException
1005 	{
1006 		return iterator(null, null, null);
1007 	}
1008 
1009 
1010 	/**
1011 	 * @see XMPMeta#iterator(IteratorOptions)
1012 	 */
1013 	public XMPIterator iterator(IteratorOptions options) throws XMPException
1014 	{
1015 		return iterator(null, null, options);
1016 	}
1017 
1018 
1019 	/**
1020 	 * @see XMPMeta#iterator(String, String, IteratorOptions)
1021 	 */
1022 	public XMPIterator iterator(String schemaNS, String propName, IteratorOptions options)
1023 			throws XMPException
1024 	{
1025 		return new XMPIteratorImpl(this, schemaNS, propName, options);
1026 	}
1027 
1028 
1029 	/**
1030 	 * @throws XMPException
1031 	 * @see XMPMeta#setArrayItem(String, String, int, String, PropertyOptions)
1032 	 */
1033 	public void setArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue,
1034 			PropertyOptions options) throws XMPException
1035 	{
1036 		ParameterAsserts.assertSchemaNS(schemaNS);
1037 		ParameterAsserts.assertArrayName(arrayName);
1038 
1039 		// Just lookup, don't try to create.
1040 		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
1041 		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);
1042 
1043 		if (arrayNode != null)
1044 		{
1045 			doSetArrayItem(arrayNode, itemIndex, itemValue, options, false);
1046 		}
1047 		else
1048 		{
1049 			throw new XMPException("Specified array does not exist", XMPError.BADXPATH);
1050 		}
1051 	}
1052 
1053 
1054 	/**
1055 	 * @see XMPMeta#setArrayItem(String, String, int, String)
1056 	 */
1057 	public void setArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue)
1058 			throws XMPException
1059 	{
1060 		setArrayItem(schemaNS, arrayName, itemIndex, itemValue, null);
1061 	}
1062 
1063 
1064 	/**
1065 	 * @throws XMPException
1066 	 * @see XMPMeta#insertArrayItem(String, String, int, String,
1067 	 *      PropertyOptions)
1068 	 */
1069 	public void insertArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue,
1070 			PropertyOptions options) throws XMPException
1071 	{
1072 		ParameterAsserts.assertSchemaNS(schemaNS);
1073 		ParameterAsserts.assertArrayName(arrayName);
1074 
1075 		// Just lookup, don't try to create.
1076 		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
1077 		XMPNode arrayNode = XMPNodeUtils.findNode(tree, arrayPath, false, null);
1078 
1079 		if (arrayNode != null)
1080 		{
1081 			doSetArrayItem(arrayNode, itemIndex, itemValue, options, true);
1082 		}
1083 		else
1084 		{
1085 			throw new XMPException("Specified array does not exist", XMPError.BADXPATH);
1086 		}
1087 	}
1088 
1089 
1090 	/**
1091 	 * @see XMPMeta#insertArrayItem(String, String, int, String)
1092 	 */
1093 	public void insertArrayItem(String schemaNS, String arrayName, int itemIndex, String itemValue)
1094 			throws XMPException
1095 	{
1096 		insertArrayItem(schemaNS, arrayName, itemIndex, itemValue, null);
1097 	}
1098 
1099 
1100 	/**
1101 	 * @throws XMPException
1102 	 * @see XMPMeta#setProperty(String, String, Object, PropertyOptions)
1103 	 */
1104 	public void setProperty(String schemaNS, String propName, Object propValue,
1105 			PropertyOptions options) throws XMPException
1106 	{
1107 		ParameterAsserts.assertSchemaNS(schemaNS);
1108 		ParameterAsserts.assertPropName(propName);
1109 
1110 		options = XMPNodeUtils.verifySetOptions(options, propValue);
1111 
1112 		XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);
1113 
1114 		XMPNode propNode = XMPNodeUtils.findNode(tree, expPath, true, options);
1115 		if (propNode != null)
1116 		{
1117 			setNode(propNode, propValue, options, false);
1118 		}
1119 		else
1120 		{
1121 			throw new XMPException("Specified property does not exist", XMPError.BADXPATH);
1122 		}
1123 	}
1124 
1125 
1126 	/**
1127 	 * @see XMPMeta#setProperty(String, String, Object)
1128 	 */
1129 	public void setProperty(String schemaNS, String propName, Object propValue) throws XMPException
1130 	{
1131 		setProperty(schemaNS, propName, propValue, null);
1132 	}
1133 
1134 
1135 	/**
1136 	 * @throws XMPException
1137 	 * @see XMPMeta#setQualifier(String, String, String, String, String,
1138 	 *      PropertyOptions)
1139 	 */
1140 	public void setQualifier(String schemaNS, String propName, String qualNS, String qualName,
1141 			String qualValue, PropertyOptions options) throws XMPException
1142 	{
1143 		ParameterAsserts.assertSchemaNS(schemaNS);
1144 		ParameterAsserts.assertPropName(propName);
1145 
1146 		if (!doesPropertyExist(schemaNS, propName))
1147 		{
1148 			throw new XMPException("Specified property does not exist!", XMPError.BADXPATH);
1149 		}
1150 
1151 		String qualPath = propName + XMPPathFactory.composeQualifierPath(qualNS, qualName);
1152 		setProperty(schemaNS, qualPath, qualValue, options);
1153 	}
1154 
1155 
1156 	/**
1157 	 * @see XMPMeta#setQualifier(String, String, String, String, String)
1158 	 */
1159 	public void setQualifier(String schemaNS, String propName, String qualNS, String qualName,
1160 			String qualValue) throws XMPException
1161 	{
1162 		setQualifier(schemaNS, propName, qualNS, qualName, qualValue, null);
1163 
1164 	}
1165 
1166 
1167 	/**
1168 	 * @see XMPMeta#setStructField(String, String, String, String, String,
1169 	 *      PropertyOptions)
1170 	 */
1171 	public void setStructField(String schemaNS, String structName, String fieldNS,
1172 			String fieldName, String fieldValue, PropertyOptions options) throws XMPException
1173 	{
1174 		ParameterAsserts.assertSchemaNS(schemaNS);
1175 		ParameterAsserts.assertStructName(structName);
1176 
1177 		String fieldPath = structName + XMPPathFactory.composeStructFieldPath(fieldNS, fieldName);
1178 		setProperty(schemaNS, fieldPath, fieldValue, options);
1179 	}
1180 
1181 
1182 	/**
1183 	 * @see XMPMeta#setStructField(String, String, String, String, String)
1184 	 */
1185 	public void setStructField(String schemaNS, String structName, String fieldNS,
1186 			String fieldName, String fieldValue) throws XMPException
1187 	{
1188 		setStructField(schemaNS, structName, fieldNS, fieldName, fieldValue, null);
1189 	}
1190 
1191 
1192 	/**
1193 	 * @see XMPMeta#getObjectName()
1194 	 */
1195 	public String getObjectName()
1196 	{
1197 		return tree.getName() != null ? tree.getName() : "";
1198 	}
1199 
1200 
1201 	/**
1202 	 * @see XMPMeta#setObjectName(String)
1203 	 */
1204 	public void setObjectName(String name)
1205 	{
1206 		tree.setName(name);
1207 	}
1208 
1209 
1210 	/**
1211 	 * @see XMPMeta#getPacketHeader()
1212 	 */
1213 	public String getPacketHeader()
1214 	{
1215 		return packetHeader;
1216 	}
1217 
1218 
1219 	/**
1220 	 * Sets the packetHeader attributes, only used by the parser.
1221 	 * @param packetHeader the processing instruction content
1222 	 */
1223 	public void setPacketHeader(String packetHeader)
1224 	{
1225 		this.packetHeader = packetHeader;
1226 	}
1227 
1228 
1229 	/**
1230 	 * Performs a deep clone of the XMPMeta-object
1231 	 *
1232 	 * @see java.lang.Object#clone()
1233 	 */
1234 	public Object clone()
1235 	{
1236 		XMPNode clonedTree = (XMPNode) tree.clone();
1237 		return new XMPMetaImpl(clonedTree);
1238 	}
1239 
1240 
1241 	/**
1242 	 * @see XMPMeta#dumpObject()
1243 	 */
1244 	public String dumpObject()
1245 	{
1246 		// renders tree recursively
1247 		return getRoot().dumpNode(true);
1248 	}
1249 
1250 
1251 	/**
1252 	 * @see XMPMeta#sort()
1253 	 */
1254 	public void sort()
1255 	{
1256 		this.tree.sort();
1257 	}
1258 
1259 
1260 	/**
1261 	 * @return Returns the root node of the XMP tree.
1262 	 */
1263 	public XMPNode getRoot()
1264 	{
1265 		return tree;
1266 	}
1267 
1268 
1269 
1270 	// -------------------------------------------------------------------------------------
1271 	// private
1272 
1273 
1274 	/**
1275 	 * Locate or create the item node and set the value. Note the index
1276 	 * parameter is one-based! The index can be in the range [1..size + 1] or
1277 	 * "last()", normalize it and check the insert flags. The order of the
1278 	 * normalization checks is important. If the array is empty we end up with
1279 	 * an index and location to set item size + 1.
1280 	 *
1281 	 * @param arrayNode an array node
1282 	 * @param itemIndex the index where to insert the item
1283 	 * @param itemValue the item value
1284 	 * @param itemOptions the options for the new item
1285 	 * @param insert insert oder overwrite at index position?
1286 	 * @throws XMPException
1287 	 */
1288 	private void doSetArrayItem(XMPNode arrayNode, int itemIndex, String itemValue,
1289 			PropertyOptions itemOptions, boolean insert) throws XMPException
1290 	{
1291 		XMPNode itemNode = new XMPNode(ARRAY_ITEM_NAME, null);
1292 		itemOptions = XMPNodeUtils.verifySetOptions(itemOptions, itemValue);
1293 
1294 		// in insert mode the index after the last is allowed,
1295 		// even ARRAY_LAST_ITEM points to the index *after* the last.
1296 		int maxIndex = insert ? arrayNode.getChildrenLength() + 1 : arrayNode.getChildrenLength();
1297 		if (itemIndex == ARRAY_LAST_ITEM)
1298 		{
1299 			itemIndex = maxIndex;
1300 		}
1301 
1302 		if (1 <= itemIndex && itemIndex <= maxIndex)
1303 		{
1304 			if (!insert)
1305 			{
1306 				arrayNode.removeChild(itemIndex);
1307 			}
1308 			arrayNode.addChild(itemIndex, itemNode);
1309 			setNode(itemNode, itemValue, itemOptions, false);
1310 		}
1311 		else
1312 		{
1313 			throw new XMPException("Array index out of bounds", XMPError.BADINDEX);
1314 		}
1315 	}
1316 
1317 
1318 	/**
1319 	 * The internals for setProperty() and related calls, used after the node is
1320 	 * found or created.
1321 	 *
1322 	 * @param node
1323 	 *            the newly created node
1324 	 * @param value
1325 	 *            the node value, can be <code>null</code>
1326 	 * @param newOptions
1327 	 *            options for the new node, must not be <code>null</code>.
1328 	 * @param deleteExisting flag if the existing value is to be overwritten
1329 	 * @throws XMPException thrown if options and value do not correspond
1330 	 */
1331 	void setNode(XMPNode node, Object value, PropertyOptions newOptions, boolean deleteExisting)
1332 			throws XMPException
1333 	{
1334 		if (deleteExisting)
1335 		{
1336 			node.clear();
1337 		}
1338 
1339 		// its checked by setOptions(), if the merged result is a valid options set
1340 		node.getOptions().mergeWith(newOptions);
1341 
1342 		if (!node.getOptions().isCompositeProperty())
1343 		{
1344 			// This is setting the value of a leaf node.
1345 			XMPNodeUtils.setNodeValue(node, value);
1346 		}
1347 		else
1348 		{
1349 			if (value != null && value.toString().length() > 0)
1350 			{
1351 				throw new XMPException("Composite nodes can't have values", XMPError.BADXPATH);
1352 			}
1353 
1354 			node.removeChildren();
1355 		}
1356 
1357 	}
1358 
1359 
1360 	/**
1361 	 * Evaluates a raw node value to the given value type, apply special
1362 	 * conversions for defined types in XMP.
1363 	 *
1364 	 * @param valueType
1365 	 *            an int indicating the value type
1366 	 * @param propNode
1367 	 *            the node containing the value
1368 	 * @return Returns a literal value for the node.
1369 	 * @throws XMPException
1370 	 */
1371 	private Object evaluateNodeValue(int valueType, final XMPNode propNode) throws XMPException
1372 	{
1373 		final Object value;
1374 		String rawValue = propNode.getValue();
1375 		switch (valueType)
1376 		{
1377 		case VALUE_BOOLEAN:
1378 			value = new Boolean(XMPUtils.convertToBoolean(rawValue));
1379 			break;
1380 		case VALUE_INTEGER:
1381 			value = new Integer(XMPUtils.convertToInteger(rawValue));
1382 			break;
1383 		case VALUE_LONG:
1384 			value = new Long(XMPUtils.convertToLong(rawValue));
1385 			break;
1386 		case VALUE_DOUBLE:
1387 			value = new Double(XMPUtils.convertToDouble(rawValue));
1388 			break;
1389 		case VALUE_DATE:
1390 			value = XMPUtils.convertToDate(rawValue);
1391 			break;
1392 		case VALUE_CALENDAR:
1393 			XMPDateTime dt = XMPUtils.convertToDate(rawValue);
1394 			value = dt.getCalendar();
1395 			break;
1396 		case VALUE_BASE64:
1397 			value = XMPUtils.decodeBase64(rawValue);
1398 			break;
1399 		case VALUE_STRING:
1400 		default:
1401 			// leaf values return empty string instead of null
1402 			// for the other cases the converter methods provides a "null"
1403 			// value.
1404 			// a default value can only occur if this method is made public.
1405 			value = rawValue != null || propNode.getOptions().isCompositeProperty() ? rawValue : "";
1406 			break;
1407 		}
1408 		return value;
1409 	}
1410 }
1411