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