    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    }