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