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