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.ArrayList; 13 import java.util.Arrays; 14 import java.util.Collections; 15 import java.util.Iterator; 16 import java.util.List; 17 import java.util.ListIterator; 18 19 import com.adobe.xmp.XMPConst; 20 import com.adobe.xmp.XMPError; 21 import com.adobe.xmp.XMPException; 22 import com.adobe.xmp.options.PropertyOptions; 23 24 25 /** 26 * A node in the internally XMP tree, which can be a schema node, a property node, an array node, 27 * an array item, a struct node or a qualifier node (without '?'). 28 * 29 * Possible improvements: 30 * 31 * 1. The kind Node of node might be better represented by a class-hierarchy of different nodes. 32 * 2. The array type should be an enum 33 * 3. isImplicitNode should be removed completely and replaced by return values of fi. 34 * 4. hasLanguage, hasType should be automatically maintained by XMPNode 35 * 36 * @since 21.02.2006 37 */ 38 class XMPNode implements Comparable 39 { 40 /** name of the node, contains different information depending of the node kind */ 41 private String name; 42 /** value of the node, contains different information depending of the node kind */ 43 private String value; 44 /** link to the parent node */ 45 private XMPNode parent; 46 /** list of child nodes, lazy initialized */ 47 private List children = null; 48 /** list of qualifier of the node, lazy initialized */ 49 private List qualifier = null; 50 /** options describing the kind of the node */ 51 private PropertyOptions options = null; 52 53 // internal processing options 54 55 /** flag if the node is implicitly created */ 56 private boolean implicit; 57 /** flag if the node has aliases */ 58 private boolean hasAliases; 59 /** flag if the node is an alias */ 60 private boolean alias; 61 /** flag if the node has an "rdf:value" child node. */ 62 private boolean hasValueChild; 63 64 65 66 /** 67 * Creates an <code>XMPNode</code> with initial values. 68 * 69 * @param name the name of the node 70 * @param value the value of the node 71 * @param options the options of the node 72 */ XMPNode(String name, String value, PropertyOptions options)73 public XMPNode(String name, String value, PropertyOptions options) 74 { 75 this.name = name; 76 this.value = value; 77 this.options = options; 78 } 79 80 81 /** 82 * Constructor for the node without value. 83 * 84 * @param name the name of the node 85 * @param options the options of the node 86 */ XMPNode(String name, PropertyOptions options)87 public XMPNode(String name, PropertyOptions options) 88 { 89 this(name, null, options); 90 } 91 92 93 /** 94 * Resets the node. 95 */ clear()96 public void clear() 97 { 98 options = null; 99 name = null; 100 value = null; 101 children = null; 102 qualifier = null; 103 } 104 105 106 /** 107 * @return Returns the parent node. 108 */ getParent()109 public XMPNode getParent() 110 { 111 return parent; 112 } 113 114 115 /** 116 * @param index an index [1..size] 117 * @return Returns the child with the requested index. 118 */ getChild(int index)119 public XMPNode getChild(int index) 120 { 121 return (XMPNode) getChildren().get(index - 1); 122 } 123 124 125 /** 126 * Adds a node as child to this node. 127 * @param node an XMPNode 128 * @throws XMPException 129 */ addChild(XMPNode node)130 public void addChild(XMPNode node) throws XMPException 131 { 132 // check for duplicate properties 133 assertChildNotExisting(node.getName()); 134 node.setParent(this); 135 getChildren().add(node); 136 } 137 138 139 /** 140 * Adds a node as child to this node. 141 * @param index the index of the node <em>before</em> which the new one is inserted. 142 * <em>Note:</em> The node children are indexed from [1..size]! 143 * An index of size + 1 appends a node. 144 * @param node an XMPNode 145 * @throws XMPException 146 */ addChild(int index, XMPNode node)147 public void addChild(int index, XMPNode node) throws XMPException 148 { 149 assertChildNotExisting(node.getName()); 150 node.setParent(this); 151 getChildren().add(index - 1, node); 152 } 153 154 155 /** 156 * Replaces a node with another one. 157 * @param index the index of the node that will be replaced. 158 * <em>Note:</em> The node children are indexed from [1..size]! 159 * @param node the replacement XMPNode 160 */ replaceChild(int index, XMPNode node)161 public void replaceChild(int index, XMPNode node) 162 { 163 node.setParent(this); 164 getChildren().set(index - 1, node); 165 } 166 167 168 /** 169 * Removes a child at the requested index. 170 * @param itemIndex the index to remove [1..size] 171 */ removeChild(int itemIndex)172 public void removeChild(int itemIndex) 173 { 174 getChildren().remove(itemIndex - 1); 175 cleanupChildren(); 176 } 177 178 179 /** 180 * Removes a child node. 181 * If its a schema node and doesn't have any children anymore, its deleted. 182 * 183 * @param node the child node to delete. 184 */ removeChild(XMPNode node)185 public void removeChild(XMPNode node) 186 { 187 getChildren().remove(node); 188 cleanupChildren(); 189 } 190 191 192 /** 193 * Removes the children list if this node has no children anymore; 194 * checks if the provided node is a schema node and doesn't have any children anymore, 195 * its deleted. 196 */ cleanupChildren()197 protected void cleanupChildren() 198 { 199 if (children.isEmpty()) 200 { 201 children = null; 202 } 203 } 204 205 206 /** 207 * Removes all children from the node. 208 */ removeChildren()209 public void removeChildren() 210 { 211 children = null; 212 } 213 214 215 /** 216 * @return Returns the number of children without neccessarily creating a list. 217 */ getChildrenLength()218 public int getChildrenLength() 219 { 220 return children != null ? 221 children.size() : 222 0; 223 } 224 225 226 /** 227 * @param expr child node name to look for 228 * @return Returns an <code>XMPNode</code> if node has been found, <code>null</code> otherwise. 229 */ findChildByName(String expr)230 public XMPNode findChildByName(String expr) 231 { 232 return find(getChildren(), expr); 233 } 234 235 236 /** 237 * @param index an index [1..size] 238 * @return Returns the qualifier with the requested index. 239 */ getQualifier(int index)240 public XMPNode getQualifier(int index) 241 { 242 return (XMPNode) getQualifier().get(index - 1); 243 } 244 245 246 /** 247 * @return Returns the number of qualifier without neccessarily creating a list. 248 */ getQualifierLength()249 public int getQualifierLength() 250 { 251 return qualifier != null ? 252 qualifier.size() : 253 0; 254 } 255 256 257 /** 258 * Appends a qualifier to the qualifier list and sets respective options. 259 * @param qualNode a qualifier node. 260 * @throws XMPException 261 */ addQualifier(XMPNode qualNode)262 public void addQualifier(XMPNode qualNode) throws XMPException 263 { 264 assertQualifierNotExisting(qualNode.getName()); 265 qualNode.setParent(this); 266 qualNode.getOptions().setQualifier(true); 267 getOptions().setHasQualifiers(true); 268 269 // contraints 270 if (qualNode.isLanguageNode()) 271 { 272 // "xml:lang" is always first and the option "hasLanguage" is set 273 options.setHasLanguage(true); 274 getQualifier().add(0, qualNode); 275 } 276 else if (qualNode.isTypeNode()) 277 { 278 // "rdf:type" must be first or second after "xml:lang" and the option "hasType" is set 279 options.setHasType(true); 280 getQualifier().add( 281 !options.getHasLanguage() ? 0 : 1, 282 qualNode); 283 } 284 else 285 { 286 // other qualifiers are appended 287 getQualifier().add(qualNode); 288 } 289 } 290 291 292 /** 293 * Removes one qualifier node and fixes the options. 294 * @param qualNode qualifier to remove 295 */ removeQualifier(XMPNode qualNode)296 public void removeQualifier(XMPNode qualNode) 297 { 298 PropertyOptions opts = getOptions(); 299 if (qualNode.isLanguageNode()) 300 { 301 // if "xml:lang" is removed, remove hasLanguage-flag too 302 opts.setHasLanguage(false); 303 } 304 else if (qualNode.isTypeNode()) 305 { 306 // if "rdf:type" is removed, remove hasType-flag too 307 opts.setHasType(false); 308 } 309 310 getQualifier().remove(qualNode); 311 if (qualifier.isEmpty()) 312 { 313 opts.setHasQualifiers(false); 314 qualifier = null; 315 } 316 317 } 318 319 320 /** 321 * Removes all qualifiers from the node and sets the options appropriate. 322 */ removeQualifiers()323 public void removeQualifiers() 324 { 325 PropertyOptions opts = getOptions(); 326 // clear qualifier related options 327 opts.setHasQualifiers(false); 328 opts.setHasLanguage(false); 329 opts.setHasType(false); 330 qualifier = null; 331 } 332 333 334 /** 335 * @param expr qualifier node name to look for 336 * @return Returns a qualifier <code>XMPNode</code> if node has been found, 337 * <code>null</code> otherwise. 338 */ findQualifierByName(String expr)339 public XMPNode findQualifierByName(String expr) 340 { 341 return find(qualifier, expr); 342 } 343 344 345 /** 346 * @return Returns whether the node has children. 347 */ hasChildren()348 public boolean hasChildren() 349 { 350 return children != null && children.size() > 0; 351 } 352 353 354 /** 355 * @return Returns an iterator for the children. 356 * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case. 357 */ iterateChildren()358 public Iterator iterateChildren() 359 { 360 if (children != null) 361 { 362 return getChildren().iterator(); 363 } 364 else 365 { 366 return Collections.EMPTY_LIST.listIterator(); 367 } 368 } 369 370 371 /** 372 * @return Returns whether the node has qualifier attached. 373 */ hasQualifier()374 public boolean hasQualifier() 375 { 376 return qualifier != null && qualifier.size() > 0; 377 } 378 379 380 /** 381 * @return Returns an iterator for the qualifier. 382 * <em>Note:</em> take care to use it.remove(), as the flag are not adjusted in that case. 383 */ iterateQualifier()384 public Iterator iterateQualifier() 385 { 386 if (qualifier != null) 387 { 388 final Iterator it = getQualifier().iterator(); 389 390 return new Iterator() 391 { 392 public boolean hasNext() 393 { 394 return it.hasNext(); 395 } 396 397 public Object next() 398 { 399 return it.next(); 400 } 401 402 public void remove() 403 { 404 throw new UnsupportedOperationException( 405 "remove() is not allowed due to the internal contraints"); 406 } 407 408 }; 409 } 410 else 411 { 412 return Collections.EMPTY_LIST.iterator(); 413 } 414 } 415 416 417 /** 418 * Performs a <b>deep clone</b> of the node and the complete subtree. 419 * 420 * @see java.lang.Object#clone() 421 */ 422 public Object clone() 423 { 424 PropertyOptions newOptions; 425 try 426 { 427 newOptions = new PropertyOptions(getOptions().getOptions()); 428 } 429 catch (XMPException e) 430 { 431 // cannot happen 432 newOptions = new PropertyOptions(); 433 } 434 435 XMPNode newNode = new XMPNode(name, value, newOptions); 436 cloneSubtree(newNode); 437 438 return newNode; 439 } 440 441 442 /** 443 * Performs a <b>deep clone</b> of the complete subtree (children and 444 * qualifier )into and add it to the destination node. 445 * 446 * @param destination the node to add the cloned subtree 447 */ 448 public void cloneSubtree(XMPNode destination) 449 { 450 try 451 { 452 for (Iterator it = iterateChildren(); it.hasNext();) 453 { 454 XMPNode child = (XMPNode) it.next(); 455 destination.addChild((XMPNode) child.clone()); 456 } 457 458 for (Iterator it = iterateQualifier(); it.hasNext();) 459 { 460 XMPNode qualifier = (XMPNode) it.next(); 461 destination.addQualifier((XMPNode) qualifier.clone()); 462 } 463 } 464 catch (XMPException e) 465 { 466 // cannot happen (duplicate childs/quals do not exist in this node) 467 assert false; 468 } 469 470 } 471 472 473 /** 474 * Renders this node and the tree unter this node in a human readable form. 475 * @param recursive Flag is qualifier and child nodes shall be rendered too 476 * @return Returns a multiline string containing the dump. 477 */ 478 public String dumpNode(boolean recursive) 479 { 480 StringBuffer result = new StringBuffer(512); 481 this.dumpNode(result, recursive, 0, 0); 482 return result.toString(); 483 } 484 485 486 /** 487 * @see Comparable#compareTo(Object) 488 */ 489 public int compareTo(Object xmpNode) 490 { 491 if (getOptions().isSchemaNode()) 492 { 493 return this.value.compareTo(((XMPNode) xmpNode).getValue()); 494 } 495 else 496 { 497 return this.name.compareTo(((XMPNode) xmpNode).getName()); 498 } 499 } 500 501 502 /** 503 * @return Returns the name. 504 */ 505 public String getName() 506 { 507 return name; 508 } 509 510 511 /** 512 * @param name The name to set. 513 */ 514 public void setName(String name) 515 { 516 this.name = name; 517 } 518 519 520 /** 521 * @return Returns the value. 522 */ 523 public String getValue() 524 { 525 return value; 526 } 527 528 529 /** 530 * @param value The value to set. 531 */ 532 public void setValue(String value) 533 { 534 this.value = value; 535 } 536 537 538 /** 539 * @return Returns the options. 540 */ 541 public PropertyOptions getOptions() 542 { 543 if (options == null) 544 { 545 options = new PropertyOptions(); 546 } 547 return options; 548 } 549 550 551 /** 552 * Updates the options of the node. 553 * @param options the options to set. 554 */ 555 public void setOptions(PropertyOptions options) 556 { 557 this.options = options; 558 } 559 560 561 /** 562 * @return Returns the implicit flag 563 */ 564 public boolean isImplicit() 565 { 566 return implicit; 567 } 568 569 570 /** 571 * @param implicit Sets the implicit node flag 572 */ 573 public void setImplicit(boolean implicit) 574 { 575 this.implicit = implicit; 576 } 577 578 579 /** 580 * @return Returns if the node contains aliases (applies only to schema nodes) 581 */ 582 public boolean getHasAliases() 583 { 584 return hasAliases; 585 } 586 587 588 /** 589 * @param hasAliases sets the flag that the node contains aliases 590 */ 591 public void setHasAliases(boolean hasAliases) 592 { 593 this.hasAliases = hasAliases; 594 } 595 596 597 /** 598 * @return Returns if the node contains aliases (applies only to schema nodes) 599 */ 600 public boolean isAlias() 601 { 602 return alias; 603 } 604 605 606 /** 607 * @param alias sets the flag that the node is an alias 608 */ 609 public void setAlias(boolean alias) 610 { 611 this.alias = alias; 612 } 613 614 615 /** 616 * @return the hasValueChild 617 */ 618 public boolean getHasValueChild() 619 { 620 return hasValueChild; 621 } 622 623 624 /** 625 * @param hasValueChild the hasValueChild to set 626 */ 627 public void setHasValueChild(boolean hasValueChild) 628 { 629 this.hasValueChild = hasValueChild; 630 } 631 632 633 634 /** 635 * Sorts the complete datamodel according to the following rules: 636 * <ul> 637 * <li>Nodes at one level are sorted by name, that is prefix + local name 638 * <li>Starting at the root node the children and qualifier are sorted recursively, 639 * which the following exceptions. 640 * <li>Sorting will not be used for arrays. 641 * <li>Within qualifier "xml:lang" and/or "rdf:type" stay at the top in that order, 642 * all others are sorted. 643 * </ul> 644 */ 645 public void sort() 646 { 647 // sort qualifier 648 if (hasQualifier()) 649 { 650 XMPNode[] quals = (XMPNode[]) getQualifier() 651 .toArray(new XMPNode[getQualifierLength()]); 652 int sortFrom = 0; 653 while ( 654 quals.length > sortFrom && 655 (XMPConst.XML_LANG.equals(quals[sortFrom].getName()) || 656 "rdf:type".equals(quals[sortFrom].getName())) 657 ) 658 { 659 quals[sortFrom].sort(); 660 sortFrom++; 661 } 662 663 Arrays.sort(quals, sortFrom, quals.length); 664 ListIterator it = qualifier.listIterator(); 665 for (int j = 0; j < quals.length; j++) 666 { 667 it.next(); 668 it.set(quals[j]); 669 quals[j].sort(); 670 } 671 } 672 673 // sort children 674 if (hasChildren()) 675 { 676 if (!getOptions().isArray()) 677 { 678 Collections.sort(children); 679 } 680 for (Iterator it = iterateChildren(); it.hasNext();) 681 { 682 ((XMPNode) it.next()).sort(); 683 684 } 685 } 686 } 687 688 689 690 //------------------------------------------------------------------------------ private methods 691 692 693 /** 694 * Dumps this node and its qualifier and children recursively. 695 * <em>Note:</em> It creats empty options on every node. 696 * 697 * @param result the buffer to append the dump. 698 * @param recursive Flag is qualifier and child nodes shall be rendered too 699 * @param indent the current indent level. 700 * @param index the index within the parent node (important for arrays) 701 */ 702 private void dumpNode(StringBuffer result, boolean recursive, int indent, int index) 703 { 704 // write indent 705 for (int i = 0; i < indent; i++) 706 { 707 result.append('\t'); 708 } 709 710 // render Node 711 if (parent != null) 712 { 713 if (getOptions().isQualifier()) 714 { 715 result.append('?'); 716 result.append(name); 717 } 718 else if (getParent().getOptions().isArray()) 719 { 720 result.append('['); 721 result.append(index); 722 result.append(']'); 723 } 724 else 725 { 726 result.append(name); 727 } 728 } 729 else 730 { 731 // applies only to the root node 732 result.append("ROOT NODE"); 733 if (name != null && name.length() > 0) 734 { 735 // the "about" attribute 736 result.append(" ("); 737 result.append(name); 738 result.append(')'); 739 } 740 } 741 742 if (value != null && value.length() > 0) 743 { 744 result.append(" = \""); 745 result.append(value); 746 result.append('"'); 747 } 748 749 // render options if at least one is set 750 if (getOptions().containsOneOf(0xffffffff)) 751 { 752 result.append("\t("); 753 result.append(getOptions().toString()); 754 result.append(" : "); 755 result.append(getOptions().getOptionsString()); 756 result.append(')'); 757 } 758 759 result.append('\n'); 760 761 // render qualifier 762 if (recursive && hasQualifier()) 763 { 764 XMPNode[] quals = (XMPNode[]) getQualifier() 765 .toArray(new XMPNode[getQualifierLength()]); 766 int i = 0; 767 while (quals.length > i && 768 (XMPConst.XML_LANG.equals(quals[i].getName()) || 769 "rdf:type".equals(quals[i].getName())) 770 ) 771 { 772 i++; 773 } 774 Arrays.sort(quals, i, quals.length); 775 for (i = 0; i < quals.length; i++) 776 { 777 XMPNode qualifier = quals[i]; 778 qualifier.dumpNode(result, recursive, indent + 2, i + 1); 779 } 780 } 781 782 // render children 783 if (recursive && hasChildren()) 784 { 785 XMPNode[] children = (XMPNode[]) getChildren() 786 .toArray(new XMPNode[getChildrenLength()]); 787 if (!getOptions().isArray()) 788 { 789 Arrays.sort(children); 790 } 791 for (int i = 0; i < children.length; i++) 792 { 793 XMPNode child = children[i]; 794 child.dumpNode(result, recursive, indent + 1, i + 1); 795 } 796 } 797 } 798 799 800 /** 801 * @return Returns whether this node is a language qualifier. 802 */ 803 private boolean isLanguageNode() 804 { 805 return XMPConst.XML_LANG.equals(name); 806 } 807 808 809 /** 810 * @return Returns whether this node is a type qualifier. 811 */ 812 private boolean isTypeNode() 813 { 814 return "rdf:type".equals(name); 815 } 816 817 818 /** 819 * <em>Note:</em> This method should always be called when accessing 'children' to be sure 820 * that its initialized. 821 * @return Returns list of children that is lazy initialized. 822 */ 823 private List getChildren() 824 { 825 if (children == null) 826 { 827 children = new ArrayList(0); 828 } 829 return children; 830 } 831 832 833 /** 834 * @return Returns a read-only copy of child nodes list. 835 */ 836 public List getUnmodifiableChildren() 837 { 838 return Collections.unmodifiableList(new ArrayList(getChildren())); 839 } 840 841 842 /** 843 * @return Returns list of qualifier that is lazy initialized. 844 */ 845 private List getQualifier() 846 { 847 if (qualifier == null) 848 { 849 qualifier = new ArrayList(0); 850 } 851 return qualifier; 852 } 853 854 855 /** 856 * Sets the parent node, this is solely done by <code>addChild(...)</code> 857 * and <code>addQualifier()</code>. 858 * 859 * @param parent 860 * Sets the parent node. 861 */ 862 protected void setParent(XMPNode parent) 863 { 864 this.parent = parent; 865 } 866 867 868 /** 869 * Internal find. 870 * @param list the list to search in 871 * @param expr the search expression 872 * @return Returns the found node or <code>nulls</code>. 873 */ 874 private XMPNode find(List list, String expr) 875 { 876 877 if (list != null) 878 { 879 for (Iterator it = list.iterator(); it.hasNext();) 880 { 881 XMPNode child = (XMPNode) it.next(); 882 if (child.getName().equals(expr)) 883 { 884 return child; 885 } 886 } 887 } 888 return null; 889 } 890 891 892 /** 893 * Checks that a node name is not existing on the same level, except for array items. 894 * @param childName the node name to check 895 * @throws XMPException Thrown if a node with the same name is existing. 896 */ 897 private void assertChildNotExisting(String childName) throws XMPException 898 { 899 if (!XMPConst.ARRAY_ITEM_NAME.equals(childName) && 900 findChildByName(childName) != null) 901 { 902 throw new XMPException("Duplicate property or field node '" + childName + "'", 903 XMPError.BADXMP); 904 } 905 } 906 907 908 /** 909 * Checks that a qualifier name is not existing on the same level. 910 * @param qualifierName the new qualifier name 911 * @throws XMPException Thrown if a node with the same name is existing. 912 */ 913 private void assertQualifierNotExisting(String qualifierName) throws XMPException 914 { 915 if (!XMPConst.ARRAY_ITEM_NAME.equals(qualifierName) && 916 findQualifierByName(qualifierName) != null) 917 { 918 throw new XMPException("Duplicate '" + qualifierName + "' qualifier", XMPError.BADXMP); 919 } 920 } 921 }