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