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 }