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
015package org.apache.tapestry5.dom;
016
017import org.apache.tapestry5.func.Predicate;
018import org.apache.tapestry5.internal.TapestryInternalUtils;
019import org.apache.tapestry5.internal.util.PrintOutCollector;
020import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
021import org.apache.tapestry5.ioc.internal.util.InternalUtils;
022import org.apache.tapestry5.ioc.util.Stack;
023
024import java.io.PrintWriter;
025import 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 */
033public 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}