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 10 11 12 package com.adobe.xmp.impl; 13 14 import java.util.Iterator; 15 16 import com.adobe.xmp.XMPConst; 17 import com.adobe.xmp.XMPError; 18 import com.adobe.xmp.XMPException; 19 import com.adobe.xmp.XMPMeta; 20 import com.adobe.xmp.XMPMetaFactory; 21 import com.adobe.xmp.XMPUtils; 22 import com.adobe.xmp.impl.xpath.XMPPath; 23 import com.adobe.xmp.impl.xpath.XMPPathParser; 24 import com.adobe.xmp.options.PropertyOptions; 25 import com.adobe.xmp.properties.XMPAliasInfo; 26 27 28 29 /** 30 * @since 11.08.2006 31 */ 32 public class XMPUtilsImpl implements XMPConst 33 { 34 /** */ 35 private static final int UCK_NORMAL = 0; 36 /** */ 37 private static final int UCK_SPACE = 1; 38 /** */ 39 private static final int UCK_COMMA = 2; 40 /** */ 41 private static final int UCK_SEMICOLON = 3; 42 /** */ 43 private static final int UCK_QUOTE = 4; 44 /** */ 45 private static final int UCK_CONTROL = 5; 46 47 48 /** 49 * Private constructor, as 50 */ 51 private XMPUtilsImpl() 52 { 53 // EMPTY 54 } 55 56 57 /** 58 * @see XMPUtils#catenateArrayItems(XMPMeta, String, String, String, String, 59 * boolean) 60 * 61 * @param xmp 62 * The XMP object containing the array to be catenated. 63 * @param schemaNS 64 * The schema namespace URI for the array. Must not be null or 65 * the empty string. 66 * @param arrayName 67 * The name of the array. May be a general path expression, must 68 * not be null or the empty string. Each item in the array must 69 * be a simple string value. 70 * @param separator 71 * The string to be used to separate the items in the catenated 72 * string. Defaults to "; ", ASCII semicolon and space 73 * (U+003B, U+0020). 74 * @param quotes 75 * The characters to be used as quotes around array items that 76 * contain a separator. Defaults to '"' 77 * @param allowCommas 78 * Option flag to control the catenation. 79 * @return Returns the string containing the catenated array items. 80 * @throws XMPException 81 * Forwards the Exceptions from the metadata processing 82 */ 83 public static String catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, 84 String separator, String quotes, boolean allowCommas) throws XMPException 85 { 86 ParameterAsserts.assertSchemaNS(schemaNS); 87 ParameterAsserts.assertArrayName(arrayName); 88 ParameterAsserts.assertImplementation(xmp); 89 if (separator == null || separator.length() == 0) 90 { 91 separator = "; "; 92 } 93 if (quotes == null || quotes.length() == 0) 94 { 95 quotes = "\""; 96 } 97 98 XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; 99 XMPNode arrayNode = null; 100 XMPNode currItem = null; 101 102 // Return an empty result if the array does not exist, 103 // hurl if it isn't the right form. 104 XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); 105 arrayNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), arrayPath, false, null); 106 if (arrayNode == null) 107 { 108 return ""; 109 } 110 else if (!arrayNode.getOptions().isArray() || arrayNode.getOptions().isArrayAlternate()) 111 { 112 throw new XMPException("Named property must be non-alternate array", XMPError.BADPARAM); 113 } 114 115 // Make sure the separator is OK. 116 checkSeparator(separator); 117 // Make sure the open and close quotes are a legitimate pair. 118 char openQuote = quotes.charAt(0); 119 char closeQuote = checkQuotes(quotes, openQuote); 120 121 // Build the result, quoting the array items, adding separators. 122 // Hurl if any item isn't simple. 123 124 StringBuffer catinatedString = new StringBuffer(); 125 126 for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) 127 { 128 currItem = (XMPNode) it.next(); 129 if (currItem.getOptions().isCompositeProperty()) 130 { 131 throw new XMPException("Array items must be simple", XMPError.BADPARAM); 132 } 133 String str = applyQuotes(currItem.getValue(), openQuote, closeQuote, allowCommas); 134 135 catinatedString.append(str); 136 if (it.hasNext()) 137 { 138 catinatedString.append(separator); 139 } 140 } 141 142 return catinatedString.toString(); 143 } 144 145 146 /** 147 * see {@link XMPUtils#separateArrayItems(XMPMeta, String, String, String, 148 * PropertyOptions, boolean)} 149 * 150 * @param xmp 151 * The XMP object containing the array to be updated. 152 * @param schemaNS 153 * The schema namespace URI for the array. Must not be null or 154 * the empty string. 155 * @param arrayName 156 * The name of the array. May be a general path expression, must 157 * not be null or the empty string. Each item in the array must 158 * be a simple string value. 159 * @param catedStr 160 * The string to be separated into the array items. 161 * @param arrayOptions 162 * Option flags to control the separation. 163 * @param preserveCommas 164 * Flag if commas shall be preserved 165 * 166 * @throws XMPException 167 * Forwards the Exceptions from the metadata processing 168 */ 169 public static void separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, 170 String catedStr, PropertyOptions arrayOptions, boolean preserveCommas) 171 throws XMPException 172 { 173 ParameterAsserts.assertSchemaNS(schemaNS); 174 ParameterAsserts.assertArrayName(arrayName); 175 if (catedStr == null) 176 { 177 throw new XMPException("Parameter must not be null", XMPError.BADPARAM); 178 } 179 ParameterAsserts.assertImplementation(xmp); 180 XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; 181 182 // Keep a zero value, has special meaning below. 183 XMPNode arrayNode = separateFindCreateArray(schemaNS, arrayName, arrayOptions, xmpImpl); 184 185 // Extract the item values one at a time, until the whole input string is done. 186 String itemValue; 187 int itemStart, itemEnd; 188 int nextKind = UCK_NORMAL, charKind = UCK_NORMAL; 189 char ch = 0, nextChar = 0; 190 191 itemEnd = 0; 192 int endPos = catedStr.length(); 193 while (itemEnd < endPos) 194 { 195 // Skip any leading spaces and separation characters. Always skip commas here. 196 // They can be kept when within a value, but not when alone between values. 197 for (itemStart = itemEnd; itemStart < endPos; itemStart++) 198 { 199 ch = catedStr.charAt(itemStart); 200 charKind = classifyCharacter(ch); 201 if (charKind == UCK_NORMAL || charKind == UCK_QUOTE) 202 { 203 break; 204 } 205 } 206 if (itemStart >= endPos) 207 { 208 break; 209 } 210 211 if (charKind != UCK_QUOTE) 212 { 213 // This is not a quoted value. Scan for the end, create an array 214 // item from the substring. 215 for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) 216 { 217 ch = catedStr.charAt(itemEnd); 218 charKind = classifyCharacter(ch); 219 220 if (charKind == UCK_NORMAL || charKind == UCK_QUOTE || 221 (charKind == UCK_COMMA && preserveCommas)) 222 { 223 continue; 224 } 225 else if (charKind != UCK_SPACE) 226 { 227 break; 228 } 229 else if ((itemEnd + 1) < endPos) 230 { 231 ch = catedStr.charAt(itemEnd + 1); 232 nextKind = classifyCharacter(ch); 233 if (nextKind == UCK_NORMAL || nextKind == UCK_QUOTE || 234 (nextKind == UCK_COMMA && preserveCommas)) 235 { 236 continue; 237 } 238 } 239 240 // Anything left? 241 break; // Have multiple spaces, or a space followed by a 242 // separator. 243 } 244 itemValue = catedStr.substring(itemStart, itemEnd); 245 } 246 else 247 { 248 // Accumulate quoted values into a local string, undoubling 249 // internal quotes that 250 // match the surrounding quotes. Do not undouble "unmatching" 251 // quotes. 252 253 char openQuote = ch; 254 char closeQuote = getClosingQuote(openQuote); 255 256 itemStart++; // Skip the opening quote; 257 itemValue = ""; 258 259 for (itemEnd = itemStart; itemEnd < endPos; itemEnd++) 260 { 261 ch = catedStr.charAt(itemEnd); 262 charKind = classifyCharacter(ch); 263 264 if (charKind != UCK_QUOTE || !isSurroundingQuote(ch, openQuote, closeQuote)) 265 { 266 // This is not a matching quote, just append it to the 267 // item value. 268 itemValue += ch; 269 } 270 else 271 { 272 // This is a "matching" quote. Is it doubled, or the 273 // final closing quote? 274 // Tolerate various edge cases like undoubled opening 275 // (non-closing) quotes, 276 // or end of input. 277 278 if ((itemEnd + 1) < endPos) 279 { 280 nextChar = catedStr.charAt(itemEnd + 1); 281 nextKind = classifyCharacter(nextChar); 282 } 283 else 284 { 285 nextKind = UCK_SEMICOLON; 286 nextChar = 0x3B; 287 } 288 289 if (ch == nextChar) 290 { 291 // This is doubled, copy it and skip the double. 292 itemValue += ch; 293 // Loop will add in charSize. 294 itemEnd++; 295 } 296 else if (!isClosingingQuote(ch, openQuote, closeQuote)) 297 { 298 // This is an undoubled, non-closing quote, copy it. 299 itemValue += ch; 300 } 301 else 302 { 303 // This is an undoubled closing quote, skip it and 304 // exit the loop. 305 itemEnd++; 306 break; 307 } 308 } 309 } 310 } 311 312 // Add the separated item to the array. 313 // Keep a matching old value in case it had separators. 314 int foundIndex = -1; 315 for (int oldChild = 1; oldChild <= arrayNode.getChildrenLength(); oldChild++) 316 { 317 if (itemValue.equals(arrayNode.getChild(oldChild).getValue())) 318 { 319 foundIndex = oldChild; 320 break; 321 } 322 } 323 324 XMPNode newItem = null; 325 if (foundIndex < 0) 326 { 327 newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null); 328 arrayNode.addChild(newItem); 329 } 330 } 331 } 332 333 334 /** 335 * Utility to find or create the array used by <code>separateArrayItems()</code>. 336 * @param schemaNS a the namespace fo the array 337 * @param arrayName the name of the array 338 * @param arrayOptions the options for the array if newly created 339 * @param xmp the xmp object 340 * @return Returns the array node. 341 * @throws XMPException Forwards exceptions 342 */ 343 private static XMPNode separateFindCreateArray(String schemaNS, String arrayName, 344 PropertyOptions arrayOptions, XMPMetaImpl xmp) throws XMPException 345 { 346 arrayOptions = XMPNodeUtils.verifySetOptions(arrayOptions, null); 347 if (!arrayOptions.isOnlyArrayOptions()) 348 { 349 throw new XMPException("Options can only provide array form", XMPError.BADOPTIONS); 350 } 351 352 // Find the array node, make sure it is OK. Move the current children 353 // aside, to be readded later if kept. 354 XMPPath arrayPath = XMPPathParser.expandXPath(schemaNS, arrayName); 355 XMPNode arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, false, null); 356 if (arrayNode != null) 357 { 358 // The array exists, make sure the form is compatible. Zero 359 // arrayForm means take what exists. 360 PropertyOptions arrayForm = arrayNode.getOptions(); 361 if (!arrayForm.isArray() || arrayForm.isArrayAlternate()) 362 { 363 throw new XMPException("Named property must be non-alternate array", 364 XMPError.BADXPATH); 365 } 366 if (arrayOptions.equalArrayTypes(arrayForm)) 367 { 368 throw new XMPException("Mismatch of specified and existing array form", 369 XMPError.BADXPATH); // *** Right error? 370 } 371 } 372 else 373 { 374 // The array does not exist, try to create it. 375 // don't modify the options handed into the method 376 arrayNode = XMPNodeUtils.findNode(xmp.getRoot(), arrayPath, true, arrayOptions 377 .setArray(true)); 378 if (arrayNode == null) 379 { 380 throw new XMPException("Failed to create named array", XMPError.BADXPATH); 381 } 382 } 383 return arrayNode; 384 } 385 386 387 /** 388 * @see XMPUtils#removeProperties(XMPMeta, String, String, boolean, boolean) 389 * 390 * @param xmp 391 * The XMP object containing the properties to be removed. 392 * 393 * @param schemaNS 394 * Optional schema namespace URI for the properties to be 395 * removed. 396 * 397 * @param propName 398 * Optional path expression for the property to be removed. 399 * 400 * @param doAllProperties 401 * Option flag to control the deletion: do internal properties in 402 * addition to external properties. 403 * @param includeAliases 404 * Option flag to control the deletion: Include aliases in the 405 * "named schema" case above. 406 * @throws XMPException If metadata processing fails 407 */ 408 public static void removeProperties(XMPMeta xmp, String schemaNS, String propName, 409 boolean doAllProperties, boolean includeAliases) throws XMPException 410 { 411 ParameterAsserts.assertImplementation(xmp); 412 XMPMetaImpl xmpImpl = (XMPMetaImpl) xmp; 413 414 if (propName != null && propName.length() > 0) 415 { 416 // Remove just the one indicated property. This might be an alias, 417 // the named schema might not actually exist. So don't lookup the 418 // schema node. 419 420 if (schemaNS == null || schemaNS.length() == 0) 421 { 422 throw new XMPException("Property name requires schema namespace", 423 XMPError.BADPARAM); 424 } 425 426 XMPPath expPath = XMPPathParser.expandXPath(schemaNS, propName); 427 428 XMPNode propNode = XMPNodeUtils.findNode(xmpImpl.getRoot(), expPath, false, null); 429 if (propNode != null) 430 { 431 if (doAllProperties 432 || !Utils.isInternalProperty(expPath.getSegment(XMPPath.STEP_SCHEMA) 433 .getName(), expPath.getSegment(XMPPath.STEP_ROOT_PROP).getName())) 434 { 435 XMPNode parent = propNode.getParent(); 436 parent.removeChild(propNode); 437 if (parent.getOptions().isSchemaNode() && !parent.hasChildren()) 438 { 439 // remove empty schema node 440 parent.getParent().removeChild(parent); 441 } 442 443 } 444 } 445 } 446 else if (schemaNS != null && schemaNS.length() > 0) 447 { 448 449 // Remove all properties from the named schema. Optionally include 450 // aliases, in which case 451 // there might not be an actual schema node. 452 453 // XMP_NodePtrPos schemaPos; 454 XMPNode schemaNode = XMPNodeUtils.findSchemaNode(xmpImpl.getRoot(), schemaNS, false); 455 if (schemaNode != null) 456 { 457 if (removeSchemaChildren(schemaNode, doAllProperties)) 458 { 459 xmpImpl.getRoot().removeChild(schemaNode); 460 } 461 } 462 463 if (includeAliases) 464 { 465 // We're removing the aliases also. Look them up by their 466 // namespace prefix. 467 // But that takes more code and the extra speed isn't worth it. 468 // Lookup the XMP node 469 // from the alias, to make sure the actual exists. 470 471 XMPAliasInfo[] aliases = XMPMetaFactory.getSchemaRegistry().findAliases(schemaNS); 472 for (int i = 0; i < aliases.length; i++) 473 { 474 XMPAliasInfo info = aliases[i]; 475 XMPPath path = XMPPathParser.expandXPath(info.getNamespace(), info 476 .getPropName()); 477 XMPNode actualProp = XMPNodeUtils 478 .findNode(xmpImpl.getRoot(), path, false, null); 479 if (actualProp != null) 480 { 481 XMPNode parent = actualProp.getParent(); 482 parent.removeChild(actualProp); 483 } 484 } 485 } 486 } 487 else 488 { 489 // Remove all appropriate properties from all schema. In this case 490 // we don't have to be 491 // concerned with aliases, they are handled implicitly from the 492 // actual properties. 493 for (Iterator it = xmpImpl.getRoot().iterateChildren(); it.hasNext();) 494 { 495 XMPNode schema = (XMPNode) it.next(); 496 if (removeSchemaChildren(schema, doAllProperties)) 497 { 498 it.remove(); 499 } 500 } 501 } 502 } 503 504 505 /** 506 * @see XMPUtils#appendProperties(XMPMeta, XMPMeta, boolean, boolean) 507 * @param source The source XMP object. 508 * @param destination The destination XMP object. 509 * @param doAllProperties Do internal properties in addition to external properties. 510 * @param replaceOldValues Replace the values of existing properties. 511 * @param deleteEmptyValues Delete destination values if source property is empty. 512 * @throws XMPException Forwards the Exceptions from the metadata processing 513 */ 514 public static void appendProperties(XMPMeta source, XMPMeta destination, 515 boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues) 516 throws XMPException 517 { 518 ParameterAsserts.assertImplementation(source); 519 ParameterAsserts.assertImplementation(destination); 520 521 XMPMetaImpl src = (XMPMetaImpl) source; 522 XMPMetaImpl dest = (XMPMetaImpl) destination; 523 524 for (Iterator it = src.getRoot().iterateChildren(); it.hasNext();) 525 { 526 XMPNode sourceSchema = (XMPNode) it.next(); 527 528 // Make sure we have a destination schema node 529 XMPNode destSchema = XMPNodeUtils.findSchemaNode(dest.getRoot(), 530 sourceSchema.getName(), false); 531 boolean createdSchema = false; 532 if (destSchema == null) 533 { 534 destSchema = new XMPNode(sourceSchema.getName(), sourceSchema.getValue(), 535 new PropertyOptions().setSchemaNode(true)); 536 dest.getRoot().addChild(destSchema); 537 createdSchema = true; 538 } 539 540 // Process the source schema's children. 541 for (Iterator ic = sourceSchema.iterateChildren(); ic.hasNext();) 542 { 543 XMPNode sourceProp = (XMPNode) ic.next(); 544 if (doAllProperties 545 || !Utils.isInternalProperty(sourceSchema.getName(), sourceProp.getName())) 546 { 547 appendSubtree( 548 dest, sourceProp, destSchema, replaceOldValues, deleteEmptyValues); 549 } 550 } 551 552 if (!destSchema.hasChildren() && (createdSchema || deleteEmptyValues)) 553 { 554 // Don't create an empty schema / remove empty schema. 555 dest.getRoot().removeChild(destSchema); 556 } 557 } 558 } 559 560 561 /** 562 * Remove all schema children according to the flag 563 * <code>doAllProperties</code>. Empty schemas are automatically remove 564 * by <code>XMPNode</code> 565 * 566 * @param schemaNode 567 * a schema node 568 * @param doAllProperties 569 * flag if all properties or only externals shall be removed. 570 * @return Returns true if the schema is empty after the operation. 571 */ 572 private static boolean removeSchemaChildren(XMPNode schemaNode, boolean doAllProperties) 573 { 574 for (Iterator it = schemaNode.iterateChildren(); it.hasNext();) 575 { 576 XMPNode currProp = (XMPNode) it.next(); 577 if (doAllProperties 578 || !Utils.isInternalProperty(schemaNode.getName(), currProp.getName())) 579 { 580 it.remove(); 581 } 582 } 583 584 return !schemaNode.hasChildren(); 585 } 586 587 588 /** 589 * @see XMPUtilsImpl#appendProperties(XMPMeta, XMPMeta, boolean, boolean, boolean) 590 * @param destXMP The destination XMP object. 591 * @param sourceNode the source node 592 * @param destParent the parent of the destination node 593 * @param replaceOldValues Replace the values of existing properties. 594 * @param deleteEmptyValues flag if properties with empty values should be deleted 595 * in the destination object. 596 * @throws XMPException 597 */ 598 private static void appendSubtree(XMPMetaImpl destXMP, XMPNode sourceNode, XMPNode destParent, 599 boolean replaceOldValues, boolean deleteEmptyValues) throws XMPException 600 { 601 XMPNode destNode = XMPNodeUtils.findChildNode(destParent, sourceNode.getName(), false); 602 603 boolean valueIsEmpty = false; 604 if (deleteEmptyValues) 605 { 606 valueIsEmpty = sourceNode.getOptions().isSimple() ? 607 sourceNode.getValue() == null || sourceNode.getValue().length() == 0 : 608 !sourceNode.hasChildren(); 609 } 610 611 if (deleteEmptyValues && valueIsEmpty) 612 { 613 if (destNode != null) 614 { 615 destParent.removeChild(destNode); 616 } 617 } 618 else if (destNode == null) 619 { 620 // The one easy case, the destination does not exist. 621 destParent.addChild((XMPNode) sourceNode.clone()); 622 } 623 else if (replaceOldValues) 624 { 625 // The destination exists and should be replaced. 626 destXMP.setNode(destNode, sourceNode.getValue(), sourceNode.getOptions(), true); 627 destParent.removeChild(destNode); 628 destNode = (XMPNode) sourceNode.clone(); 629 destParent.addChild(destNode); 630 } 631 else 632 { 633 // The destination exists and is not totally replaced. Structs and 634 // arrays are merged. 635 636 PropertyOptions sourceForm = sourceNode.getOptions(); 637 PropertyOptions destForm = destNode.getOptions(); 638 if (sourceForm != destForm) 639 { 640 return; 641 } 642 if (sourceForm.isStruct()) 643 { 644 // To merge a struct process the fields recursively. E.g. add simple missing fields. 645 // The recursive call to AppendSubtree will handle deletion for fields with empty 646 // values. 647 for (Iterator it = sourceNode.iterateChildren(); it.hasNext();) 648 { 649 XMPNode sourceField = (XMPNode) it.next(); 650 appendSubtree(destXMP, sourceField, destNode, 651 replaceOldValues, deleteEmptyValues); 652 if (deleteEmptyValues && !destNode.hasChildren()) 653 { 654 destParent.removeChild(destNode); 655 } 656 } 657 } 658 else if (sourceForm.isArrayAltText()) 659 { 660 // Merge AltText arrays by the "xml:lang" qualifiers. Make sure x-default is first. 661 // Make a special check for deletion of empty values. Meaningful in AltText arrays 662 // because the "xml:lang" qualifier provides unambiguous source/dest correspondence. 663 for (Iterator it = sourceNode.iterateChildren(); it.hasNext();) 664 { 665 XMPNode sourceItem = (XMPNode) it.next(); 666 if (!sourceItem.hasQualifier() 667 || !XMPConst.XML_LANG.equals(sourceItem.getQualifier(1).getName())) 668 { 669 continue; 670 } 671 672 int destIndex = XMPNodeUtils.lookupLanguageItem(destNode, 673 sourceItem.getQualifier(1).getValue()); 674 if (deleteEmptyValues && 675 (sourceItem.getValue() == null || 676 sourceItem.getValue().length() == 0)) 677 { 678 if (destIndex != -1) 679 { 680 destNode.removeChild(destIndex); 681 if (!destNode.hasChildren()) 682 { 683 destParent.removeChild(destNode); 684 } 685 } 686 } 687 else if (destIndex == -1) 688 { 689 // Not replacing, keep the existing item. 690 if (!XMPConst.X_DEFAULT.equals(sourceItem.getQualifier(1).getValue()) 691 || !destNode.hasChildren()) 692 { 693 sourceItem.cloneSubtree(destNode); 694 } 695 else 696 { 697 XMPNode destItem = new XMPNode( 698 sourceItem.getName(), 699 sourceItem.getValue(), 700 sourceItem.getOptions()); 701 sourceItem.cloneSubtree(destItem); 702 destNode.addChild(1, destItem); 703 } 704 } 705 } 706 } 707 else if (sourceForm.isArray()) 708 { 709 // Merge other arrays by item values. Don't worry about order or duplicates. Source 710 // items with empty values do not cause deletion, that conflicts horribly with 711 // merging. 712 713 for (Iterator is = sourceNode.iterateChildren(); is.hasNext();) 714 { 715 XMPNode sourceItem = (XMPNode) is.next(); 716 717 boolean match = false; 718 for (Iterator id = destNode.iterateChildren(); id.hasNext();) 719 { 720 XMPNode destItem = (XMPNode) id.next(); 721 if (itemValuesMatch(sourceItem, destItem)) 722 { 723 match = true; 724 } 725 } 726 if (!match) 727 { 728 destNode = (XMPNode) sourceItem.clone(); 729 destParent.addChild(destNode); 730 } 731 } 732 } 733 } 734 } 735 736 737 /** 738 * Compares two nodes including its children and qualifier. 739 * @param leftNode an <code>XMPNode</code> 740 * @param rightNode an <code>XMPNode</code> 741 * @return Returns true if the nodes are equal, false otherwise. 742 * @throws XMPException Forwards exceptions to the calling method. 743 */ 744 private static boolean itemValuesMatch(XMPNode leftNode, XMPNode rightNode) throws XMPException 745 { 746 PropertyOptions leftForm = leftNode.getOptions(); 747 PropertyOptions rightForm = rightNode.getOptions(); 748 749 if (leftForm.equals(rightForm)) 750 { 751 return false; 752 } 753 754 if (leftForm.getOptions() == 0) 755 { 756 // Simple nodes, check the values and xml:lang qualifiers. 757 if (!leftNode.getValue().equals(rightNode.getValue())) 758 { 759 return false; 760 } 761 if (leftNode.getOptions().getHasLanguage() != rightNode.getOptions().getHasLanguage()) 762 { 763 return false; 764 } 765 if (leftNode.getOptions().getHasLanguage() 766 && !leftNode.getQualifier(1).getValue().equals( 767 rightNode.getQualifier(1).getValue())) 768 { 769 return false; 770 } 771 } 772 else if (leftForm.isStruct()) 773 { 774 // Struct nodes, see if all fields match, ignoring order. 775 776 if (leftNode.getChildrenLength() != rightNode.getChildrenLength()) 777 { 778 return false; 779 } 780 781 for (Iterator it = leftNode.iterateChildren(); it.hasNext();) 782 { 783 XMPNode leftField = (XMPNode) it.next(); 784 XMPNode rightField = XMPNodeUtils.findChildNode(rightNode, leftField.getName(), 785 false); 786 if (rightField == null || !itemValuesMatch(leftField, rightField)) 787 { 788 return false; 789 } 790 } 791 } 792 else 793 { 794 // Array nodes, see if the "leftNode" values are present in the 795 // "rightNode", ignoring order, duplicates, 796 // and extra values in the rightNode-> The rightNode is the 797 // destination for AppendProperties. 798 799 assert leftForm.isArray(); 800 801 for (Iterator il = leftNode.iterateChildren(); il.hasNext();) 802 { 803 XMPNode leftItem = (XMPNode) il.next(); 804 805 boolean match = false; 806 for (Iterator ir = rightNode.iterateChildren(); ir.hasNext();) 807 { 808 XMPNode rightItem = (XMPNode) ir.next(); 809 if (itemValuesMatch(leftItem, rightItem)) 810 { 811 match = true; 812 break; 813 } 814 } 815 if (!match) 816 { 817 return false; 818 } 819 } 820 } 821 return true; // All of the checks passed. 822 } 823 824 825 /** 826 * Make sure the separator is OK. It must be one semicolon surrounded by 827 * zero or more spaces. Any of the recognized semicolons or spaces are 828 * allowed. 829 * 830 * @param separator 831 * @throws XMPException 832 */ 833 private static void checkSeparator(String separator) throws XMPException 834 { 835 boolean haveSemicolon = false; 836 for (int i = 0; i < separator.length(); i++) 837 { 838 int charKind = classifyCharacter(separator.charAt(i)); 839 if (charKind == UCK_SEMICOLON) 840 { 841 if (haveSemicolon) 842 { 843 throw new XMPException("Separator can have only one semicolon", 844 XMPError.BADPARAM); 845 } 846 haveSemicolon = true; 847 } 848 else if (charKind != UCK_SPACE) 849 { 850 throw new XMPException("Separator can have only spaces and one semicolon", 851 XMPError.BADPARAM); 852 } 853 } 854 if (!haveSemicolon) 855 { 856 throw new XMPException("Separator must have one semicolon", XMPError.BADPARAM); 857 } 858 } 859 860 861 /** 862 * Make sure the open and close quotes are a legitimate pair and return the 863 * correct closing quote or an exception. 864 * 865 * @param quotes 866 * opened and closing quote in a string 867 * @param openQuote 868 * the open quote 869 * @return Returns a corresponding closing quote. 870 * @throws XMPException 871 */ 872 private static char checkQuotes(String quotes, char openQuote) throws XMPException 873 { 874 char closeQuote; 875 876 int charKind = classifyCharacter(openQuote); 877 if (charKind != UCK_QUOTE) 878 { 879 throw new XMPException("Invalid quoting character", XMPError.BADPARAM); 880 } 881 882 if (quotes.length() == 1) 883 { 884 closeQuote = openQuote; 885 } 886 else 887 { 888 closeQuote = quotes.charAt(1); 889 charKind = classifyCharacter(closeQuote); 890 if (charKind != UCK_QUOTE) 891 { 892 throw new XMPException("Invalid quoting character", XMPError.BADPARAM); 893 } 894 } 895 896 if (closeQuote != getClosingQuote(openQuote)) 897 { 898 throw new XMPException("Mismatched quote pair", XMPError.BADPARAM); 899 } 900 return closeQuote; 901 } 902 903 904 /** 905 * Classifies the character into normal chars, spaces, semicola, quotes, 906 * control chars. 907 * 908 * @param ch 909 * a char 910 * @return Return the character kind. 911 */ 912 private static int classifyCharacter(char ch) 913 { 914 if (SPACES.indexOf(ch) >= 0 || (0x2000 <= ch && ch <= 0x200B)) 915 { 916 return UCK_SPACE; 917 } 918 else if (COMMAS.indexOf(ch) >= 0) 919 { 920 return UCK_COMMA; 921 } 922 else if (SEMICOLA.indexOf(ch) >= 0) 923 { 924 return UCK_SEMICOLON; 925 } 926 else if (QUOTES.indexOf(ch) >= 0 || (0x3008 <= ch && ch <= 0x300F) 927 || (0x2018 <= ch && ch <= 0x201F)) 928 { 929 return UCK_QUOTE; 930 } 931 else if (ch < 0x0020 || CONTROLS.indexOf(ch) >= 0) 932 { 933 return UCK_CONTROL; 934 } 935 else 936 { 937 // Assume typical case. 938 return UCK_NORMAL; 939 } 940 } 941 942 943 /** 944 * @param openQuote 945 * the open quote char 946 * @return Returns the matching closing quote for an open quote. 947 */ 948 private static char getClosingQuote(char openQuote) 949 { 950 switch (openQuote) 951 { 952 case 0x0022: 953 return 0x0022; // ! U+0022 is both opening and closing. 954 case 0x005B: 955 return 0x005D; 956 case 0x00AB: 957 return 0x00BB; // ! U+00AB and U+00BB are reversible. 958 case 0x00BB: 959 return 0x00AB; 960 case 0x2015: 961 return 0x2015; // ! U+2015 is both opening and closing. 962 case 0x2018: 963 return 0x2019; 964 case 0x201A: 965 return 0x201B; 966 case 0x201C: 967 return 0x201D; 968 case 0x201E: 969 return 0x201F; 970 case 0x2039: 971 return 0x203A; // ! U+2039 and U+203A are reversible. 972 case 0x203A: 973 return 0x2039; 974 case 0x3008: 975 return 0x3009; 976 case 0x300A: 977 return 0x300B; 978 case 0x300C: 979 return 0x300D; 980 case 0x300E: 981 return 0x300F; 982 case 0x301D: 983 return 0x301F; // ! U+301E also closes U+301D. 984 default: 985 return 0; 986 } 987 } 988 989 990 /** 991 * Add quotes to the item. 992 * 993 * @param item 994 * the array item 995 * @param openQuote 996 * the open quote character 997 * @param closeQuote 998 * the closing quote character 999 * @param allowCommas 1000 * flag if commas are allowed 1001 * @return Returns the value in quotes. 1002 */ 1003 private static String applyQuotes(String item, char openQuote, char closeQuote, 1004 boolean allowCommas) 1005 { 1006 if (item == null) 1007 { 1008 item = ""; 1009 } 1010 1011 boolean prevSpace = false; 1012 int charOffset; 1013 int charKind; 1014 1015 // See if there are any separators in the value. Stop at the first 1016 // occurrance. This is a bit 1017 // tricky in order to make typical typing work conveniently. The purpose 1018 // of applying quotes 1019 // is to preserve the values when splitting them back apart. That is 1020 // CatenateContainerItems 1021 // and SeparateContainerItems must round trip properly. For the most 1022 // part we only look for 1023 // separators here. Internal quotes, as in -- Irving "Bud" Jones -- 1024 // won't cause problems in 1025 // the separation. An initial quote will though, it will make the value 1026 // look quoted. 1027 1028 int i; 1029 for (i = 0; i < item.length(); i++) 1030 { 1031 char ch = item.charAt(i); 1032 charKind = classifyCharacter(ch); 1033 if (i == 0 && charKind == UCK_QUOTE) 1034 { 1035 break; 1036 } 1037 1038 if (charKind == UCK_SPACE) 1039 { 1040 // Multiple spaces are a separator. 1041 if (prevSpace) 1042 { 1043 break; 1044 } 1045 prevSpace = true; 1046 } 1047 else 1048 { 1049 prevSpace = false; 1050 if ((charKind == UCK_SEMICOLON || charKind == UCK_CONTROL) 1051 || (charKind == UCK_COMMA && !allowCommas)) 1052 { 1053 break; 1054 } 1055 } 1056 } 1057 1058 1059 if (i < item.length()) 1060 { 1061 // Create a quoted copy, doubling any internal quotes that match the 1062 // outer ones. Internal quotes did not stop the "needs quoting" 1063 // search, but they do need 1064 // doubling. So we have to rescan the front of the string for 1065 // quotes. Handle the special 1066 // case of U+301D being closed by either U+301E or U+301F. 1067 1068 StringBuffer newItem = new StringBuffer(item.length() + 2); 1069 int splitPoint; 1070 for (splitPoint = 0; splitPoint <= i; splitPoint++) 1071 { 1072 if (classifyCharacter(item.charAt(i)) == UCK_QUOTE) 1073 { 1074 break; 1075 } 1076 } 1077 1078 // Copy the leading "normal" portion. 1079 newItem.append(openQuote).append(item.substring(0, splitPoint)); 1080 1081 for (charOffset = splitPoint; charOffset < item.length(); charOffset++) 1082 { 1083 newItem.append(item.charAt(charOffset)); 1084 if (classifyCharacter(item.charAt(charOffset)) == UCK_QUOTE 1085 && isSurroundingQuote(item.charAt(charOffset), openQuote, closeQuote)) 1086 { 1087 newItem.append(item.charAt(charOffset)); 1088 } 1089 } 1090 1091 newItem.append(closeQuote); 1092 1093 item = newItem.toString(); 1094 } 1095 1096 return item; 1097 } 1098 1099 1100 /** 1101 * @param ch a character 1102 * @param openQuote the opening quote char 1103 * @param closeQuote the closing quote char 1104 * @return Return it the character is a surrounding quote. 1105 */ 1106 private static boolean isSurroundingQuote(char ch, char openQuote, char closeQuote) 1107 { 1108 return ch == openQuote || isClosingingQuote(ch, openQuote, closeQuote); 1109 } 1110 1111 1112 /** 1113 * @param ch a character 1114 * @param openQuote the opening quote char 1115 * @param closeQuote the closing quote char 1116 * @return Returns true if the character is a closing quote. 1117 */ 1118 private static boolean isClosingingQuote(char ch, char openQuote, char closeQuote) 1119 { 1120 return ch == closeQuote || (openQuote == 0x301D && ch == 0x301E || ch == 0x301F); 1121 } 1122 1123 1124 1125 /** 1126 * U+0022 ASCII space<br> 1127 * U+3000, ideographic space<br> 1128 * U+303F, ideographic half fill space<br> 1129 * U+2000..U+200B, en quad through zero width space 1130 */ 1131 private static final String SPACES = "\u0020\u3000\u303F"; 1132 /** 1133 * U+002C, ASCII comma<br> 1134 * U+FF0C, full width comma<br> 1135 * U+FF64, half width ideographic comma<br> 1136 * U+FE50, small comma<br> 1137 * U+FE51, small ideographic comma<br> 1138 * U+3001, ideographic comma<br> 1139 * U+060C, Arabic comma<br> 1140 * U+055D, Armenian comma 1141 */ 1142 private static final String COMMAS = "\u002C\uFF0C\uFF64\uFE50\uFE51\u3001\u060C\u055D"; 1143 /** 1144 * U+003B, ASCII semicolon<br> 1145 * U+FF1B, full width semicolon<br> 1146 * U+FE54, small semicolon<br> 1147 * U+061B, Arabic semicolon<br> 1148 * U+037E, Greek "semicolon" (really a question mark) 1149 */ 1150 private static final String SEMICOLA = "\u003B\uFF1B\uFE54\u061B\u037E"; 1151 /** 1152 * U+0022 ASCII quote<br> 1153 * ASCII '[' (0x5B) and ']' (0x5D) are used as quotes in Chinese and 1154 * Korean.<br> 1155 * U+00AB and U+00BB, guillemet quotes<br> 1156 * U+3008..U+300F, various quotes.<br> 1157 * U+301D..U+301F, double prime quotes.<br> 1158 * U+2015, dash quote.<br> 1159 * U+2018..U+201F, various quotes.<br> 1160 * U+2039 and U+203A, guillemet quotes. 1161 */ 1162 private static final String QUOTES = 1163 "\"\u005B\u005D\u00AB\u00BB\u301D\u301E\u301F\u2015\u2039\u203A"; 1164 /** 1165 * U+0000..U+001F ASCII controls<br> 1166 * U+2028, line separator.<br> 1167 * U+2029, paragraph separator. 1168 */ 1169 private static final String CONTROLS = "\u2028\u2029"; 1170 } 1171