001 // Copyright 2006, 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 015 package org.apache.tapestry5.dom; 016 017 import org.apache.tapestry5.func.Predicate; 018 import org.apache.tapestry5.internal.TapestryInternalUtils; 019 import org.apache.tapestry5.internal.util.PrintOutCollector; 020 import org.apache.tapestry5.ioc.internal.util.CollectionFactory; 021 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 022 import org.apache.tapestry5.ioc.util.Stack; 023 024 import java.io.PrintWriter; 025 import java.util.*; 026 027 /** 028 * An element that will render with a begin tag and attributes, a body, and an end tag. Also acts as a factory for 029 * enclosed Element, Text and Comment nodes. 030 * <p/> 031 * TODO: Support for CDATA nodes. Do we need Entity nodes? 032 */ 033 public final class Element extends Node 034 { 035 036 private final String name; 037 038 private Node firstChild; 039 040 private Node lastChild; 041 042 private Attribute firstAttribute; 043 044 private final Document document; 045 046 private static final String CLASS_ATTRIBUTE = "class"; 047 048 /** 049 * URI of the namespace which contains the element. A quirk in XML is that the element may be in a namespace it 050 * defines itself, so resolving the namespace to a prefix must wait until render time (since the Element is created 051 * before the namespaces for it are defined). 052 */ 053 private final String namespace; 054 055 private Map<String, String> namespaceToPrefix; 056 057 /** 058 * Constructor for a root element. 059 */ 060 Element(Document container, String namespace, String name) 061 { 062 super(null); 063 064 document = container; 065 this.namespace = namespace; 066 this.name = name; 067 } 068 069 /** 070 * Constructor for a nested element. 071 */ 072 Element(Element parent, String namespace, String name) 073 { 074 super(parent); 075 076 this.namespace = namespace; 077 this.name = name; 078 079 document = null; 080 } 081 082 @Override 083 public Document getDocument() 084 { 085 return document != null ? document : super.getDocument(); 086 } 087 088 /** 089 * Adds an attribute to the element, but only if the attribute name does not already exist. 090 * 091 * @param name the name of the attribute to add 092 * @param value the value for the attribute. A value of null is allowed, and no attribute will be added to the 093 * element. 094 */ 095 public Element attribute(String name, String value) 096 { 097 return attribute(null, name, value); 098 } 099 100 /** 101 * Adds a namespaced attribute to the element, but only if the attribute name does not already exist. 102 * 103 * @param namespace the namespace to contain the attribute, or null 104 * @param name the name of the attribute to add 105 * @param value the value for the attribute. A value of null is allowed, and no attribute will be added to the 106 * element. 107 */ 108 public Element attribute(String namespace, String name, String value) 109 { 110 assert InternalUtils.isNonBlank(name); 111 updateAttribute(namespace, name, value, false); 112 113 return this; 114 } 115 116 private void updateAttribute(String namespace, String name, String value, boolean force) 117 { 118 if (!force && value == null) 119 return; 120 121 Attribute prior = null; 122 Attribute cursor = firstAttribute; 123 124 while (cursor != null) 125 { 126 if (cursor.matches(namespace, name)) 127 { 128 if (!force) 129 return; 130 131 if (value != null) 132 { 133 cursor.value = value; 134 return; 135 } 136 137 // Remove this Attribute node from the linked list 138 139 if (prior == null) 140 firstAttribute = cursor.nextAttribute; 141 else 142 prior.nextAttribute = cursor.nextAttribute; 143 144 return; 145 } 146 147 prior = cursor; 148 cursor = cursor.nextAttribute; 149 } 150 151 // Don't add a Attribute if the value is null. 152 153 if (value == null) 154 return; 155 156 firstAttribute = new Attribute(this, namespace, name, value, firstAttribute); 157 } 158 159 /** 160 * Convenience for invoking {@link #attribute(String, String)} multiple times. 161 * 162 * @param namesAndValues alternating attribute names and attribute values 163 */ 164 public Element attributes(String... namesAndValues) 165 { 166 int i = 0; 167 while (i < namesAndValues.length) 168 { 169 String name = namesAndValues[i++]; 170 String value = namesAndValues[i++]; 171 172 attribute(name, value); 173 } 174 175 return this; 176 } 177 178 /** 179 * Forces changes to a number of attributes. The new attributes <em>overwrite</em> previous values. Overriding an 180 * attribute's value to null will remove the attribute entirely. 181 * 182 * @param namesAndValues alternating attribute names and attribute values 183 * @return this element 184 */ 185 public Element forceAttributes(String... namesAndValues) 186 { 187 return forceAttributesNS(null, namesAndValues); 188 } 189 190 /** 191 * Forces changes to a number of attributes in the global namespace. The new attributes <em>overwrite</em> previous 192 * values. Overriding attribute's value to null will remove the attribute entirely. 193 * TAP5-708: don't use element namespace for attributes 194 * 195 * @param namespace the namespace or null 196 * @param namesAndValues alternating attribute name and value 197 * @return this element 198 */ 199 public Element forceAttributesNS(String namespace, String... namesAndValues) 200 { 201 int i = 0; 202 203 while (i < namesAndValues.length) 204 { 205 String name = namesAndValues[i++]; 206 String value = namesAndValues[i++]; 207 208 updateAttribute(namespace, name, value, true); 209 } 210 211 return this; 212 } 213 214 /** 215 * Creates and returns a new Element node as a child of this node. 216 * 217 * @param name the name of the element to create 218 * @param namesAndValues alternating attribute names and attribute values 219 */ 220 public Element element(String name, String... namesAndValues) 221 { 222 assert InternalUtils.isNonBlank(name); 223 Element child = newChild(new Element(this, null, name)); 224 225 child.attributes(namesAndValues); 226 227 return child; 228 } 229 230 /** 231 * Inserts a new element before this element. 232 * 233 * @param name element name 234 * @param namesAndValues attribute names and values 235 * @return the new element 236 * @since 5.3 237 */ 238 public Element elementBefore(String name, String... namesAndValues) 239 { 240 assert InternalUtils.isNonBlank(name); 241 242 Element sibling = container.element(name, namesAndValues); 243 244 sibling.moveBefore(this); 245 246 return sibling; 247 } 248 249 250 /** 251 * Creates and returns a new Element within a namespace as a child of this node. 252 * 253 * @param namespace namespace to contain the element, or null 254 * @param name element name to create within the namespace 255 * @return the newly created element 256 */ 257 public Element elementNS(String namespace, String name) 258 { 259 assert InternalUtils.isNonBlank(name); 260 return newChild(new Element(this, namespace, name)); 261 } 262 263 /** 264 * Creates a new element, as a child of the current index, at the indicated index. 265 * 266 * @param index to insert at 267 * @param name element name 268 * @param namesAndValues attribute name / attribute value pairs 269 * @return the new element 270 */ 271 public Element elementAt(int index, String name, String... namesAndValues) 272 { 273 assert InternalUtils.isNonBlank(name); 274 Element child = new Element(this, null, name); 275 child.attributes(namesAndValues); 276 277 insertChildAt(index, child); 278 279 return child; 280 } 281 282 /** 283 * Adds the comment and returns this element for further construction. 284 */ 285 public Element comment(String text) 286 { 287 newChild(new Comment(this, text)); 288 289 return this; 290 } 291 292 /** 293 * Adds the raw text and returns this element for further construction. 294 */ 295 public Element raw(String text) 296 { 297 newChild(new Raw(this, text)); 298 299 return this; 300 } 301 302 /** 303 * Adds and returns a new text node (the text node is returned so that {@link Text#write(String)} or [@link 304 * {@link Text#writef(String, Object[])} may be invoked . 305 * 306 * @param text initial text for the node 307 * @return the new Text node 308 */ 309 public Text text(String text) 310 { 311 return newChild(new Text(this, text)); 312 } 313 314 /** 315 * Adds and returns a new CDATA node. 316 * 317 * @param content the content to be rendered by the node 318 * @return the newly created node 319 */ 320 public CData cdata(String content) 321 { 322 return newChild(new CData(this, content)); 323 } 324 325 private <T extends Node> T newChild(T child) 326 { 327 addChild(child); 328 329 return child; 330 } 331 332 @Override 333 void toMarkup(Document document, PrintWriter writer, Map<String, String> containerNamespacePrefixToURI) 334 { 335 Map<String, String> localNamespacePrefixToURI = createNamespaceURIToPrefix(containerNamespacePrefixToURI); 336 337 MarkupModel markupModel = document.getMarkupModel(); 338 339 StringBuilder builder = new StringBuilder(); 340 341 String prefixedElementName = toPrefixedName(localNamespacePrefixToURI, namespace, name); 342 343 builder.append("<").append(prefixedElementName); 344 345 // Output order used to be alpha sorted, but now it tends to be the inverse 346 // of the order in which attributes were added. 347 348 for (Attribute attr = firstAttribute; attr != null; attr = attr.nextAttribute) 349 { 350 attr.render(markupModel, builder, localNamespacePrefixToURI); 351 } 352 353 // Next, emit namespace declarations for each namespace. 354 355 List<String> namespaces = InternalUtils.sortedKeys(namespaceToPrefix); 356 357 for (String namespace : namespaces) 358 { 359 if (namespace.equals(Document.XML_NAMESPACE_URI)) 360 continue; 361 362 String prefix = namespaceToPrefix.get(namespace); 363 364 builder.append(" xmlns"); 365 366 if (!prefix.equals("")) 367 { 368 builder.append(":").append(prefix); 369 } 370 371 builder.append("="); 372 builder.append(markupModel.getAttributeQuote()); 373 374 markupModel.encodeQuoted(namespace, builder); 375 376 builder.append(markupModel.getAttributeQuote()); 377 } 378 379 EndTagStyle style = markupModel.getEndTagStyle(name); 380 381 boolean hasChildren = hasChildren(); 382 383 String close = (!hasChildren && style == EndTagStyle.ABBREVIATE) ? "/>" : ">"; 384 385 builder.append(close); 386 387 writer.print(builder.toString()); 388 389 if (hasChildren) 390 writeChildMarkup(document, writer, localNamespacePrefixToURI); 391 392 if (hasChildren || style == EndTagStyle.REQUIRE) 393 { 394 // TAP5-471: Avoid use of printf(). 395 writer.print("</"); 396 writer.print(prefixedElementName); 397 writer.print(">"); 398 } 399 } 400 401 String toPrefixedName(Map<String, String> namespaceURIToPrefix, String namespace, String name) 402 { 403 if (namespace == null || namespace.equals("")) 404 return name; 405 406 if (namespace.equals(Document.XML_NAMESPACE_URI)) 407 return "xml:" + name; 408 409 String prefix = namespaceURIToPrefix.get(namespace); 410 411 // This should never happen, because namespaces are automatically defined as needed. 412 413 if (prefix == null) 414 throw new IllegalArgumentException(String.format("No prefix has been defined for namespace '%s'.", 415 namespace)); 416 417 // The empty string indicates the default namespace which doesn't use a prefix. 418 419 if (prefix.equals("")) 420 return name; 421 422 return prefix + ":" + name; 423 } 424 425 /** 426 * Tries to find an element under this element (including itself) whose id is specified. 427 * Performs a width-first 428 * search of the document tree. 429 * 430 * @param id the value of the id attribute of the element being looked for 431 * @return the element if found. null if not found. 432 */ 433 public Element getElementById(final String id) 434 { 435 return getElementByAttributeValue("id", id); 436 } 437 438 /** 439 * Tries to find an element under this element (including itself) whose given attribute has a given value. 440 * 441 * @param attributeName the name of the attribute of the element being looked for 442 * @param attributeValue the value of the attribute of the element being looked for 443 * @return the element if found. null if not found. 444 * @since 5.2.3 445 */ 446 public Element getElementByAttributeValue(final String attributeName, final String attributeValue) 447 { 448 assert attributeName != null; 449 assert attributeValue != null; 450 451 return getElement(new Predicate<Element>() 452 { 453 public boolean accept(Element e) 454 { 455 String elementId = e.getAttribute(attributeName); 456 return attributeValue.equals(elementId); 457 } 458 }); 459 } 460 461 /** 462 * Tries to find an element under this element (including itself) accepted by the given predicate. 463 * 464 * @param predicate Predicate to accept the element 465 * @return the element if found. null if not found. 466 * @since 5.2.3 467 */ 468 public Element getElement(Predicate<Element> predicate) 469 { 470 LinkedList<Element> queue = CollectionFactory.newLinkedList(); 471 472 queue.add(this); 473 474 while (!queue.isEmpty()) 475 { 476 Element e = queue.removeFirst(); 477 478 if (predicate.accept(e)) 479 return e; 480 481 for (Element child : e.childElements()) 482 { 483 queue.addLast(child); 484 } 485 } 486 487 // Exhausted the entire tree 488 489 return null; 490 } 491 492 /** 493 * Searchs for a child element with a particular name below this element. The path parameter is a slash separated 494 * series of element names. 495 */ 496 public Element find(String path) 497 { 498 assert InternalUtils.isNonBlank(path); 499 Element search = this; 500 501 for (String name : TapestryInternalUtils.splitPath(path)) 502 { 503 search = search.findChildWithElementName(name); 504 505 if (search == null) 506 break; 507 } 508 509 return search; 510 } 511 512 private Element findChildWithElementName(String name) 513 { 514 for (Element child : childElements()) 515 { 516 if (child.getName().equals(name)) 517 return child; 518 } 519 520 // Not found. 521 522 return null; 523 } 524 525 private Iterable<Element> childElements() 526 { 527 return new Iterable<Element>() 528 { 529 public Iterator<Element> iterator() 530 { 531 return new Iterator<Element>() 532 { 533 private Node cursor = firstChild; 534 535 { 536 advance(); 537 } 538 539 private void advance() 540 { 541 while (cursor != null) 542 { 543 if (cursor instanceof Element) 544 return; 545 546 cursor = cursor.nextSibling; 547 } 548 } 549 550 public boolean hasNext() 551 { 552 return cursor != null; 553 } 554 555 public Element next() 556 { 557 Element result = (Element) cursor; 558 559 cursor = cursor.nextSibling; 560 561 advance(); 562 563 return result; 564 } 565 566 public void remove() 567 { 568 throw new UnsupportedOperationException("remove() not supported."); 569 } 570 }; 571 } 572 }; 573 } 574 575 public String getAttribute(String attributeName) 576 { 577 for (Attribute attr = firstAttribute; attr != null; attr = attr.nextAttribute) 578 { 579 if (attr.getName().equalsIgnoreCase(attributeName)) 580 return attr.value; 581 } 582 583 return null; 584 } 585 586 public String getName() 587 { 588 return name; 589 } 590 591 /** 592 * Adds one or more CSS class names to the "class" attribute. No check for duplicates is made. Note that CSS class 593 * names are case insensitive on the client. 594 * 595 * @param className one or more CSS class names 596 * @return the element for further configuration 597 */ 598 public Element addClassName(String... className) 599 { 600 String classes = getAttribute(CLASS_ATTRIBUTE); 601 602 StringBuilder builder = new StringBuilder(); 603 604 if (classes != null) 605 builder.append(classes); 606 607 for (String name : className) 608 { 609 if (builder.length() > 0) 610 builder.append(" "); 611 612 builder.append(name); 613 } 614 615 forceAttributes(CLASS_ATTRIBUTE, builder.toString()); 616 617 return this; 618 } 619 620 /** 621 * Defines a namespace for this element, mapping a URI to a prefix. This will affect how namespaced elements and 622 * attributes nested within the element are rendered, and will also cause <code>xmlns:</code> attributes (to define 623 * the namespace and prefix) to be rendered. 624 * 625 * @param namespace URI of the namespace 626 * @param namespacePrefix prefix 627 * @return this element 628 */ 629 public Element defineNamespace(String namespace, String namespacePrefix) 630 { 631 assert namespace != null; 632 assert namespacePrefix != null; 633 if (namespace.equals(Document.XML_NAMESPACE_URI)) 634 return this; 635 636 if (namespaceToPrefix == null) 637 namespaceToPrefix = CollectionFactory.newMap(); 638 639 namespaceToPrefix.put(namespace, namespacePrefix); 640 641 return this; 642 } 643 644 /** 645 * Returns the namespace for this element (which is typically a URL). The namespace may be null. 646 */ 647 public String getNamespace() 648 { 649 return namespace; 650 } 651 652 /** 653 * Removes an element; the element's children take the place of the node within its container. 654 */ 655 public void pop() 656 { 657 // Have to be careful because we'll be modifying the underlying list of children 658 // as we work, so we need a copy of the children. 659 660 List<Node> childrenCopy = CollectionFactory.newList(getChildren()); 661 662 for (Node child : childrenCopy) 663 { 664 child.moveBefore(this); 665 } 666 667 remove(); 668 } 669 670 /** 671 * Removes all children from this element. 672 * 673 * @return the element, for method chaining 674 */ 675 public Element removeChildren() 676 { 677 firstChild = null; 678 lastChild = null; 679 680 return this; 681 } 682 683 /** 684 * Creates the URI to namespace prefix map for this element, which reflects namespace mappings from containing 685 * elements. In addition, automatic namespaces are defined for any URIs that are not explicitly mapped (this occurs 686 * sometimes in Ajax partial render scenarios). 687 * 688 * @return a mapping from namespace URI to namespace prefix 689 */ 690 private Map<String, String> createNamespaceURIToPrefix(Map<String, String> containerNamespaceURIToPrefix) 691 { 692 MapHolder holder = new MapHolder(containerNamespaceURIToPrefix); 693 694 holder.putAll(namespaceToPrefix); 695 696 // result now contains all the mappings, including this element's. 697 698 // Add a mapping for the element's namespace. 699 700 if (InternalUtils.isNonBlank(namespace)) 701 { 702 703 // Add the namespace for the element as the default namespace. 704 705 if (!holder.getResult().containsKey(namespace)) 706 { 707 defineNamespace(namespace, ""); 708 holder.put(namespace, ""); 709 } 710 } 711 712 // And for any attributes that have a namespace. 713 714 for (Attribute attr = firstAttribute; attr != null; attr = attr.nextAttribute) 715 addMappingIfNeeded(holder, attr.getNamespace()); 716 717 return holder.getResult(); 718 } 719 720 private void addMappingIfNeeded(MapHolder holder, String namespace) 721 { 722 if (InternalUtils.isBlank(namespace)) 723 return; 724 725 Map<String, String> current = holder.getResult(); 726 727 if (current.containsKey(namespace)) 728 return; 729 730 // A missing namespace. 731 732 Set<String> prefixes = CollectionFactory.newSet(holder.getResult().values()); 733 734 // A clumsy way to find a unique id for the new namespace. 735 736 int i = 0; 737 while (true) 738 { 739 String prefix = "ns" + i; 740 741 if (!prefixes.contains(prefix)) 742 { 743 defineNamespace(namespace, prefix); 744 holder.put(namespace, prefix); 745 return; 746 } 747 748 i++; 749 } 750 } 751 752 @Override 753 protected Map<String, String> getNamespaceURIToPrefix() 754 { 755 MapHolder holder = new MapHolder(); 756 757 List<Element> elements = CollectionFactory.newList(this); 758 759 Element cursor = container; 760 761 while (cursor != null) 762 { 763 elements.add(cursor); 764 cursor = cursor.container; 765 } 766 767 // Reverse the list, so that later elements will overwrite earlier ones. 768 769 Collections.reverse(elements); 770 771 for (Element e : elements) 772 holder.putAll(e.namespaceToPrefix); 773 774 return holder.getResult(); 775 } 776 777 /** 778 * Returns true if the element has no children, or has only text children that contain only whitespace. 779 * 780 * @since 5.1.0.0 781 */ 782 public boolean isEmpty() 783 { 784 List<Node> children = getChildren(); 785 786 if (children.isEmpty()) 787 return true; 788 789 for (Node n : children) 790 { 791 if (n instanceof Text) 792 { 793 Text t = (Text) n; 794 795 if (t.isEmpty()) 796 continue; 797 } 798 799 // Not a text node, or a non-empty text node, then the element isn't empty. 800 return false; 801 } 802 803 return true; 804 } 805 806 /** 807 * Depth-first visitor traversal of this Element and its Element children. The traversal order is the same as render 808 * order. 809 * 810 * @param visitor callback 811 * @since 5.1.0.0 812 */ 813 public void visit(Visitor visitor) 814 { 815 Stack<Element> queue = CollectionFactory.newStack(); 816 817 queue.push(this); 818 819 while (!queue.isEmpty()) 820 { 821 Element e = queue.pop(); 822 823 visitor.visit(e); 824 825 e.queueChildren(queue); 826 } 827 } 828 829 private void queueChildren(Stack<Element> queue) 830 { 831 if (firstChild == null) 832 return; 833 834 List<Element> childElements = CollectionFactory.newList(); 835 836 for (Node cursor = firstChild; cursor != null; cursor = cursor.nextSibling) 837 { 838 if (cursor instanceof Element) 839 childElements.add((Element) cursor); 840 } 841 842 Collections.reverse(childElements); 843 844 for (Element e : childElements) 845 queue.push(e); 846 } 847 848 void addChild(Node child) 849 { 850 child.container = this; 851 852 if (lastChild == null) 853 { 854 firstChild = child; 855 lastChild = child; 856 return; 857 } 858 859 lastChild.nextSibling = child; 860 lastChild = child; 861 } 862 863 void insertChildAt(int index, Node newChild) 864 { 865 newChild.container = this; 866 867 if (index < 1) 868 { 869 newChild.nextSibling = firstChild; 870 firstChild = newChild; 871 } else 872 { 873 Node cursor = firstChild; 874 for (int i = 1; i < index; i++) 875 { 876 cursor = cursor.nextSibling; 877 } 878 879 newChild.nextSibling = cursor.nextSibling; 880 cursor.nextSibling = newChild; 881 } 882 883 if (index < 1) 884 firstChild = newChild; 885 886 if (newChild.nextSibling == null) 887 lastChild = newChild; 888 } 889 890 boolean hasChildren() 891 { 892 return firstChild != null; 893 } 894 895 void writeChildMarkup(Document document, PrintWriter writer, Map<String, String> namespaceURIToPrefix) 896 { 897 Node cursor = firstChild; 898 899 while (cursor != null) 900 { 901 cursor.toMarkup(document, writer, namespaceURIToPrefix); 902 903 cursor = cursor.nextSibling; 904 } 905 } 906 907 /** 908 * @return the concatenation of the String representations {@link #toString()} of its children. 909 */ 910 public final String getChildMarkup() 911 { 912 PrintOutCollector collector = new PrintOutCollector(); 913 914 writeChildMarkup(getDocument(), collector.getPrintWriter(), null); 915 916 return collector.getPrintOut(); 917 } 918 919 /** 920 * Returns an unmodifiable list of children for this element. Only {@link org.apache.tapestry5.dom.Element}s will 921 * have children. Also, note that unlike W3C DOM, attributes are not represented as 922 * {@link org.apache.tapestry5.dom.Node}s. 923 * 924 * @return unmodifiable list of children nodes 925 */ 926 @SuppressWarnings("unchecked") 927 public List<Node> getChildren() 928 { 929 List<Node> result = CollectionFactory.newList(); 930 Node cursor = firstChild; 931 932 while (cursor != null) 933 { 934 result.add(cursor); 935 cursor = cursor.nextSibling; 936 } 937 938 return result; 939 } 940 941 void remove(Node node) 942 { 943 Node prior = null; 944 Node cursor = firstChild; 945 946 while (cursor != null) 947 { 948 if (cursor == node) 949 { 950 Node afterNode = node.nextSibling; 951 952 if (prior != null) 953 prior.nextSibling = afterNode; 954 else 955 firstChild = afterNode; 956 957 // If node was the final node in the element then handle deletion. 958 // It's even possible node was the only node in the container. 959 960 if (lastChild == node) 961 { 962 lastChild = prior != null ? prior : null; 963 } 964 965 node.nextSibling = null; 966 967 return; 968 } 969 970 prior = cursor; 971 cursor = cursor.nextSibling; 972 } 973 974 throw new IllegalArgumentException("Node to remove was not present as a child of this element."); 975 } 976 977 void insertChildBefore(Node existing, Node node) 978 { 979 int index = indexOfNode(existing); 980 981 node.container = this; 982 983 insertChildAt(index, node); 984 } 985 986 void insertChildAfter(Node existing, Node node) 987 { 988 Node oldAfter = existing.nextSibling; 989 990 existing.nextSibling = node; 991 node.nextSibling = oldAfter; 992 993 if (oldAfter == null) 994 lastChild = node; 995 996 node.container = this; 997 } 998 999 int indexOfNode(Node node) 1000 { 1001 int index = 0; 1002 Node cursor = firstChild; 1003 1004 while (cursor != null) 1005 { 1006 if (node == cursor) 1007 return index; 1008 1009 cursor = cursor.nextSibling; 1010 index++; 1011 } 1012 1013 throw new IllegalArgumentException("Node not a child of this element."); 1014 } 1015 1016 /** 1017 * Returns the attributes for this Element as a (often empty) collection of 1018 * {@link org.apache.tapestry5.dom.Attribute}s. The order of the attributes within the collection is not specified. 1019 * Modifying the collection will not affect the attributes (use {@link #forceAttributes(String[])} to change 1020 * existing attribute values, and {@link #attribute(String, String, String)} to add new attribute values. 1021 * 1022 * @return attribute collection 1023 */ 1024 public Collection<Attribute> getAttributes() 1025 { 1026 Collection<Attribute> result = CollectionFactory.newList(); 1027 1028 for (Attribute a = firstAttribute; a != null; a = a.nextAttribute) 1029 { 1030 result.add(a); 1031 } 1032 1033 return result; 1034 } 1035 }