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