    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}