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