summaryrefslogtreecommitdiff
path: root/XMPCore/src/com/adobe/xmp/impl/XMPUtilsImpl.java
blob: ab36996e59aad664790af2d479e81321f2476a07 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
// =================================================================================================
// ADOBE SYSTEMS INCORPORATED
// Copyright 2006 Adobe Systems Incorporated
// All Rights Reserved
//
// NOTICE:  Adobe permits you to use, modify, and distribute this file in accordance with the terms
// of the Adobe license agreement accompanying it.
// =================================================================================================



package com.adobe.xmp.impl;

import java.util.Iterator;

import com.adobe.xmp.XMPConst;
import com.adobe.xmp.XMPError;
import com.adobe.xmp.XMPException;
import com.adobe.xmp.XMPMeta;
import com.adobe.xmp.XMPMetaFactory;
import com.adobe.xmp.XMPUtils;
import com.adobe.xmp.impl.xpath.XMPPath;
import com.adobe.xmp.impl.xpath.XMPPathParser;
import com.adobe.xmp.options.PropertyOptions;
import com.adobe.xmp.properties.XMPAliasInfo;



/**
 * @since 11.08.2006
 */
public class XMPUtilsImpl implements XMPConst
{
	/** */
	private static final int UCK_NORMAL = 0;
	/** */
	private static final int UCK_SPACE = 1;
	/** */
	private static final int UCK_COMMA = 2;
	/** */
	private static final int UCK_SEMICOLON = 3;
	/** */
	private static final int UCK_QUOTE = 4;
	/** */
	private static final int UCK_CONTROL = 5;


	/**
	 * Private constructor, as
	 */
	private XMPUtilsImpl()
	{
		// EMPTY
	}


