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