| // Copyright 2006, 2007, 2008, 2009, 2010 The Apache Software Foundation |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry5.dom; |
| |
| import java.io.PrintWriter; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.tapestry5.func.Predicate; |
| import org.apache.tapestry5.internal.TapestryInternalUtils; |
| import org.apache.tapestry5.internal.util.PrintOutCollector; |
| import org.apache.tapestry5.ioc.internal.util.CollectionFactory; |
| import org.apache.tapestry5.ioc.internal.util.InternalUtils; |
| import org.apache.tapestry5.ioc.util.Stack; |
| |
| /** |
| * An element that will render with a begin tag and attributes, a body, and an end tag. Also acts as a factory for |
| * enclosed Element, Text and Comment nodes. |
| * <p/> |
| * TODO: Support for CDATA nodes. Do we need Entity nodes? |
| */ |
| public final class Element extends Node |
| { |
| |
| private final String name; |
| |
| private Node firstChild; |
| |
| private Node lastChild; |
| |
| private Attribute firstAttribute; |
| |
| private final Document document; |
| |
| private static final String CLASS_ATTRIBUTE = "class"; |
| |
| /** |
| * URI of the namespace which contains the element. A quirk in XML is that the element may be in a namespace it |
| * defines itself, so resolving the namespace to a prefix must wait until render time (since the Element is created |
| * before the namespaces for it are defined). |
| */ |
| private final String namespace; |
| |
| private Map<String, String> namespaceToPrefix; |
| |
| /** |
| * Constructor for a root element. |
| */ |
| Element(Document container, String namespace, String name) |
| { |
| super(null); |
| |
| document = container; |
| this.namespace = namespace; |
| this.name = name; |
| } |
| |
| /** |
| * Constructor for a nested element. |
| */ |
| Element(Element parent, String namespace, String name) |
| { |
| super(parent); |
| |
| this.namespace = namespace; |
| this.name = name; |
| |
| document = null; |
| } |
| |
| @Override |
| public Document getDocument() |
| { |
| return document != null ? document : super.getDocument(); |
| } |
| |
| /** |
| * Returns the containing element for this element. This will be null for the root element of a document. |
| * |
| * @deprecated since 5.1.0.1, use {@link Node#getContainer()} instead |
| */ |
| public Element getParent() |
| { |
| return container; |
| } |
| |
| /** |
| * Adds an attribute to the element, but only if the attribute name does not already exist. |
| * |
| * @param name the name of the attribute to add |
| * @param value the value for the attribute. A value of null is allowed, and no attribute will be added to the |
| * element. |
| */ |
| public Element attribute(String name, String value) |
| { |
| return attribute(null, name, value); |
| } |
| |
| /** |
| * Adds a namespaced attribute to the element, but only if the attribute name does not already exist. |
| * |
| * @param namespace the namespace to contain the attribute, or null |
| * @param name the name of the attribute to add |
| * @param value the value for the attribute. A value of null is allowed, and no attribute will be added to the |
| * element. |
| */ |
| public Element attribute(String namespace, String name, String value) |
| { |
| assert InternalUtils.isNonBlank(name); |
| updateAttribute(namespace, name, value, false); |
| |
| return this; |
| } |
| |
| private void updateAttribute(String namespace, String name, String value, boolean force) |
| { |
| if (!force && value == null) return; |
| |
| Attribute prior = null; |
| Attribute cursor = firstAttribute; |
| |
| while (cursor != null) |
| { |
| if (cursor.matches(namespace, name)) |
| { |
| if (!force) return; |
| |
| if (value != null) |
| { |
| cursor.value = value; |
| return; |
| } |
| |
| // Remove this Attribute node from the linked list |
| |
| if (prior == null) |
| firstAttribute = cursor.nextAttribute; |
| else |
| prior.nextAttribute = cursor.nextAttribute; |
| |
| return; |
| } |
| |
| prior = cursor; |
| cursor = cursor.nextAttribute; |
| } |
| |
| // Don't add a Attribute if the value is null. |
| |
| if (value == null) return; |
| |
| firstAttribute = new Attribute(this, namespace, name, value, firstAttribute); |
| } |
| |
| |
| /** |
| * Convenience for invoking {@link #attribute(String, String)} multiple times. |
| * |
| * @param namesAndValues alternating attribute names and attribute values |
| */ |
| public Element attributes(String... namesAndValues) |
| { |
| int i = 0; |
| while (i < namesAndValues.length) |
| { |
| String name = namesAndValues[i++]; |
| String value = namesAndValues[i++]; |
| |
| attribute(name, value); |
| } |
| |
| return this; |
| } |
| |
| /** |
| * Forces changes to a number of attributes. The new attributes <em>overwrite</em> previous values. Overriding an |
| * attribute's value to null will remove the attribute entirely. |
| * |
| * @param namesAndValues alternating attribute names and attribute values |
| * @return this element |
| */ |
| public Element forceAttributes(String... namesAndValues) |
| { |
| return forceAttributesNS(null, namesAndValues); |
| } |
| |
| /** |
| * Forces changes to a number of attributes in the global namespace. The new attributes <em>overwrite</em> previous |
| * values. Overriding attribute's value to null will remove the attribute entirely. |
| * TAP5-708: don't use element namespace for attributes |
| * |
| * @param namespace the namespace or null |
| * @param namesAndValues alternating attribute name and value |
| * @return this element |
| * |
| */ |
| public Element forceAttributesNS(String namespace, String... namesAndValues) |
| { |
| int i = 0; |
| |
| while (i < namesAndValues.length) |
| { |
| String name = namesAndValues[i++]; |
| String value = namesAndValues[i++]; |
| |
| updateAttribute(namespace, name, value, true); |
| } |
| |
| return this; |
| } |
| |
| /** |
| * Creates and returns a new Element node as a child of this node. |
| * |
| * @param name the name of the element to create |
| * @param namesAndValues alternating attribute names and attribute values |
| */ |
| public Element element(String name, String... namesAndValues) |
| { |
| assert InternalUtils.isNonBlank(name); |
| Element child = newChild(new Element(this, null, name)); |
| |
| child.attributes(namesAndValues); |
| |
| return child; |
| } |
| |
| /** |
| * Creates and returns a new Element within a namespace as a child of this node. |
| * |
| * @param namespace namespace to contain the element, or null |
| * @param name element name to create within the namespace |
| * @return the newly created element |
| */ |
| public Element elementNS(String namespace, String name) |
| { |
| assert InternalUtils.isNonBlank(name); |
| return newChild(new Element(this, namespace, name)); |
| } |
| |
| public Element elementAt(int index, String name, String... namesAndValues) |
| { |
| assert InternalUtils.isNonBlank(name); |
| Element child = new Element(this, null, name); |
| child.attributes(namesAndValues); |
| |
| insertChildAt(index, child); |
| |
| return child; |
| } |
| |
| /** |
| * Adds the comment and returns this element for further construction. |
| */ |
| public Element comment(String text) |
| { |
| newChild(new Comment(this, text)); |
| |
| return this; |
| } |
| |
| /** |
| * Adds the raw text and returns this element for further construction. |
| */ |
| public Element raw(String text) |
| { |
| newChild(new Raw(this, text)); |
| |
| return this; |
| } |
| |
| /** |
| * Adds and returns a new text node (the text node is returned so that {@link Text#write(String)} or [@link {@link |
| * Text#writef(String, Object[])} may be invoked . |
| * |
| * @param text initial text for the node |
| * @return the new Text node |
| */ |
| public Text text(String text) |
| { |
| return newChild(new Text(this, text)); |
| } |
| |
| /** |
| * Adds and returns a new CDATA node. |
| * |
| * @param content the content to be rendered by the node |
| * @return the newly created node |
| */ |
| public CData cdata(String content) |
| { |
| return newChild(new CData(this, content)); |
| } |
| |
| |
| private <T extends Node> T newChild(T child) |
| { |
| addChild(child); |
| |
| return child; |
| } |
| |
| @Override |
| void toMarkup(Document document, PrintWriter writer, Map<String, String> containerNamespacePrefixToURI) |
| { |
| Map<String, String> localNamespacePrefixToURI = createNamespaceURIToPrefix(containerNamespacePrefixToURI); |
| |
| MarkupModel markupModel = document.getMarkupModel(); |
| |
| StringBuilder builder = new StringBuilder(); |
| |
| String prefixedElementName = toPrefixedName(localNamespacePrefixToURI, namespace, name); |
| |
| builder.append("<").append(prefixedElementName); |
| |
| // Output order used to be alpha sorted, but now it tends to be the inverse |
| // of the order in which attributes were added. |
| |
| for (Attribute attr = firstAttribute; attr != null; attr = attr.nextAttribute) |
| { |
| attr.render(markupModel, builder, localNamespacePrefixToURI); |
| } |
| |
| // Next, emit namespace declarations for each namespace. |
| |
| List<String> namespaces = InternalUtils.sortedKeys(namespaceToPrefix); |
| |
| for (String namespace : namespaces) |
| { |
| if (namespace.equals(Document.XML_NAMESPACE_URI)) continue; |
| |
| String prefix = namespaceToPrefix.get(namespace); |
| |
| builder.append(" xmlns"); |
| |
| if (!prefix.equals("")) |
| { |
| builder.append(":").append(prefix); |
| } |
| |
| builder.append("="); |
| builder.append(markupModel.getAttributeQuote()); |
| |
| markupModel.encodeQuoted(namespace, builder); |
| |
| builder.append(markupModel.getAttributeQuote()); |
| } |
| |
| EndTagStyle style = markupModel.getEndTagStyle(name); |
| |
| boolean hasChildren = hasChildren(); |
| |
| String close = (!hasChildren && style == EndTagStyle.ABBREVIATE) ? "/>" : ">"; |
| |
| builder.append(close); |
| |
| writer.print(builder.toString()); |
| |
| if (hasChildren) writeChildMarkup(document, writer, localNamespacePrefixToURI); |
| |
| // Dangerous -- perhaps it should be an error for a tag of type OMIT to even have children! |
| // We'll certainly be writing out unbalanced markup in that case. |
| |
| if (style == EndTagStyle.OMIT) return; |
| |
| if (hasChildren || style == EndTagStyle.REQUIRE) |
| { |
| // TAP5-471: Avoid use of printf(). |
| writer.print("</"); |
| writer.print(prefixedElementName); |
| writer.print(">"); |
| } |
| } |
| |
| String toPrefixedName(Map<String, String> namespaceURIToPrefix, String namespace, String name) |
| { |
| if (namespace == null || namespace.equals("")) return name; |
| |
| if (namespace.equals(Document.XML_NAMESPACE_URI)) return "xml:" + name; |
| |
| String prefix = namespaceURIToPrefix.get(namespace); |
| |
| // This should never happen, because namespaces are automatically defined as needed. |
| |
| if (prefix == null) |
| throw new IllegalArgumentException( |
| String.format("No prefix has been defined for namespace '%s'.", namespace)); |
| |
| // The empty string indicates the default namespace which doesn't use a prefix. |
| |
| if (prefix.equals("")) return name; |
| |
| return prefix + ":" + name; |
| } |
| |
| /** |
| * Tries to find an element under this element (including itself) whose id is specified. |
| * Performs a width-first |
| * search of the document tree. |
| * |
| * @param id |
| * the value of the id attribute of the element being looked for |
| * @return the element if found. null if not found. |
| */ |
| public Element getElementById(final String id) |
| { |
| return getElementByAttributeValue("id", id); |
| } |
| |
| /** |
| * Tries to find an element under this element (including itself) whose given attribute has a given value. |
| * |
| * @since 5.2.3 |
| * |
| * @param attributeName the name of the attribute of the element being looked for |
| * @param attributeValue |
| * the value of the attribute of the element being looked for |
| * @return the element if found. null if not found. |
| */ |
| public Element getElementByAttributeValue(final String attributeName, final String attributeValue) |
| { |
| assert attributeName != null; |
| assert attributeValue != null; |
| |
| return getElement(new Predicate<Element>() |
| { |
| |
| @Override |
| public boolean accept(Element e) |
| { |
| String elementId = e.getAttribute(attributeName); |
| return attributeValue.equals(elementId); |
| } |
| }); |
| } |
| |
| /** |
| * Tries to find an element under this element (including itself) accepted by the given predicate. |
| * |
| * @since 5.2.3 |
| * |
| * @param predicate Predicate to accept the element |
| * @return the element if found. null if not found. |
| */ |
| public Element getElement(Predicate<Element> predicate) |
| { |
| LinkedList<Element> queue = CollectionFactory.newLinkedList(); |
| |
| queue.add(this); |
| |
| while (!queue.isEmpty()) |
| { |
| Element e = queue.removeFirst(); |
| |
| if (predicate.accept(e)) return e; |
| |
| for (Element child : e.childElements()) |
| { |
| queue.addLast(child); |
| } |
| } |
| |
| // Exhausted the entire tree |
| |
| return null; |
| } |
| |
| |
| /** |
| * Searchs for a child element with a particular name below this element. The path parameter is a slash separated |
| * series of element names. |
| * |
| * @param path |
| * @return |
| */ |
| public Element find(String path) |
| { |
| assert InternalUtils.isNonBlank(path); |
| Element search = this; |
| |
| for (String name : TapestryInternalUtils.splitPath(path)) |
| { |
| search = search.findChildWithElementName(name); |
| |
| if (search == null) break; |
| } |
| |
| return search; |
| } |
| |
| private Element findChildWithElementName(String name) |
| { |
| for (Element child : childElements()) |
| { |
| if (child.getName().equals(name)) |
| return child; |
| } |
| |
| // Not found. |
| |
| return null; |
| } |
| |
| private Iterable<Element> childElements() |
| { |
| return new Iterable<Element>() |
| { |
| public Iterator<Element> iterator() |
| { |
| return new Iterator<Element>() |
| { |
| private Node cursor = firstChild; |
| |
| { |
| advance(); |
| } |
| |
| private void advance() |
| { |
| while (cursor != null) |
| { |
| if (cursor instanceof Element) return; |
| |
| cursor = cursor.nextSibling; |
| } |
| } |
| |
| public boolean hasNext() |
| { |
| return cursor != null; |
| } |
| |
| public Element next() |
| { |
| Element result = (Element) cursor; |
| |
| cursor = cursor.nextSibling; |
| |
| advance(); |
| |
| return result; |
| } |
| |
| public void remove() |
| { |
| throw new UnsupportedOperationException("remove() not supported."); |
| } |
| }; |
| } |
| }; |
| } |
| |
| public String getAttribute(String attributeName) |
| { |
| for (Attribute attr = firstAttribute; attr != null; attr = attr.nextAttribute) |
| { |
| if (attr.getName().equalsIgnoreCase(attributeName)) |
| return attr.value; |
| } |
| |
| return null; |
| } |
| |
| public String getName() |
| { |
| return name; |
| } |
| |
| /** |
| * Adds one or more CSS class names to the "class" attribute. No check for duplicates is made. Note that CSS class |
| * names are case insensitive on the client. |
| * |
| * @param className one or more CSS class names |
| * @return the element for further configuration |
| */ |
| public Element addClassName(String... className) |
| { |
| String classes = getAttribute(CLASS_ATTRIBUTE); |
| |
| StringBuilder builder = new StringBuilder(); |
| |
| if (classes != null) builder.append(classes); |
| |
| for (String name : className) |
| { |
| if (builder.length() > 0) builder.append(" "); |
| |
| builder.append(name); |
| } |
| |
| forceAttributes(CLASS_ATTRIBUTE, builder.toString()); |
| |
| return this; |
| } |
| |
| /** |
| * Defines a namespace for this element, mapping a URI to a prefix. This will affect how namespaced elements and |
| * attributes nested within the element are rendered, and will also cause <code>xmlns:</code> attributes (to define |
| * the namespace and prefix) to be rendered. |
| * |
| * @param namespace URI of the namespace |
| * @param namespacePrefix prefix |
| * @return this element |
| */ |
| public Element defineNamespace(String namespace, String namespacePrefix) |
| { |
| assert namespace != null; |
| assert namespacePrefix != null; |
| if (namespace.equals(Document.XML_NAMESPACE_URI)) |
| return this; |
| |
| if (namespaceToPrefix == null) |
| namespaceToPrefix = CollectionFactory.newMap(); |
| |
| namespaceToPrefix.put(namespace, namespacePrefix); |
| |
| return this; |
| } |
| |
| /** |
| * Returns the namespace for this element (which is typically a URL). The namespace may be null. |
| */ |
| public String getNamespace() |
| { |
| return namespace; |
| } |
| |
| /** |
| * Removes an element; the element's children take the place of the node within its container. |
| */ |
| public void pop() |
| { |
| // Have to be careful because we'll be modifying the underlying list of children |
| // as we work, so we need a copy of the children. |
| |
| List<Node> childrenCopy = CollectionFactory.newList(getChildren()); |
| |
| for (Node child : childrenCopy) |
| { |
| child.moveBefore(this); |
| } |
| |
| remove(); |
| } |
| |
| /** |
| * Removes all children from this element. |
| * |
| * @return the element, for method chaining |
| */ |
| public Element removeChildren() |
| { |
| firstChild = null; |
| lastChild = null; |
| |
| return this; |
| } |
| |
| /** |
| * Creates the URI to namespace prefix map for this element, which reflects namespace mappings from containing |
| * elements. In addition, automatic namespaces are defined for any URIs that are not explicitly mapped (this occurs |
| * sometimes in Ajax partial render scenarios). |
| * |
| * @return a mapping from namespace URI to namespace prefix |
| */ |
| private Map<String, String> createNamespaceURIToPrefix(Map<String, String> containerNamespaceURIToPrefix) |
| { |
| MapHolder holder = new MapHolder(containerNamespaceURIToPrefix); |
| |
| holder.putAll(namespaceToPrefix); |
| |
| |
| // result now contains all the mappings, including this element's. |
| |
| // Add a mapping for the element's namespace. |
| |
| if (InternalUtils.isNonBlank(namespace)) |
| { |
| |
| // Add the namespace for the element as the default namespace. |
| |
| if (!holder.getResult().containsKey(namespace)) |
| { |
| defineNamespace(namespace, ""); |
| holder.put(namespace, ""); |
| } |
| } |
| |
| // And for any attributes that have a namespace. |
| |
| for (Attribute attr = firstAttribute; attr != null; attr = attr.nextAttribute) |
| addMappingIfNeeded(holder, attr.getNamespace()); |
| |
| return holder.getResult(); |
| } |
| |
| private void addMappingIfNeeded(MapHolder holder, String namespace) |
| { |
| if (InternalUtils.isBlank(namespace)) return; |
| |
| Map<String, String> current = holder.getResult(); |
| |
| if (current.containsKey(namespace)) return; |
| |
| // A missing namespace. |
| |
| Set<String> prefixes = CollectionFactory.newSet(holder.getResult().values()); |
| |
| // A clumsy way to find a unique id for the new namespace. |
| |
| int i = 0; |
| while (true) |
| { |
| String prefix = "ns" + i; |
| |
| if (!prefixes.contains(prefix)) |
| { |
| defineNamespace(namespace, prefix); |
| holder.put(namespace, prefix); |
| return; |
| } |
| |
| i++; |
| } |
| } |
| |
| @Override |
| protected Map<String, String> getNamespaceURIToPrefix() |
| { |
| MapHolder holder = new MapHolder(); |
| |
| List<Element> elements = CollectionFactory.newList(this); |
| |
| Element cursor = container; |
| |
| while (cursor != null) |
| { |
| elements.add(cursor); |
| cursor = cursor.container; |
| } |
| |
| // Reverse the list, so that later elements will overwrite earlier ones. |
| |
| Collections.reverse(elements); |
| |
| for (Element e : elements) |
| holder.putAll(e.namespaceToPrefix); |
| |
| return holder.getResult(); |
| } |
| |
| /** |
| * Returns true if the element has no children, or has only text children that contain only whitespace. |
| * |
| * @since 5.1.0.0 |
| */ |
| public boolean isEmpty() |
| { |
| List<Node> children = getChildren(); |
| |
| if (children.isEmpty()) return true; |
| |
| for (Node n : children) |
| { |
| if (n instanceof Text) |
| { |
| Text t = (Text) n; |
| |
| if (t.isEmpty()) continue; |
| } |
| |
| // Not a text node, or a non-empty text node, then the element isn't empty. |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Depth-first visitor traversal of this Element and its Element children. The traversal order is the same as render |
| * order. |
| * |
| * @param visitor callback |
| * @since 5.1.0.0 |
| */ |
| public void visit(Visitor visitor) |
| { |
| Stack<Element> queue = CollectionFactory.newStack(); |
| |
| queue.push(this); |
| |
| while (!queue.isEmpty()) |
| { |
| Element e = queue.pop(); |
| |
| visitor.visit(e); |
| |
| e.queueChildren(queue); |
| } |
| } |
| |
| |
| private void queueChildren(Stack<Element> queue) |
| { |
| if (firstChild == null) return; |
| |
| List<Element> childElements = CollectionFactory.newList(); |
| |
| for (Node cursor = firstChild; cursor != null; cursor = cursor.nextSibling) |
| { |
| if (cursor instanceof Element) |
| childElements.add((Element) cursor); |
| } |
| |
| Collections.reverse(childElements); |
| |
| for (Element e : childElements) |
| queue.push(e); |
| } |
| |
| void addChild(Node child) |
| { |
| child.container = this; |
| |
| if (lastChild == null) |
| { |
| firstChild = child; |
| lastChild = child; |
| return; |
| } |
| |
| lastChild.nextSibling = child; |
| lastChild = child; |
| } |
| |
| void insertChildAt(int index, Node newChild) |
| { |
| newChild.container = this; |
| |
| if (index < 1) |
| { |
| newChild.nextSibling = firstChild; |
| firstChild = newChild; |
| } |
| else |
| { |
| Node cursor = firstChild; |
| for (int i = 1; i < index; i++) |
| { |
| cursor = cursor.nextSibling; |
| } |
| |
| |
| newChild.nextSibling = cursor.nextSibling; |
| cursor.nextSibling = newChild; |
| } |
| |
| if (index < 1) |
| firstChild = newChild; |
| |
| if (newChild.nextSibling == null) |
| lastChild = newChild; |
| } |
| |
| boolean hasChildren() |
| { |
| return firstChild != null; |
| } |
| |
| void writeChildMarkup(Document document, PrintWriter writer, Map<String, String> namespaceURIToPrefix) |
| { |
| Node cursor = firstChild; |
| |
| while (cursor != null) |
| { |
| cursor.toMarkup(document, writer, namespaceURIToPrefix); |
| |
| cursor = cursor.nextSibling; |
| } |
| } |
| |
| /** |
| * @return the concatenation of the String representations {@link #toString()} of its children. |
| */ |
| public final String getChildMarkup() |
| { |
| PrintOutCollector collector = new PrintOutCollector(); |
| |
| writeChildMarkup(getDocument(), collector.getPrintWriter(), null); |
| |
| return collector.getPrintOut(); |
| } |
| |
| /** |
| * Returns an unmodifiable list of children for this element. Only {@link org.apache.tapestry5.dom.Element}s will |
| * have children. Also, note that unlike W3C DOM, attributes are not represented as {@link |
| * org.apache.tapestry5.dom.Node}s. |
| * |
| * @return unmodifiable list of children nodes |
| */ |
| @SuppressWarnings("unchecked") |
| public List<Node> getChildren() |
| { |
| List<Node> result = CollectionFactory.newList(); |
| Node cursor = firstChild; |
| |
| while (cursor != null) |
| { |
| result.add(cursor); |
| cursor = cursor.nextSibling; |
| } |
| |
| return result; |
| } |
| |
| void remove(Node node) |
| { |
| Node prior = null; |
| Node cursor = firstChild; |
| |
| while (cursor != null) |
| { |
| if (cursor == node) |
| { |
| Node afterNode = node.nextSibling; |
| |
| if (prior != null) |
| prior.nextSibling = afterNode; |
| else |
| firstChild = afterNode; |
| |
| // If node was the final node in the element then handle deletion. |
| // It's even possible node was the only node in the container. |
| |
| if (lastChild == node) |
| { |
| lastChild = prior != null ? prior : null; |
| } |
| |
| return; |
| } |
| |
| prior = cursor; |
| cursor = cursor.nextSibling; |
| } |
| |
| throw new IllegalArgumentException("Node to remove was not present as a child of this element."); |
| } |
| |
| void insertChildBefore(Node existing, Node node) |
| { |
| int index = indexOfNode(existing); |
| |
| node.container = this; |
| |
| insertChildAt(index, node); |
| } |
| |
| void insertChildAfter(Node existing, Node node) |
| { |
| Node oldAfter = existing.nextSibling; |
| |
| existing.nextSibling = node; |
| node.nextSibling = oldAfter; |
| |
| if (oldAfter == null) |
| lastChild = node; |
| |
| node.container = this; |
| } |
| |
| int indexOfNode(Node node) |
| { |
| int index = 0; |
| Node cursor = firstChild; |
| |
| while (cursor != null) |
| { |
| if (node == cursor) return index; |
| |
| cursor = cursor.nextSibling; |
| index++; |
| } |
| |
| throw new IllegalArgumentException("Node not a child of this element."); |
| } |
| |
| /** |
| * Returns the attributes for this Element as a (often empty) collection of {@link |
| * org.apache.tapestry5.dom.Attribute}s. The order of the attributes within the collection is not specified. |
| * Modifying the collection will not affect the attributes (use {@link #forceAttributes(String[])} to change |
| * existing attribute values, and {@link #attribute(String, String, String)} to add new attribute values. |
| * |
| * @return attribute collection |
| */ |
| public Collection<Attribute> getAttributes() |
| { |
| Collection<Attribute> result = CollectionFactory.newList(); |
| |
| for (Attribute a = firstAttribute; a != null; a = a.nextAttribute) |
| { |
| result.add(a); |
| } |
| |
| return result; |
| } |
| } |