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 */ XMPUtilsImpl()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 */ catenateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, String separator, String quotes, boolean allowCommas)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 */ separateArrayItems(XMPMeta xmp, String schemaNS, String arrayName, String catedStr, PropertyOptions arrayOptions, boolean preserveCommas)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 */ separateFindCreateArray(String schemaNS, String arrayName, PropertyOptions arrayOptions, XMPMetaImpl xmp)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 */ removeProperties(XMPMeta xmp, String schemaNS, String propName, boolean doAllProperties, boolean includeAliases)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 */ appendProperties(XMPMeta source, XMPMeta destination, boolean doAllProperties, boolean replaceOldValues, boolean deleteEmptyValues)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 */ removeSchemaChildren(XMPNode schemaNode, boolean doAllProperties)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 */ appendSubtree(XMPMetaImpl destXMP, XMPNode sourceNode, XMPNode destParent, boolean replaceOldValues, boolean deleteEmptyValues)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 */ itemValuesMatch(XMPNode leftNode, XMPNode rightNode)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 */ checkSeparator(String separator)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 */ checkQuotes(String quotes, char openQuote)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 */ classifyCharacter(char ch)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 */ getClosingQuote(char openQuote)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 */ applyQuotes(String item, char openQuote, char closeQuote, boolean allowCommas)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 */ isSurroundingQuote(char ch, char openQuote, char closeQuote)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 */ isClosingingQuote(char ch, char openQuote, char closeQuote)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