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 package com.adobe.xmp.impl; 11 12 import java.util.GregorianCalendar; 13 import java.util.Iterator; 14 15 import com.adobe.xmp.XMPConst; 16 import com.adobe.xmp.XMPDateTime; 17 import com.adobe.xmp.XMPDateTimeFactory; 18 import com.adobe.xmp.XMPError; 19 import com.adobe.xmp.XMPException; 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.XMPPathSegment; 24 import com.adobe.xmp.options.AliasOptions; 25 import com.adobe.xmp.options.PropertyOptions; 26 27 28 /** 29 * Utilities for <code>XMPNode</code>. 30 * 31 * @since Aug 28, 2006 32 */ 33 public class XMPNodeUtils implements XMPConst 34 { 35 /** */ 36 static final int CLT_NO_VALUES = 0; 37 /** */ 38 static final int CLT_SPECIFIC_MATCH = 1; 39 /** */ 40 static final int CLT_SINGLE_GENERIC = 2; 41 /** */ 42 static final int CLT_MULTIPLE_GENERIC = 3; 43 /** */ 44 static final int CLT_XDEFAULT = 4; 45 /** */ 46 static final int CLT_FIRST_ITEM = 5; 47 48 49 /** 50 * Private Constructor 51 */ 52 private XMPNodeUtils() 53 { 54 // EMPTY 55 } 56 57 58 /** 59 * Find or create a schema node if <code>createNodes</code> is false and 60 * 61 * @param tree the root of the xmp tree. 62 * @param namespaceURI a namespace 63 * @param createNodes a flag indicating if the node shall be created if not found. 64 * <em>Note:</em> The namespace must be registered prior to this call. 65 * 66 * @return Returns the schema node if found, <code>null</code> otherwise. 67 * Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> 68 * returned a valid node. 69 * @throws XMPException An exception is only thrown if an error occurred, not if a 70 * node was not found. 71 */ 72 static XMPNode findSchemaNode(XMPNode tree, String namespaceURI, 73 boolean createNodes) 74 throws XMPException 75 { 76 return findSchemaNode(tree, namespaceURI, null, createNodes); 77 } 78 79 80 /** 81 * Find or create a schema node if <code>createNodes</code> is true. 82 * 83 * @param tree the root of the xmp tree. 84 * @param namespaceURI a namespace 85 * @param suggestedPrefix If a prefix is suggested, the namespace is allowed to be registered. 86 * @param createNodes a flag indicating if the node shall be created if not found. 87 * <em>Note:</em> The namespace must be registered prior to this call. 88 * 89 * @return Returns the schema node if found, <code>null</code> otherwise. 90 * Note: If <code>createNodes</code> is <code>true</code>, it is <b>always</b> 91 * returned a valid node. 92 * @throws XMPException An exception is only thrown if an error occurred, not if a 93 * node was not found. 94 */ 95 static XMPNode findSchemaNode(XMPNode tree, String namespaceURI, String suggestedPrefix, 96 boolean createNodes) 97 throws XMPException 98 { 99 assert tree.getParent() == null; // make sure that its the root 100 XMPNode schemaNode = tree.findChildByName(namespaceURI); 101 102 if (schemaNode == null && createNodes) 103 { 104 schemaNode = new XMPNode(namespaceURI, 105 new PropertyOptions() 106 .setSchemaNode(true)); 107 schemaNode.setImplicit(true); 108 109 // only previously registered schema namespaces are allowed in the XMP tree. 110 String prefix = XMPMetaFactory.getSchemaRegistry().getNamespacePrefix(namespaceURI); 111 if (prefix == null) 112 { 113 if (suggestedPrefix != null && suggestedPrefix.length() != 0) 114 { 115 prefix = XMPMetaFactory.getSchemaRegistry().registerNamespace(namespaceURI, 116 suggestedPrefix); 117 } 118 else 119 { 120 throw new XMPException("Unregistered schema namespace URI", 121 XMPError.BADSCHEMA); 122 } 123 } 124 125 schemaNode.setValue(prefix); 126 127 tree.addChild(schemaNode); 128 } 129 130 return schemaNode; 131 } 132 133 134 /** 135 * Find or create a child node under a given parent node. If the parent node is no 136 * Returns the found or created child node. 137 * 138 * @param parent 139 * the parent node 140 * @param childName 141 * the node name to find 142 * @param createNodes 143 * flag, if new nodes shall be created. 144 * @return Returns the found or created node or <code>null</code>. 145 * @throws XMPException Thrown if 146 */ 147 static XMPNode findChildNode(XMPNode parent, String childName, boolean createNodes) 148 throws XMPException 149 { 150 if (!parent.getOptions().isSchemaNode() && !parent.getOptions().isStruct()) 151 { 152 if (!parent.isImplicit()) 153 { 154 throw new XMPException("Named children only allowed for schemas and structs", 155 XMPError.BADXPATH); 156 } 157 else if (parent.getOptions().isArray()) 158 { 159 throw new XMPException("Named children not allowed for arrays", 160 XMPError.BADXPATH); 161 } 162 else if (createNodes) 163 { 164 parent.getOptions().setStruct(true); 165 } 166 } 167 168 XMPNode childNode = parent.findChildByName(childName); 169 170 if (childNode == null && createNodes) 171 { 172 PropertyOptions options = new PropertyOptions(); 173 childNode = new XMPNode(childName, options); 174 childNode.setImplicit(true); 175 parent.addChild(childNode); 176 } 177 178 assert childNode != null || !createNodes; 179 180 return childNode; 181 } 182 183 184 /** 185 * Follow an expanded path expression to find or create a node. 186 * 187 * @param xmpTree the node to begin the search. 188 * @param xpath the complete xpath 189 * @param createNodes flag if nodes shall be created 190 * (when called by <code>setProperty()</code>) 191 * @param leafOptions the options for the created leaf nodes (only when 192 * <code>createNodes == true</code>). 193 * @return Returns the node if found or created or <code>null</code>. 194 * @throws XMPException An exception is only thrown if an error occurred, 195 * not if a node was not found. 196 */ 197 static XMPNode findNode(XMPNode xmpTree, XMPPath xpath, boolean createNodes, 198 PropertyOptions leafOptions) throws XMPException 199 { 200 // check if xpath is set. 201 if (xpath == null || xpath.size() == 0) 202 { 203 throw new XMPException("Empty XMPPath", XMPError.BADXPATH); 204 } 205 206 // Root of implicitly created subtree to possible delete it later. 207 // Valid only if leaf is new. 208 XMPNode rootImplicitNode = null; 209 XMPNode currNode = null; 210 211 // resolve schema step 212 currNode = findSchemaNode(xmpTree, 213 xpath.getSegment(XMPPath.STEP_SCHEMA).getName(), createNodes); 214 if (currNode == null) 215 { 216 return null; 217 } 218 else if (currNode.isImplicit()) 219 { 220 currNode.setImplicit(false); // Clear the implicit node bit. 221 rootImplicitNode = currNode; // Save the top most implicit node. 222 } 223 224 225 // Now follow the remaining steps of the original XMPPath. 226 try 227 { 228 for (int i = 1; i < xpath.size(); i++) 229 { 230 currNode = followXPathStep(currNode, xpath.getSegment(i), createNodes); 231 if (currNode == null) 232 { 233 if (createNodes) 234 { 235 // delete implicitly created nodes 236 deleteNode(rootImplicitNode); 237 } 238 return null; 239 } 240 else if (currNode.isImplicit()) 241 { 242 // clear the implicit node flag 243 currNode.setImplicit(false); 244 245 // if node is an ALIAS (can be only in root step, auto-create array 246 // when the path has been resolved from a not simple alias type 247 if (i == 1 && 248 xpath.getSegment(i).isAlias() && 249 xpath.getSegment(i).getAliasForm() != 0) 250 { 251 currNode.getOptions().setOption(xpath.getSegment(i).getAliasForm(), true); 252 } 253 // "CheckImplicitStruct" in C++ 254 else if (i < xpath.size() - 1 && 255 xpath.getSegment(i).getKind() == XMPPath.STRUCT_FIELD_STEP && 256 !currNode.getOptions().isCompositeProperty()) 257 { 258 currNode.getOptions().setStruct(true); 259 } 260 261 if (rootImplicitNode == null) 262 { 263 rootImplicitNode = currNode; // Save the top most implicit node. 264 } 265 } 266 } 267 } 268 catch (XMPException e) 269 { 270 // if new notes have been created prior to the error, delete them 271 if (rootImplicitNode != null) 272 { 273 deleteNode(rootImplicitNode); 274 } 275 throw e; 276 } 277 278 279 if (rootImplicitNode != null) 280 { 281 // set options only if a node has been successful created 282 currNode.getOptions().mergeWith(leafOptions); 283 currNode.setOptions(currNode.getOptions()); 284 } 285 286 return currNode; 287 } 288 289 290 /** 291 * Deletes the the given node and its children from its parent. 292 * Takes care about adjusting the flags. 293 * @param node the top-most node to delete. 294 */ 295 static void deleteNode(XMPNode node) 296 { 297 XMPNode parent = node.getParent(); 298 299 if (node.getOptions().isQualifier()) 300 { 301 // root is qualifier 302 parent.removeQualifier(node); 303 } 304 else 305 { 306 // root is NO qualifier 307 parent.removeChild(node); 308 } 309 310 // delete empty Schema nodes 311 if (!parent.hasChildren() && parent.getOptions().isSchemaNode()) 312 { 313 parent.getParent().removeChild(parent); 314 } 315 } 316 317 318 /** 319 * This is setting the value of a leaf node. 320 * 321 * @param node an XMPNode 322 * @param value a value 323 */ 324 static void setNodeValue(XMPNode node, Object value) 325 { 326 String strValue = serializeNodeValue(value); 327 if (!(node.getOptions().isQualifier() && XML_LANG.equals(node.getName()))) 328 { 329 node.setValue(strValue); 330 } 331 else 332 { 333 node.setValue(Utils.normalizeLangValue(strValue)); 334 } 335 } 336 337 338 /** 339 * Verifies the PropertyOptions for consistancy and updates them as needed. 340 * If options are <code>null</code> they are created with default values. 341 * 342 * @param options the <code>PropertyOptions</code> 343 * @param itemValue the node value to set 344 * @return Returns the updated options. 345 * @throws XMPException If the options are not consistant. 346 */ 347 static PropertyOptions verifySetOptions(PropertyOptions options, Object itemValue) 348 throws XMPException 349 { 350 // create empty and fix existing options 351 if (options == null) 352 { 353 // set default options 354 options = new PropertyOptions(); 355 } 356 357 if (options.isArrayAltText()) 358 { 359 options.setArrayAlternate(true); 360 } 361 362 if (options.isArrayAlternate()) 363 { 364 options.setArrayOrdered(true); 365 } 366 367 if (options.isArrayOrdered()) 368 { 369 options.setArray(true); 370 } 371 372 if (options.isCompositeProperty() && itemValue != null && itemValue.toString().length() > 0) 373 { 374 throw new XMPException("Structs and arrays can't have values", 375 XMPError.BADOPTIONS); 376 } 377 378 options.assertConsistency(options.getOptions()); 379 380 return options; 381 } 382 383 384 /** 385 * Converts the node value to String, apply special conversions for defined 386 * types in XMP. 387 * 388 * @param value 389 * the node value to set 390 * @return Returns the String representation of the node value. 391 */ 392 static String serializeNodeValue(Object value) 393 { 394 String strValue; 395 if (value == null) 396 { 397 strValue = null; 398 } 399 else if (value instanceof Boolean) 400 { 401 strValue = XMPUtils.convertFromBoolean(((Boolean) value).booleanValue()); 402 } 403 else if (value instanceof Integer) 404 { 405 strValue = XMPUtils.convertFromInteger(((Integer) value).intValue()); 406 } 407 else if (value instanceof Long) 408 { 409 strValue = XMPUtils.convertFromLong(((Long) value).longValue()); 410 } 411 else if (value instanceof Double) 412 { 413 strValue = XMPUtils.convertFromDouble(((Double) value).doubleValue()); 414 } 415 else if (value instanceof XMPDateTime) 416 { 417 strValue = XMPUtils.convertFromDate((XMPDateTime) value); 418 } 419 else if (value instanceof GregorianCalendar) 420 { 421 XMPDateTime dt = XMPDateTimeFactory.createFromCalendar((GregorianCalendar) value); 422 strValue = XMPUtils.convertFromDate(dt); 423 } 424 else if (value instanceof byte[]) 425 { 426 strValue = XMPUtils.encodeBase64((byte[]) value); 427 } 428 else 429 { 430 strValue = value.toString(); 431 } 432 433 return strValue != null ? Utils.removeControlChars(strValue) : null; 434 } 435 436 437 /** 438 * After processing by ExpandXPath, a step can be of these forms: 439 * <ul> 440 * <li>qualName - A top level property or struct field. 441 * <li>[index] - An element of an array. 442 * <li>[last()] - The last element of an array. 443 * <li>[qualName="value"] - An element in an array of structs, chosen by a field value. 444 * <li>[?qualName="value"] - An element in an array, chosen by a qualifier value. 445 * <li>?qualName - A general qualifier. 446 * </ul> 447 * Find the appropriate child node, resolving aliases, and optionally creating nodes. 448 * 449 * @param parentNode the node to start to start from 450 * @param nextStep the xpath segment 451 * @param createNodes 452 * @return returns the found or created XMPPath node 453 * @throws XMPException 454 */ 455 private static XMPNode followXPathStep( 456 XMPNode parentNode, 457 XMPPathSegment nextStep, 458 boolean createNodes) throws XMPException 459 { 460 XMPNode nextNode = null; 461 int index = 0; 462 int stepKind = nextStep.getKind(); 463 464 if (stepKind == XMPPath.STRUCT_FIELD_STEP) 465 { 466 nextNode = findChildNode(parentNode, nextStep.getName(), createNodes); 467 } 468 else if (stepKind == XMPPath.QUALIFIER_STEP) 469 { 470 nextNode = findQualifierNode( 471 parentNode, nextStep.getName().substring(1), createNodes); 472 } 473 else 474 { 475 // This is an array indexing step. First get the index, then get the node. 476 477 if (!parentNode.getOptions().isArray()) 478 { 479 throw new XMPException("Indexing applied to non-array", XMPError.BADXPATH); 480 } 481 482 if (stepKind == XMPPath.ARRAY_INDEX_STEP) 483 { 484 index = findIndexedItem(parentNode, nextStep.getName(), createNodes); 485 } 486 else if (stepKind == XMPPath.ARRAY_LAST_STEP) 487 { 488 index = parentNode.getChildrenLength(); 489 } 490 else if (stepKind == XMPPath.FIELD_SELECTOR_STEP) 491 { 492 String[] result = Utils.splitNameAndValue(nextStep.getName()); 493 String fieldName = result[0]; 494 String fieldValue = result[1]; 495 index = lookupFieldSelector(parentNode, fieldName, fieldValue); 496 } 497 else if (stepKind == XMPPath.QUAL_SELECTOR_STEP) 498 { 499 String[] result = Utils.splitNameAndValue(nextStep.getName()); 500 String qualName = result[0]; 501 String qualValue = result[1]; 502 index = lookupQualSelector( 503 parentNode, qualName, qualValue, nextStep.getAliasForm()); 504 } 505 else 506 { 507 throw new XMPException("Unknown array indexing step in FollowXPathStep", 508 XMPError.INTERNALFAILURE); 509 } 510 511 if (1 <= index && index <= parentNode.getChildrenLength()) 512 { 513 nextNode = parentNode.getChild(index); 514 } 515 } 516 517 return nextNode; 518 } 519 520 521 /** 522 * Find or create a qualifier node under a given parent node. Returns a pointer to the 523 * qualifier node, and optionally an iterator for the node's position in 524 * the parent's vector of qualifiers. The iterator is unchanged if no qualifier node (null) 525 * is returned. 526 * <em>Note:</em> On entry, the qualName parameter must not have the leading '?' from the 527 * XMPPath step. 528 * 529 * @param parent the parent XMPNode 530 * @param qualName the qualifier name 531 * @param createNodes flag if nodes shall be created 532 * @return Returns the qualifier node if found or created, <code>null</code> otherwise. 533 * @throws XMPException 534 */ 535 private static XMPNode findQualifierNode(XMPNode parent, String qualName, boolean createNodes) 536 throws XMPException 537 { 538 assert !qualName.startsWith("?"); 539 540 XMPNode qualNode = parent.findQualifierByName(qualName); 541 542 if (qualNode == null && createNodes) 543 { 544 qualNode = new XMPNode(qualName, null); 545 qualNode.setImplicit(true); 546 547 parent.addQualifier(qualNode); 548 } 549 550 return qualNode; 551 } 552 553 554 /** 555 * @param arrayNode an array node 556 * @param segment the segment containing the array index 557 * @param createNodes flag if new nodes are allowed to be created. 558 * @return Returns the index or index = -1 if not found 559 * @throws XMPException Throws Exceptions 560 */ 561 private static int findIndexedItem(XMPNode arrayNode, String segment, boolean createNodes) 562 throws XMPException 563 { 564 int index = 0; 565 566 try 567 { 568 segment = segment.substring(1, segment.length() - 1); 569 index = Integer.parseInt(segment); 570 if (index < 1) 571 { 572 throw new XMPException("Array index must be larger than zero", 573 XMPError.BADXPATH); 574 } 575 } 576 catch (NumberFormatException e) 577 { 578 throw new XMPException("Array index not digits.", XMPError.BADXPATH); 579 } 580 581 if (createNodes && index == arrayNode.getChildrenLength() + 1) 582 { 583 // Append a new last + 1 node. 584 XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, null); 585 newItem.setImplicit(true); 586 arrayNode.addChild(newItem); 587 } 588 589 return index; 590 } 591 592 593 /** 594 * Searches for a field selector in a node: 595 * [fieldName="value] - an element in an array of structs, chosen by a field value. 596 * No implicit nodes are created by field selectors. 597 * 598 * @param arrayNode 599 * @param fieldName 600 * @param fieldValue 601 * @return Returns the index of the field if found, otherwise -1. 602 * @throws XMPException 603 */ 604 private static int lookupFieldSelector(XMPNode arrayNode, String fieldName, String fieldValue) 605 throws XMPException 606 { 607 int result = -1; 608 609 for (int index = 1; index <= arrayNode.getChildrenLength() && result < 0; index++) 610 { 611 XMPNode currItem = arrayNode.getChild(index); 612 613 if (!currItem.getOptions().isStruct()) 614 { 615 throw new XMPException("Field selector must be used on array of struct", 616 XMPError.BADXPATH); 617 } 618 619 for (int f = 1; f <= currItem.getChildrenLength(); f++) 620 { 621 XMPNode currField = currItem.getChild(f); 622 if (!fieldName.equals(currField.getName())) 623 { 624 continue; 625 } 626 if (fieldValue.equals(currField.getValue())) 627 { 628 result = index; 629 break; 630 } 631 } 632 } 633 634 return result; 635 } 636 637 638 /** 639 * Searches for a qualifier selector in a node: 640 * [?qualName="value"] - an element in an array, chosen by a qualifier value. 641 * No implicit nodes are created for qualifier selectors, 642 * except for an alias to an x-default item. 643 * 644 * @param arrayNode an array node 645 * @param qualName the qualifier name 646 * @param qualValue the qualifier value 647 * @param aliasForm in case the qual selector results from an alias, 648 * an x-default node is created if there has not been one. 649 * @return Returns the index of th 650 * @throws XMPException 651 */ 652 private static int lookupQualSelector(XMPNode arrayNode, String qualName, 653 String qualValue, int aliasForm) throws XMPException 654 { 655 if (XML_LANG.equals(qualName)) 656 { 657 qualValue = Utils.normalizeLangValue(qualValue); 658 int index = XMPNodeUtils.lookupLanguageItem(arrayNode, qualValue); 659 if (index < 0 && (aliasForm & AliasOptions.PROP_ARRAY_ALT_TEXT) > 0) 660 { 661 XMPNode langNode = new XMPNode(ARRAY_ITEM_NAME, null); 662 XMPNode xdefault = new XMPNode(XML_LANG, X_DEFAULT, null); 663 langNode.addQualifier(xdefault); 664 arrayNode.addChild(1, langNode); 665 return 1; 666 } 667 else 668 { 669 return index; 670 } 671 } 672 else 673 { 674 for (int index = 1; index < arrayNode.getChildrenLength(); index++) 675 { 676 XMPNode currItem = arrayNode.getChild(index); 677 678 for (Iterator it = currItem.iterateQualifier(); it.hasNext();) 679 { 680 XMPNode qualifier = (XMPNode) it.next(); 681 if (qualName.equals(qualifier.getName()) && 682 qualValue.equals(qualifier.getValue())) 683 { 684 return index; 685 } 686 } 687 } 688 return -1; 689 } 690 } 691 692 693 /** 694 * Make sure the x-default item is first. Touch up "single value" 695 * arrays that have a default plus one real language. This case should have 696 * the same value for both items. Older Adobe apps were hardwired to only 697 * use the "x-default" item, so we copy that value to the other 698 * item. 699 * 700 * @param arrayNode 701 * an alt text array node 702 */ 703 static void normalizeLangArray(XMPNode arrayNode) 704 { 705 if (!arrayNode.getOptions().isArrayAltText()) 706 { 707 return; 708 } 709 710 // check if node with x-default qual is first place 711 for (int i = 2; i <= arrayNode.getChildrenLength(); i++) 712 { 713 XMPNode child = arrayNode.getChild(i); 714 if (child.hasQualifier() && X_DEFAULT.equals(child.getQualifier(1).getValue())) 715 { 716 // move node to first place 717 try 718 { 719 arrayNode.removeChild(i); 720 arrayNode.addChild(1, child); 721 } 722 catch (XMPException e) 723 { 724 // cannot occur, because same child is removed before 725 assert false; 726 } 727 728 if (i == 2) 729 { 730 arrayNode.getChild(2).setValue(child.getValue()); 731 } 732 break; 733 } 734 } 735 } 736 737 738 /** 739 * See if an array is an alt-text array. If so, make sure the x-default item 740 * is first. 741 * 742 * @param arrayNode 743 * the array node to check if its an alt-text array 744 */ 745 static void detectAltText(XMPNode arrayNode) 746 { 747 if (arrayNode.getOptions().isArrayAlternate() && arrayNode.hasChildren()) 748 { 749 boolean isAltText = false; 750 for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) 751 { 752 XMPNode child = (XMPNode) it.next(); 753 if (child.getOptions().getHasLanguage()) 754 { 755 isAltText = true; 756 break; 757 } 758 } 759 760 if (isAltText) 761 { 762 arrayNode.getOptions().setArrayAltText(true); 763 normalizeLangArray(arrayNode); 764 } 765 } 766 } 767 768 769 /** 770 * Appends a language item to an alt text array. 771 * 772 * @param arrayNode the language array 773 * @param itemLang the language of the item 774 * @param itemValue the content of the item 775 * @throws XMPException Thrown if a duplicate property is added 776 */ 777 static void appendLangItem(XMPNode arrayNode, String itemLang, String itemValue) 778 throws XMPException 779 { 780 XMPNode newItem = new XMPNode(ARRAY_ITEM_NAME, itemValue, null); 781 XMPNode langQual = new XMPNode(XML_LANG, itemLang, null); 782 newItem.addQualifier(langQual); 783 784 if (!X_DEFAULT.equals(langQual.getValue())) 785 { 786 arrayNode.addChild(newItem); 787 } 788 else 789 { 790 arrayNode.addChild(1, newItem); 791 } 792 } 793 794 795 /** 796 * <ol> 797 * <li>Look for an exact match with the specific language. 798 * <li>If a generic language is given, look for partial matches. 799 * <li>Look for an "x-default"-item. 800 * <li>Choose the first item. 801 * </ol> 802 * 803 * @param arrayNode 804 * the alt text array node 805 * @param genericLang 806 * the generic language 807 * @param specificLang 808 * the specific language 809 * @return Returns the kind of match as an Integer and the found node in an 810 * array. 811 * 812 * @throws XMPException 813 */ 814 static Object[] chooseLocalizedText(XMPNode arrayNode, String genericLang, String specificLang) 815 throws XMPException 816 { 817 // See if the array has the right form. Allow empty alt arrays, 818 // that is what parsing returns. 819 if (!arrayNode.getOptions().isArrayAltText()) 820 { 821 throw new XMPException("Localized text array is not alt-text", XMPError.BADXPATH); 822 } 823 else if (!arrayNode.hasChildren()) 824 { 825 return new Object[] { new Integer(XMPNodeUtils.CLT_NO_VALUES), null }; 826 } 827 828 int foundGenericMatches = 0; 829 XMPNode resultNode = null; 830 XMPNode xDefault = null; 831 832 // Look for the first partial match with the generic language. 833 for (Iterator it = arrayNode.iterateChildren(); it.hasNext();) 834 { 835 XMPNode currItem = (XMPNode) it.next(); 836 837 // perform some checks on the current item 838 if (currItem.getOptions().isCompositeProperty()) 839 { 840 throw new XMPException("Alt-text array item is not simple", XMPError.BADXPATH); 841 } 842 else if (!currItem.hasQualifier() 843 || !XML_LANG.equals(currItem.getQualifier(1).getName())) 844 { 845 throw new XMPException("Alt-text array item has no language qualifier", 846 XMPError.BADXPATH); 847 } 848 849 String currLang = currItem.getQualifier(1).getValue(); 850 851 // Look for an exact match with the specific language. 852 if (specificLang.equals(currLang)) 853 { 854 return new Object[] { new Integer(XMPNodeUtils.CLT_SPECIFIC_MATCH), currItem }; 855 } 856 else if (genericLang != null && currLang.startsWith(genericLang)) 857 { 858 if (resultNode == null) 859 { 860 resultNode = currItem; 861 } 862 // ! Don't return/break, need to look for other matches. 863 foundGenericMatches++; 864 } 865 else if (X_DEFAULT.equals(currLang)) 866 { 867 xDefault = currItem; 868 } 869 } 870 871 // evaluate loop 872 if (foundGenericMatches == 1) 873 { 874 return new Object[] { new Integer(XMPNodeUtils.CLT_SINGLE_GENERIC), resultNode }; 875 } 876 else if (foundGenericMatches > 1) 877 { 878 return new Object[] { new Integer(XMPNodeUtils.CLT_MULTIPLE_GENERIC), resultNode }; 879 } 880 else if (xDefault != null) 881 { 882 return new Object[] { new Integer(XMPNodeUtils.CLT_XDEFAULT), xDefault }; 883 } 884 else 885 { 886 // Everything failed, choose the first item. 887 return new Object[] { new Integer(XMPNodeUtils.CLT_FIRST_ITEM), arrayNode.getChild(1) }; 888 } 889 } 890 891 892 /** 893 * Looks for the appropriate language item in a text alternative array.item 894 * 895 * @param arrayNode 896 * an array node 897 * @param language 898 * the requested language 899 * @return Returns the index if the language has been found, -1 otherwise. 900 * @throws XMPException 901 */ 902 static int lookupLanguageItem(XMPNode arrayNode, String language) throws XMPException 903 { 904 if (!arrayNode.getOptions().isArray()) 905 { 906 throw new XMPException("Language item must be used on array", XMPError.BADXPATH); 907 } 908 909 for (int index = 1; index <= arrayNode.getChildrenLength(); index++) 910 { 911 XMPNode child = arrayNode.getChild(index); 912 if (!child.hasQualifier() || !XML_LANG.equals(child.getQualifier(1).getName())) 913 { 914 continue; 915 } 916 else if (language.equals(child.getQualifier(1).getValue())) 917 { 918 return index; 919 } 920 } 921 922 return -1; 923 } 924 } 925