	/**
	 * @see XMPUtils#catenateArrayItems(XMPMeta, String, String, String, String,
	 *      boolean)
	 * 
	 * @param xmp
	 *            The XMP object containing the array to be catenated.
	 * @param schemaNS
	 *            The schema namespace URI for the array. Must not be null or
	 *            the empty string.
	 * @param arrayName
	 *            The name of the array. May be a general path expression, must
	 *            not be null or the empty string. Each item in the array must
	 *            be a simple string value.
	 * @param separator
	 *            The string to be used to separate the items in the catenated
	 *            string. Defaults to "; ", ASCII semicolon and space
	 *            (U+003B, U+0020).
	 * @param quotes
	 *            The characters to be used as quotes around array items that
	 *            contain a separator. Defaults to '"'
	 * @param allowCommas
	 *            Option flag to control the catenation.
	 * @return Returns the string containing the catenated array items.
	 * @throws XMPException
	 *             Forwards the Exceptions from the metadata processing
	 */
	public static String catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName,
			String separator, String quotes, boolean allowCommas) throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(arrayName);
		ParameterAsserts.assertImplementation(xmp);
		if (separator == null  ||  separator.length() == 0)
		{
			separator = "; ";	
		}
		if (quotes == null  ||  quotes.length() == 0)
		{	
			quotes = "\"";
		}
		
		XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp;
		XMPNode arrayNode = null;
		XMPNode currItem = null;

		// Return an empty result if the array does not exist, 
		// hurl if it isn't the right form.
		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
		arrayNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), arrayPath, false, null);
		if (arrayNode == null)
		{
			return "";
		}
		else if (!arrayNode.getOptions().isArray() || arrayNode.getOptions().isArrayAlternate())
		{
			throw new XMPException("Named property must be non-alternate array", XMPError.BADPARAM);
		}

		// Make sure the separator is OK.
		checkSeparator(separator);
		// Make sure the open and close quotes are a legitimate pair.
		char openQuote = quotes.charAt(0);
		char closeQuote = checkQuotes(quotes, openQuote);

		// Build the result, quoting the array items, adding separators.
		// Hurl if any item isn't simple.

		StringBuffer catinatedString = new StringBuffer();

		for (Iterator it = arrayNode.iterateChildren(); it.hasNext();)
		{
			currItem = (XMPNode) it.next();
			if (currItem.getOptions().isCompositeProperty())
			{
				throw new XMPException("Array items must be simple", XMPError.BADPARAM);
			}
			String str = applyQuotes(currItem.getValue(), openQuote, closeQuote, allowCommas);

			catinatedString.append(str);
			if (it.hasNext())
			{
				catinatedString.append(separator);
			}
		}

		return catinatedString.toString();
	}


	/**
	 * see {@link XMPUtils#separateArrayItems(XMPMeta, String, String, String, 
	 * PropertyOptions, boolean)}
	 * 
	 * @param xmp
	 *            The XMP object containing the array to be updated.
	 * @param schemaNS
	 *            The schema namespace URI for the array. Must not be null or
	 *            the empty string.
	 * @param arrayName
	 *            The name of the array. May be a general path expression, must
	 *            not be null or the empty string. Each item in the array must
	 *            be a simple string value.
	 * @param catedStr
	 *            The string to be separated into the array items.
	 * @param arrayOptions
	 *            Option flags to control the separation.
	 * @param preserveCommas
	 *            Flag if commas shall be preserved
	 * 
	 * @throws XMPException
	 *             Forwards the Exceptions from the metadata processing
	 */
	public static void separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName,
			String catedStr, PropertyOptions arrayOptions, boolean preserveCommas)
			throws XMPException
	{
		ParameterAsserts.assertSchemaNS(schemaNS);
		ParameterAsserts.assertArrayName(arrayName);
		if (catedStr == null)
		{
			throw new XMPException("Parameter must not be null", XMPError.BADPARAM);
		}
		ParameterAsserts.assertImplementation(xmp);
		XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp;

		// Keep a zero value, has special meaning below.
		XMPNode arrayNode = separateFindCreateArray(schemaNS, arrayName, arrayOptions, xmpImpl);

		// Extract the item values one at a time, until the whole input string is done.
		String itemValue;
		int itemStart, itemEnd;
		int nextKind = UCK_NORMAL, charKind = UCK_NORMAL;
		char ch = 0, nextChar = 0;
		
		itemEnd = 0;
		int endPos = catedStr.length();
		while (itemEnd < endPos)
		{
			// Skip any leading spaces and separation characters. Always skip commas here.
			// They can be kept when within a value, but not when alone between values.
			for (itemStart = itemEnd; itemStart < endPos; itemStart++)
			{
				ch = catedStr.charAt(itemStart);
				charKind = classifyCharacter(ch);
				if (charKind == UCK_NORMAL || charKind == UCK_QUOTE)
				{
					break;
				}
			}
			if (itemStart >= endPos)
			{
				break;
			}

			if (charKind != UCK_QUOTE)
			{
				// This is not a quoted value. Scan for the end, create an array
				// item from the substring.
				for (itemEnd = itemStart; itemEnd < endPos; itemEnd++)
				{
					ch = catedStr.charAt(itemEnd);
					charKind = classifyCharacter(ch);

					if (charKind == UCK_NORMAL || charKind == UCK_QUOTE  ||
						(charKind == UCK_COMMA && preserveCommas))
					{
						continue;
					}
					else if (charKind != UCK_SPACE)
					{
						break;
					}
					else if ((itemEnd + 1) < endPos)
					{
						ch = catedStr.charAt(itemEnd + 1);
						nextKind = classifyCharacter(ch);
						if (nextKind == UCK_NORMAL  ||  nextKind == UCK_QUOTE  ||
							(nextKind == UCK_COMMA && preserveCommas))
						{
							continue;
						}
					}
					
					// Anything left?
					break; // Have multiple spaces, or a space followed by a
							// separator.
				}
				itemValue = catedStr.substring(itemStart, itemEnd);
			}
			else
			{
				// Accumulate quoted values into a local string, undoubling
				// internal quotes that
				// match the surrounding quotes. Do not undouble "unmatching"
				// quotes.

				char openQuote = ch;
				char closeQuote = getClosingQuote(openQuote);

				itemStart++; // Skip the opening quote;
				itemValue = "";

				for (itemEnd = itemStart; itemEnd < endPos; itemEnd++)
				{
					ch = catedStr.charAt(itemEnd);
					charKind = classifyCharacter(ch);

					if (charKind != UCK_QUOTE || !isSurroundingQuote(ch, openQuote, closeQuote))
					{
						// This is not a matching quote, just append it to the
						// item value.
						itemValue += ch;
					}
					else
					{
						// This is a "matching" quote. Is it doubled, or the
						// final closing quote?
						// Tolerate various edge cases like undoubled opening
						// (non-closing) quotes,
						// or end of input.

						if ((itemEnd + 1) < endPos)
						{
							nextChar = catedStr.charAt(itemEnd + 1);
							nextKind = classifyCharacter(nextChar);
						}
						else
						{
							nextKind = UCK_SEMICOLON;
							nextChar = 0x3B;
						}

						if (ch == nextChar)
						{
							// This is doubled, copy it and skip the double.
							itemValue += ch;
							// Loop will add in charSize.
							itemEnd++;
						}
						else if (!isClosingingQuote(ch, openQuote, closeQuote))
						{
							// This is an undoubled, non-closing quote, copy it.
							itemValue += ch;
						}
						else
						{
							// This is an undoubled closing quote, skip it and
							// exit the loop.
							itemEnd++;
							break;
						}
					}
				}
			}

			// Add the separated item to the array. 
			// Keep a matching old value in case it had separators.
			int foundIndex = -1;
			for (int oldChild = 1; oldChild <= arrayNode.getChildrenLength(); oldChild++)
			{
				if (itemValue.equals(arrayNode.getChild(oldChild).getValue()))
				{
					foundIndex = oldChild;
					break;
				}
			}

			XMPNode newItem = null;
			if (foundIndex < 0)
			{
				newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null);
				arrayNode.addChild(newItem);
			}			
		}
	}

	
	/**
	 * Utility to find or create the array used by <code>separateArrayItems()</code>.
	 * @param schemaNS a the namespace fo the array
	 * @param arrayName the name of the array 
	 * @param arrayOptions the options for the array if newly created
	 * @param xmp the xmp object
	 * @return Returns the array node.
	 * @throws XMPException Forwards exceptions
	 */
	private static XMPNode separateFindCreateArray(String schemaNS, String arrayName,
			PropertyOptions arrayOptions, XMPMetaImpl xmp) throws XMPException
	{
		arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null);
		if (!arrayOptions.isOnlyArrayOptions())
		{
			throw new XMPException("Options can only provide array form", XMPError.BADOPTIONS);
		}

		// Find the array node, make sure it is OK. Move the current children
		// aside, to be readded later if kept.
		XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName);
		XMPNode arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, false, null);
		if (arrayNode != null)
		{
			// The array exists, make sure the form is compatible. Zero
			// arrayForm means take what exists.
			PropertyOptions arrayForm = arrayNode.getOptions();
			if (!arrayForm.isArray() || arrayForm.isArrayAlternate())
			{
				throw new XMPException("Named property must be non-alternate array", 
					XMPError.BADXPATH);
			}
			if (arrayOptions.equalArrayTypes(arrayForm))
			{
				throw new XMPException("Mismatch of specified and existing array form",
						XMPError.BADXPATH); // *** Right error?
			}
		}
		else
		{
			// The array does not exist, try to create it.
			// don't modify the options handed into the method
			arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, true, arrayOptions
					.setArray(true));
			if (arrayNode == null)
			{
				throw new XMPException("Failed to create named array", XMPError.BADXPATH);
			}
		}
		return arrayNode;
	}


	/**
	 * @see XMPUtils#removeProperties(XMPMeta, String, String, boolean, boolean)
	 * 
	 * @param xmp
	 *            The XMP object containing the properties to be removed.
	 * 
	 * @param schemaNS
	 *            Optional schema namespace URI for the properties to be
	 *            removed.
	 * 
	 * @param propName
	 *            Optional path expression for the property to be removed.
	 * 
	 * @param doAllProperties
	 *            Option flag to control the deletion: do internal properties in
	 *            addition to external properties.
	 * @param includeAliases
	 *            Option flag to control the deletion: Include aliases in the
	 *            "named schema" case above.
	 * @throws XMPException If metadata processing fails
	 */
	public static void removeProperties(XMPMeta xmp, String schemaNS, String propName,
			boolean doAllProperties, boolean includeAliases) throws XMPException
	{
		ParameterAsserts.assertImplementation(xmp);
		XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp;

		if (propName != null && propName.length() > 0)
		{
			// Remove just the one indicated property. This might be an alias,
			// the named schema might not actually exist. So don't lookup the
			// schema node.

			if (schemaNS == null || schemaNS.length() == 0)
			{
				throw new XMPException("Property name requires schema namespace", 
					XMPError.BADPARAM);
			}

			XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName);

			XMPNode propNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), expPath, false, null);
			if (propNode != null)
			{
				if (doAllProperties
						|| !Utils.isInternalProperty(expPath.getSegment(XMPPath.STEP_SCHEMA)
								.getName(), expPath.getSegment(XMPPath.STEP_ROOT_PROP).getName()))
				{
					XMPNode parent = propNode.getParent();
					parent.removeChild(propNode);
					if (parent.getOptions().isSchemaNode()  &&  !parent.hasChildren())
					{
						// remove empty schema node
						parent.getParent().removeChild(parent);
					}
						
				}
			}
		}
		else if (schemaNS != null && schemaNS.length() > 0)
		{

			// Remove all properties from the named schema. Optionally include
			// aliases, in which case
			// there might not be an actual schema node.

			// XMP_NodePtrPos schemaPos;
			XMPNode schemaNode = XMPNodeUtils.findSchemaNode(xmpImpl.getRoot(), schemaNS, false);
			if (schemaNode != null)
			{
				if (removeSchemaChildren(schemaNode, doAllProperties))
				{
					xmpImpl.getRoot().removeChild(schemaNode);
				}
			}

			if (includeAliases)
			{
				// We're removing the aliases also. Look them up by their
				// namespace prefix.
				// But that takes more code and the extra speed isn't worth it.
				// Lookup the XMP node
				// from the alias, to make sure the actual exists.

				XMPAliasInfo[] aliases = XMPMetaFactory.getSchemaRegistry().findAliases(schemaNS);
				for (int i = 0; i < aliases.length; i++)
				{
					XMPAliasInfo info = aliases[i];
					XMPPath path = XMPPathParser.expandXPath(info.getNamespace(), info
							.getPropName());
					XMPNode actualProp = XMPNodeUtils
							.findNode(xmpImpl.getRoot(), path, false, null);
					if (actualProp != null)
					{
						XMPNode parent = actualProp.getParent();
						parent.removeChild(actualProp);
					}
				}
			}
		}
		else
		{
			// Remove all appropriate properties from all schema. In this case
			// we don't have to be
			// concerned with aliases, they are handled implicitly from the
			// actual properties.
			for (Iterator it = xmpImpl.getRoot().iterateChildren(); it.hasNext();)
			{
				XMPNode schema = (XMPNode) it.next();
				if (removeSchemaChildren(schema, doAllProperties))
				{
					it.remove();
				}
			}
		}
	}


	/**
	 * @see XMPUtils#appendProperties(XMPMeta, XMPMeta, boolean, boolean)
	 * @param source The source XMP object.
	 * @param destination The destination XMP object.
	 * @param doAllProperties Do internal properties in addition to external properties.
	 * @param replaceOldValues Replace the values of existing properties.
	 * @param deleteEmptyValues Delete destination values if source property is empty. 
	 * @throws XMPException Forwards the Exceptions from the metadata processing
	 */
	public static void appendProperties(XMPMeta source, XMPMeta destination,
			boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues) 
		throws XMPException
	{
		ParameterAsserts.assertImplementation(source);
		ParameterAsserts.assertImplementation(destination);

		XMPMetaImpl src = (XMPMetaImpl) source;
		XMPMetaImpl dest = (XMPMetaImpl) destination;

		for (Iterator it = src.getRoot().iterateChildren(); it.hasNext();)
		{
			XMPNode sourceSchema = (XMPNode) it.next();
			
			// Make sure we have a destination schema node
			XMPNode destSchema = XMPNodeUtils.findSchemaNode(dest.getRoot(),
					sourceSchema.getName(), false);
			boolean createdSchema = false;
			if (destSchema == null)
			{
				destSchema = new XMPNode(sourceSchema.getName(), sourceSchema.getValue(),
						new PropertyOptions().setSchemaNode(true));
				dest.getRoot().addChild(destSchema);
				createdSchema = true;
			}

			// Process the source schema's children.			
			for (Iterator ic = sourceSchema.iterateChildren(); ic.hasNext();)
			{
				XMPNode sourceProp = (XMPNode) ic.next();
				if (doAllProperties
						|| !Utils.isInternalProperty(sourceSchema.getName(), sourceProp.getName()))
				{
					appendSubtree(
						dest, sourceProp, destSchema, replaceOldValues, deleteEmptyValues);
				}
			}

			if (!destSchema.hasChildren()  &&  (createdSchema  ||  deleteEmptyValues))
			{
				// Don't create an empty schema / remove empty schema.
				dest.getRoot().removeChild(destSchema);
			}
		}
	}


	/**
	 * Remove all schema children according to the flag
	 * <code>doAllProperties</code>. Empty schemas are automatically remove
	 * by <code>XMPNode</code>
	 * 
	 * @param schemaNode
	 *            a schema node
	 * @param doAllProperties
	 *            flag if all properties or only externals shall be removed.
	 * @return Returns true if the schema is empty after the operation.
	 */
	private static boolean removeSchemaChildren(XMPNode schemaNode, boolean doAllProperties)
	{
		for (Iterator it = schemaNode.iterateChildren(); it.hasNext();)
		{
			XMPNode currProp = (XMPNode) it.next();
			if (doAllProperties
					|| !Utils.isInternalProperty(schemaNode.getName(), currProp.getName()))
			{
				it.remove();
			}
		}
		
		return !schemaNode.hasChildren();
	}


	/**
	 * @see XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean)
	 * @param destXMP The destination XMP object.
	 * @param sourceNode the source node
	 * @param destParent the parent of the destination node
	 * @param replaceOldValues Replace the values of existing properties.
	 * @param deleteEmptyValues flag if properties with empty values should be deleted 
	 * 		   in the destination object.
	 * @throws XMPException
	 */
	private static void appendSubtree(XMPMetaImpl destXMP, XMPNode sourceNode, XMPNode destParent,
			boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException
	{
		XMPNode destNode = XMPNodeUtils.findChildNode(destParent, sourceNode.getName(), false);

		boolean valueIsEmpty = false;
		if (deleteEmptyValues)
		{
			valueIsEmpty = sourceNode.getOptions().isSimple() ?
				sourceNode.getValue() == null  ||  sourceNode.getValue().length() == 0 :
				!sourceNode.hasChildren();
		}
		
		if (deleteEmptyValues  &&  valueIsEmpty)
		{
			if (destNode != null)
			{
				destParent.removeChild(destNode);
			}
		} 
		else if (destNode == null)
		{
			// The one easy case, the destination does not exist.
			destParent.addChild((XMPNode) sourceNode.clone());
		}
		else if (replaceOldValues)
		{
			// The destination exists and should be replaced.
			destXMP.setNode(destNode, sourceNode.getValue(), sourceNode.getOptions(), true);
			destParent.removeChild(destNode);
			destNode = (XMPNode) sourceNode.clone();
			destParent.addChild(destNode);
		}
		else
		{
			// The destination exists and is not totally replaced. Structs and
			// arrays are merged.

			PropertyOptions sourceForm = sourceNode.getOptions();
			PropertyOptions destForm = destNode.getOptions();
			if (sourceForm != destForm)
			{
				return;
			}
			if (sourceForm.isStruct())
			{
				// To merge a struct process the fields recursively. E.g. add simple missing fields.
				// The recursive call to AppendSubtree will handle deletion for fields with empty 
				// values.
				for (Iterator it = sourceNode.iterateChildren(); it.hasNext();)
				{
					XMPNode sourceField = (XMPNode) it.next();
					appendSubtree(destXMP, sourceField, destNode, 
						replaceOldValues, deleteEmptyValues);
					if (deleteEmptyValues  &&  !destNode.hasChildren())
					{
						destParent.removeChild(destNode);
					}
				}
			}
			else if (sourceForm.isArrayAltText())
			{
				// Merge AltText arrays by the "xml:lang" qualifiers. Make sure x-default is first. 
				// Make a special check for deletion of empty values. Meaningful in AltText arrays 
				// because the "xml:lang" qualifier provides unambiguous source/dest correspondence.
				for (Iterator it = sourceNode.iterateChildren(); it.hasNext();)
				{
					XMPNode sourceItem = (XMPNode) it.next();
					if (!sourceItem.hasQualifier()
							|| !XMPConst.XML_LANG.equals(sourceItem.getQualifier(1).getName()))
					{
						continue;
					}
					
					int destIndex = XMPNodeUtils.lookupLanguageItem(destNode, 
							sourceItem.getQualifier(1).getValue());
					if (deleteEmptyValues  &&  
							(sourceItem.getValue() == null  ||
							 sourceItem.getValue().length() == 0))
					{
						if (destIndex != -1)
						{
							destNode.removeChild(destIndex);
							if (!destNode.hasChildren())
							{
								destParent.removeChild(destNode);
							}
						}	
					}
					else if (destIndex == -1)
					{
						// Not replacing, keep the existing item.						
						if (!XMPConst.X_DEFAULT.equals(sourceItem.getQualifier(1).getValue())
								|| !destNode.hasChildren())
						{
							sourceItem.cloneSubtree(destNode);
						}
						else
						{
							XMPNode destItem = new XMPNode(
								sourceItem.getName(), 
								sourceItem.getValue(), 
								sourceItem.getOptions());
							sourceItem.cloneSubtree(destItem);
							destNode.addChild(1, destItem);
						}	
					}
				}				
			}
			else if (sourceForm.isArray())
			{
				// Merge other arrays by item values. Don't worry about order or duplicates. Source 
				// items with empty values do not cause deletion, that conflicts horribly with 
				// merging.

				for (Iterator is = sourceNode.iterateChildren(); is.hasNext();)
				{
					XMPNode sourceItem = (XMPNode) is.next();

					boolean match = false;
					for (Iterator id = destNode.iterateChildren(); id.hasNext();)
					{
						XMPNode destItem = (XMPNode) id.next();
						if (itemValuesMatch(sourceItem, destItem))
						{
							match = true;
						}
					}
					if (!match)
					{
						destNode = (XMPNode) sourceItem.clone();
						destParent.addChild(destNode);
					}
				}
			}
		}
	}


	/**
	 * Compares two nodes including its children and qualifier.
	 * @param leftNode an <code>XMPNode</code>
	 * @param rightNode an <code>XMPNode</code>
	 * @return Returns true if the nodes are equal, false otherwise.
	 * @throws XMPException Forwards exceptions to the calling method.
	 */
	private static boolean itemValuesMatch(XMPNode leftNode, XMPNode rightNode) throws XMPException
	{
		PropertyOptions leftForm = leftNode.getOptions();
		PropertyOptions rightForm = rightNode.getOptions();

		if (leftForm.equals(rightForm))
		{
			return false;
		}

		if (leftForm.getOptions() == 0)
		{
			// Simple nodes, check the values and xml:lang qualifiers.
			if (!leftNode.getValue().equals(rightNode.getValue()))
			{
				return false;
			}
			if (leftNode.getOptions().getHasLanguage() != rightNode.getOptions().getHasLanguage())
			{
				return false;
			}
			if (leftNode.getOptions().getHasLanguage()
					&& !leftNode.getQualifier(1).getValue().equals(
							rightNode.getQualifier(1).getValue()))
			{
				return false;
			}
		}
		else if (leftForm.isStruct())
		{
			// Struct nodes, see if all fields match, ignoring order.

			if (leftNode.getChildrenLength() != rightNode.getChildrenLength())
			{
				return false;
			}

			for (Iterator it = leftNode.iterateChildren(); it.hasNext();)
			{
				XMPNode leftField = (XMPNode) it.next();
				XMPNode rightField = XMPNodeUtils.findChildNode(rightNode, leftField.getName(),
						false);
				if (rightField == null || !itemValuesMatch(leftField, rightField))
				{
					return false;
				}
			}
		}
		else
		{
			// Array nodes, see if the "leftNode" values are present in the
			// "rightNode", ignoring order, duplicates,
			// and extra values in the rightNode-> The rightNode is the
			// destination for AppendProperties.

			assert leftForm.isArray();

			for (Iterator il = leftNode.iterateChildren(); il.hasNext();)
			{
				XMPNode leftItem = (XMPNode) il.next();

				boolean match = false;
				for (Iterator ir = rightNode.iterateChildren(); ir.hasNext();)
				{
					XMPNode rightItem = (XMPNode) ir.next();
					if (itemValuesMatch(leftItem, rightItem))
					{
						match = true;
						break;
					}
				}
				if (!match)
				{
					return false;
				}
			}
		}
		return true; // All of the checks passed.
	}


	/**
	 * Make sure the separator is OK. It must be one semicolon surrounded by
	 * zero or more spaces. Any of the recognized semicolons or spaces are
	 * allowed.
	 * 
	 * @param separator
	 * @throws XMPException
	 */
	private static void checkSeparator(String separator) throws XMPException
	{
		boolean haveSemicolon = false;
		for (int i = 0; i < separator.length(); i++)
		{
			int charKind = classifyCharacter(separator.charAt(i));
			if (charKind == UCK_SEMICOLON)
			{
				if (haveSemicolon)
				{
					throw new XMPException("Separator can have only one semicolon", 
						XMPError.BADPARAM);
				}
				haveSemicolon = true;
			}
			else if (charKind != UCK_SPACE)
			{
				throw new XMPException("Separator can have only spaces and one semicolon",
						XMPError.BADPARAM);
			}
		}
		if (!haveSemicolon)
		{
			throw new XMPException("Separator must have one semicolon", XMPError.BADPARAM);
		}
	}


	/**
	 * Make sure the open and close quotes are a legitimate pair and return the
	 * correct closing quote or an exception.
	 * 
	 * @param quotes
	 *            opened and closing quote in a string
	 * @param openQuote
	 *            the open quote
	 * @return Returns a corresponding closing quote.
	 * @throws XMPException
	 */
	private static char checkQuotes(String quotes, char openQuote) throws XMPException
	{
		char closeQuote;

		int charKind = classifyCharacter(openQuote);
		if (charKind != UCK_QUOTE)
		{
			throw new XMPException("Invalid quoting character", XMPError.BADPARAM);
		}

		if (quotes.length() == 1)
		{
			closeQuote = openQuote;
		}
		else
		{
			closeQuote = quotes.charAt(1);
			charKind = classifyCharacter(closeQuote);
			if (charKind != UCK_QUOTE)
			{
				throw new XMPException("Invalid quoting character", XMPError.BADPARAM);
			}
		}

		if (closeQuote != getClosingQuote(openQuote))
		{
			throw new XMPException("Mismatched quote pair", XMPError.BADPARAM);
		}
		return closeQuote;
	}


	/**
	 * Classifies the character into normal chars, spaces, semicola, quotes,
	 * control chars.
	 * 
	 * @param ch
	 *            a char
	 * @return Return the character kind.
	 */
	private static int classifyCharacter(char ch)
	{
		if (SPACES.indexOf(ch) >= 0 || (0x2000 <= ch && ch <= 0x200B))
		{
			return UCK_SPACE;
		}
		else if (COMMAS.indexOf(ch) >= 0)
		{
			return UCK_COMMA;
		}
		else if (SEMICOLA.indexOf(ch) >= 0)
		{
			return UCK_SEMICOLON;
		}
		else if (QUOTES.indexOf(ch) >= 0 || (0x3008 <= ch && ch <= 0x300F)
				|| (0x2018 <= ch && ch <= 0x201F))
		{
			return UCK_QUOTE;
		}
		else if (ch < 0x0020 || CONTROLS.indexOf(ch) >= 0)
		{
			return UCK_CONTROL;
		}
		else
		{
			// Assume typical case.
			return UCK_NORMAL;
		}
	}


	/**
	 * @param openQuote
	 *            the open quote char
	 * @return Returns the matching closing quote for an open quote.
	 */
	private static char getClosingQuote(char openQuote)
	{
		switch (openQuote)
		{
		case 0x0022:
			return 0x0022; // ! U+0022 is both opening and closing.
		case 0x005B:
			return 0x005D;
		case 0x00AB:
			return 0x00BB; // ! U+00AB and U+00BB are reversible.
		case 0x00BB:
			return 0x00AB;
		case 0x2015:
			return 0x2015; // ! U+2015 is both opening and closing.
		case 0x2018:
			return 0x2019;
		case 0x201A:
			return 0x201B;
		case 0x201C:
			return 0x201D;
		case 0x201E:
			return 0x201F;
		case 0x2039:
			return 0x203A; // ! U+2039 and U+203A are reversible.
		case 0x203A:
			return 0x2039;
		case 0x3008:
			return 0x3009;
		case 0x300A:
			return 0x300B;
		case 0x300C:
			return 0x300D;
		case 0x300E:
			return 0x300F;
		case 0x301D:
			return 0x301F; // ! U+301E also closes U+301D.
		default:
			return 0;
		}
	}


	/**
	 * Add quotes to the item.
	 * 
	 * @param item
	 *            the array item
	 * @param openQuote
	 *            the open quote character
	 * @param closeQuote
	 *            the closing quote character
	 * @param allowCommas
	 *            flag if commas are allowed
	 * @return Returns the value in quotes.
	 */
	private static String applyQuotes(String item, char openQuote, char closeQuote,
			boolean allowCommas)
	{
		if (item == null)
		{
			item = "";
		}
		
		boolean prevSpace = false;
		int charOffset;
		int charKind;

		// See if there are any separators in the value. Stop at the first
		// occurrance. This is a bit
		// tricky in order to make typical typing work conveniently. The purpose
		// of applying quotes
		// is to preserve the values when splitting them back apart. That is
		// CatenateContainerItems
		// and SeparateContainerItems must round trip properly. For the most
		// part we only look for
		// separators here. Internal quotes, as in -- Irving "Bud" Jones --
		// won't cause problems in
		// the separation. An initial quote will though, it will make the value
		// look quoted.

		int i;
		for (i = 0; i < item.length(); i++)
		{
			char ch = item.charAt(i);
			charKind = classifyCharacter(ch);
			if (i == 0 && charKind == UCK_QUOTE)
			{
				break;
			}

			if (charKind == UCK_SPACE)
			{
				// Multiple spaces are a separator.
				if (prevSpace)
				{
					break;
				}
				prevSpace = true;
			}
			else
			{
				prevSpace = false;
				if ((charKind == UCK_SEMICOLON || charKind == UCK_CONTROL)
						|| (charKind == UCK_COMMA && !allowCommas))
				{
					break;
				}
			}
		}


		if (i < item.length())
		{
			// Create a quoted copy, doubling any internal quotes that match the
			// outer ones. Internal quotes did not stop the "needs quoting"
			// search, but they do need
			// doubling. So we have to rescan the front of the string for
			// quotes. Handle the special
			// case of U+301D being closed by either U+301E or U+301F.

			StringBuffer newItem = new StringBuffer(item.length() + 2);
			int splitPoint;
			for (splitPoint = 0; splitPoint <= i; splitPoint++)
			{
				if (classifyCharacter(item.charAt(i)) == UCK_QUOTE)
				{
					break;
				}
			}

			// Copy the leading "normal" portion.
			newItem.append(openQuote).append(item.substring(0, splitPoint));

			for (charOffset = splitPoint; charOffset < item.length(); charOffset++)
			{
				newItem.append(item.charAt(charOffset));
				if (classifyCharacter(item.charAt(charOffset)) == UCK_QUOTE
						&& isSurroundingQuote(item.charAt(charOffset), openQuote, closeQuote))
				{
					newItem.append(item.charAt(charOffset));
				}
			}

			newItem.append(closeQuote);

			item = newItem.toString();
		}

		return item;
	}


	/**
	 * @param ch a character
	 * @param openQuote the opening quote char
	 * @param closeQuote the closing quote char 
	 * @return Return it the character is a surrounding quote.
	 */
	private static boolean isSurroundingQuote(char ch, char openQuote, char closeQuote)
	{
		return ch == openQuote || isClosingingQuote(ch, openQuote, closeQuote);
	}


	/**
	 * @param ch a character
	 * @param openQuote the opening quote char
	 * @param closeQuote the closing quote char 
	 * @return Returns true if the character is a closing quote.
	 */
	private static boolean isClosingingQuote(char ch, char openQuote, char closeQuote)
	{
		return ch == closeQuote || (openQuote == 0x301D && ch == 0x301E || ch == 0x301F);
	}
	
	
	
	/**
	 * U+0022 ASCII space<br>
	 * U+3000, ideographic space<br>
	 * U+303F, ideographic half fill space<br>
	 * U+2000..U+200B, en quad through zero width space
	 */
	private static final String SPACES = "\u0020\u3000\u303F";
	/**
	 * U+002C, ASCII comma<br>
	 * U+FF0C, full width comma<br>
	 * U+FF64, half width ideographic comma<br>
	 * U+FE50, small comma<br>
	 * U+FE51, small ideographic comma<br>
	 * U+3001, ideographic comma<br>
	 * U+060C, Arabic comma<br>
	 * U+055D, Armenian comma
	 */
	private static final String COMMAS = "\u002C\uFF0C\uFF64\uFE50\uFE51\u3001\u060C\u055D";
	/**
	 * U+003B, ASCII semicolon<br>
	 * U+FF1B, full width semicolon<br>
	 * U+FE54, small semicolon<br>
	 * U+061B, Arabic semicolon<br>
	 * U+037E, Greek "semicolon" (really a question mark)
	 */
	private static final String SEMICOLA = "\u003B\uFF1B\uFE54\u061B\u037E";
	/**
	 * U+0022 ASCII quote<br>
	 * ASCII '[' (0x5B) and ']' (0x5D) are used as quotes in Chinese and
	 * Korean.<br>
	 * U+00AB and U+00BB, guillemet quotes<br>
	 * U+3008..U+300F, various quotes.<br>
	 * U+301D..U+301F, double prime quotes.<br>
	 * U+2015, dash quote.<br>
	 * U+2018..U+201F, various quotes.<br>
	 * U+2039 and U+203A, guillemet quotes.
	 */
	private static final String QUOTES = 
		"\"\u005B\u005D\u00AB\u00BB\u301D\u301E\u301F\u2015\u2039\u203A";
	/**
	 * U+0000..U+001F ASCII controls<br>
	 * U+2028, line separator.<br>
	 * U+2029, paragraph separator.
	 */
	private static final String CONTROLS = "\u2028\u2029";	
}