/* Element.java Purpose: Description: History: 2001/10/22 17:10:29, Create, Tom M. Yeh Copyright (C) 2001 Potix Corporation. All Rights Reserved. {{IS_RIGHT This program is distributed under LGPL Version 2.1 in the hope that it will be useful, but WITHOUT ANY WARRANTY. }}IS_RIGHT */ package org.zkoss.idom; import java.util.Map; import java.util.LinkedHashMap; import java.util.Collection; import java.util.List; import java.util.Iterator; import java.util.LinkedList; import java.util.Collections; import java.util.regex.Pattern; import org.w3c.dom.Attr; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.TypeInfo; import static org.zkoss.lang.Generics.cast; import org.zkoss.util.NotableLinkedList; import org.zkoss.idom.impl.FacadeNodeList; import org.zkoss.idom.impl.*; /** * The iDOM element. * * @author tomyeh * @see Attribute */ public class Element extends AbstractGroup implements Attributable, Namespaceable, org.w3c.dom.Element { /** The namespace. */ protected Namespace _ns; /** The local name. */ protected String _lname; /** The attributes. May be null. */ protected List<Attribute> _attrs = null; /** Additional namespaces. May be null*/ protected Map<String, Namespace> _addNamespaces = null; /** * Constructor. * * @param nsURI the namespace URI * @param tname the tag name */ public Element(String nsURI, String tname) { int kp = tname.indexOf(':'); String prefix = kp >= 0 ? tname.substring(0, kp): ""; String lname = kp >= 0 ? tname.substring(kp + 1): tname; setNamespace(prefix, nsURI); setLocalName(lname); } /** * Constructor. * * @param ns the namespace; if null, the default namespace is assumed * (not necessary {@link Namespace#NO_NAMESPACE}). * @param lname the local name */ public Element(Namespace ns, String lname) { setNamespace(ns); setLocalName(lname); } /** * Constructor without a namespace (i.e., {@link Namespace#NO_NAMESPACE}). * * @param lname the local name */ public Element(String lname) { this(Namespace.NO_NAMESPACE, lname); } /** * Constructor. * Unlike other constructors, it doesn't set the modification flag. */ protected Element() { _ns = Namespace.NO_NAMESPACE; } //-- Element extras --// /** * Tests whether this element is the root element of * the owning document. * * <p>Note: false is returned if it doesn't has any parent. */ public final boolean isRootElement() { return getParent() instanceof Document; } /** * Returns the Namespace in scope on this element for the given * prefix (this involves searching up the tree, so the results depend * on the current location of the element), or null if not found. * * <p>If prefix is empty, it searches for the "default" namespace * in scope. Thus, to search for attribute's namespace, caller * have to skip this one and use NO_NAMESPACE. * (due XML, an attribute without prefix is NO_NAMESPACE) * * @param prefix namespace prefix to look up; null for empty */ public final Namespace getNamespace(String prefix) { if (prefix == null) prefix = ""; Namespace ns = Namespace.getSpecial(prefix); if (ns != null) return ns; ns = getNamespace(); //might be null if in constructor if (ns != null && prefix.equals(ns.getPrefix())) return ns; if (_addNamespaces != null) { ns = _addNamespaces.get(prefix); if (ns != null) return ns; } if (getParent() instanceof Element) //only Element implements it; not Namespaceable return ((Element)getParent()).getNamespace(prefix); return prefix.length() > 0 ? null: Namespace.NO_NAMESPACE; } /** * Returns namespace declared on this element. * * <p>It is <i>not</i> a "live" representation. Also, it is read-only. * * <p>Note: Namespace.equals compares namespace's URI. However, * the distinction here is the prefix, because it is mainly for * getNamespace(prefix). * * @return the namespace declarations. */ public final Collection<Namespace> getDeclaredNamespaces() { if (_addNamespaces != null) return _addNamespaces.values(); return Collections.emptyList(); } /** * Adds a namespace to the namespace declaration. * * @return true if the namespace is added * @exception DOMException if the name space with the same prefix * already exists but with different URI */ public final boolean addDeclaredNamespace(Namespace ns) { if (_addNamespaces == null) { _addNamespaces = new LinkedHashMap<String, Namespace>(4); } else { final Namespace old = _addNamespaces.get(ns.getPrefix()); if (old != null) { if (!old.equals(ns)) throw new DOMException(DOMException.NAMESPACE_ERR, "Add a conflict namespace: "+ns+", while "+old+" already exists"); return false; } } _addNamespaces.put(ns.getPrefix(), ns); return true; } /** * Gets the content of this element. * * <p>The content of an element is the first Binary or Text child of * the element. Each element can has zero or one content. * * <p>Note: {@link #getText} returns the concatenation of all Text * children, not just the first one. * * @return the content of this element; null if no such child * @see #getContent(String) */ public final Object getContent() { for (final Iterator it = _children.iterator(); it.hasNext();) { Object o = it.next(); if (o instanceof Text) { return ((Text)o).getText(); } else if (o instanceof Binary) { return ((Binary)o).getValue(); } else if (o instanceof CData) { return ((CData)o).getText(); } } return null; } /** * Sets the content of this element. * * <p>All existent Binary or Text children of this element are removed * first. If the object is a String, a Text item is created to hold * it. Otherwise, a Binary item is created to hold it. * * <p>Non-Binary/Text children are preserved. * * <p>If obj is a {@link Item} or an array/collection of {@link Item}, * this method will add them as child vertices rather than * being the content. * Moreover, if the first item of the array/collection is {@link Item}, * it is assumed to be all valid component to being has valid vertices. * If not, an exception is thrown. * * <p>Thus, getContent might not return the object being set by setContent. * * @param obj the object to set; null is OK * @return the previous content * @see #getContent() */ public final Object setContent(Object obj) { if (obj instanceof Item) { getChildren().add((Item)obj); return null; //done } if (obj instanceof Collection) { final Collection c = (Collection)obj; final Iterator it = c.iterator(); if (it.hasNext() && (it.next() instanceof Item)) { final Collection<Item> ci = cast(c); getChildren().addAll(ci); return null; //done } } else if (obj instanceof Object[]) { Object[] ary = (Object[])obj; if (ary.length > 0 && (ary[0] instanceof Item)) { for (int j = 0; j < ary.length; ++j) getChildren().add((Item)ary[j]); return null; //done } } Object ret = null; boolean retFound = false; boolean bStr = obj instanceof String; for (final Iterator it = _children.iterator(); it.hasNext();) { Object o = it.next(); if (o instanceof Text) { if (!retFound) { retFound = true; ret = ((Text)o).getText(); } if (!bStr || obj == null) { it.remove(); } else { ((Text)o).setText((String)obj); obj = null; //then, the following will be removed } } else if (o instanceof Binary) { if (!retFound) { retFound = true; ret = ((Binary)o).getValue(); } if (bStr || obj == null) { it.remove(); } else { ((Binary)o).setValue(obj); obj = null; //then, the following will be removed } } else if (o instanceof CData) { if (!retFound) { retFound = true; ret = ((CData)o).getText(); } it.remove(); //always remove and add } } if (obj != null) _children.add(0, bStr ? new Text((String)obj): new Binary(obj)); return ret; } /** * Returns the content of the child element with the giving path, or * null if the content is null or the child element doesn't exist. * * <p>Note that there might be more than one child with the same path * in an iDOM tree; this method simply picks the first one that matches and * returns its content. To access certain one, you might use [n] to * [@attr = value] specify which one to access. * * * <p>To know whether the child element exists or content is null, * use {@link #hasContent}. * * <p>The content of an element is a special feature of iDOM. * Like a Map, it is designed to let developers use names (in a path-like * format) to access objects. See {@link #setContent(String, Object)}. * * <p>Like Unix path, the giving name could use '/' to concatenate a series * of child elements. * * <p>An empty path denotes this element itself. Leading, ending * and consecutive '/' will be ignored. * * <p>Example:<br> * <code>Object o = element.getContent("abc/def");<br> * String s = Objects.toString(element.getContent("ab/cd"));<br> * element.setContent("t:ab/cd/f:ef", new Integer(10));</code> * * <p>TODO: support [n] and [@attr = value] * * @param path a path; e.g., "b", "a/b", "t:a/t:b" * @see #getContent() */ public final Object getContent(String path) { Element e = this; int j = 0; while (true) { int k = path.indexOf('/', j); String tname = k >= 0 ? path.substring(j, k): path.substring(j); if (tname.length() > 0) { e = e.getElement(tname); if (e == null) return null; } if (k < 0) return e.getContent(); j = k + 1; } } /** * Tests whether the child element with the giving path exists. Note that * there might be more than one child with the same path in an iDOM tree; * this method simply tell you that "yes", at least on such path exist. * * To get the content, use {@link #getContent(String)}. */ public final boolean hasContent(String path) { Element e = this; int j = 0; while (true) { int k = path.indexOf('/', j); String tname = k >= 0 ? path.substring(j, k): path.substring(j); if (tname.length() > 0) { e = e.getElement(tname); if (e == null) return false; } if (k < 0) return true; j = k + 1; } } /** * Sets the content of the child element with the giving path. * * <p>Note that there might be more than one child with the same path * in an iDOM tree; this method simply pick one that matches and set * its content (see {@link #setContent(Object)}). * * <p>The content of an element is a special feature of iDOM. * Like a Map, it is designed to let developers use names (in a path-like * format) to access objects. See {@link #getContent(String)}. * * <p>Like Unix path, the giving name could use '/' to concatenate a series * of child elements. * * <p>An empty path denotes this element itself. Leading, ending * and consecutive '/' will be ignored. * * <p>If any element in the path is not found, it will be created * automatically. * * @param path a path; e.g., "b", "a/b", "t:a/t:b" * @param obj the object to set; null is acceptable * @return the previous content * * @see #setContent(Object) * @see #removeContent * @see #hasContent */ public final Object setContent(String path, Object obj) { Element e = this; int j = 0; while (true) { int k = path.indexOf('/', j); String tname = k >= 0 ? path.substring(j, k): path.substring(j); if (tname.length() > 0) { Element e2 = e.getElement(tname); if (e2 == null) { e2 = new Element(e.getNamespace().getURI(), tname); e.getChildren().add(e2); } e = e2; } if (k < 0) return e.setContent(obj); j = k + 1; } } /** * Removes the content of the child element with the giving path, * and the child element itself if no other child. * * <p>Unlike {@link #setContent(String, Object)} with null, * the child element identified by path will be detached if it has no * other child (but the content). So does its parent * <i>excluding</i> this element. Thus, removeContent(path) * could undo setContent(path, v). * * @return the previous content * @see #setContent(String, Object) */ public final Object removeContent(String path) { Element e = this; int j = 0; while (true) { int k = path.indexOf('/', j); String tname = k >= 0 ? path.substring(j, k): path.substring(j); if (tname.length() > 0) { e = e.getElement(tname); if (e == null) return null; } if (k < 0) { Object ret = e.setContent(null); //try to remove e; not including this for (Group group = e; group != this && group.getChildren().size() == 0;) { Group parent = group.getParent(); group.detach(); group = parent; } return ret; } j = k + 1; } } //-- utilities --// /** Returns the text of a child; never null. */ private static final String getTextOfChild(Object o) { if (!(o instanceof AbstractTextual)) return ""; final AbstractTextual t = (AbstractTextual)o; return t.isPartOfParentText() ? t.getText(): ""; } //-- Namespaceable --// /** * Sets the namespace. * If ns is null, the default namespace is assumed (not necessary * {@link Namespace#NO_NAMESPACE}. * <p>According W3C/DOM, unlike element, an attribute doesn't allow * a namespace that has an URI but without a prefix. */ public final void setNamespace(Namespace ns) { if (ns == null) { if (_ns != null && _ns.getPrefix().length() == 0) return; //nothing to do ns = getNamespace(""); if (ns == null) ns = Namespace.NO_NAMESPACE; } final Namespace old = getNamespace(ns.getPrefix()); if (old != null && old.equals(ns)) ns = old; //re-use if already defined _ns = ns; } /** Sets the namespace. */ public final void setNamespace(String prefix, String nsURI) { if (nsURI == null) nsURI = ""; final Namespace ns = getNamespace(prefix); if (ns != null) { if (ns.getURI().equals(nsURI)) { setNamespace(ns); return; } } setNamespace(new Namespace(prefix, nsURI)); } public final Namespace getNamespace() { return _ns; } public final String getTagName() { return _ns.tagNameOf(_lname); } public final void setTagName(String tname) { int kp = tname.indexOf(':'); String prefix = kp >= 0 ? tname.substring(0, kp): ""; String lname = kp >= 0 ? tname.substring(kp + 1): tname; setPrefix(prefix); setLocalName(lname); } public final String getLocalName() { return _lname; } public final void setLocalName(String lname) { Verifier.checkElementName(lname, getLocator()); _lname = lname; } //-- Item --// /** * Gets the tag name of the element -- the name with prefix. * To get the local name, use getLocalName. */ public final String getName() { return getTagName(); } /** * Sets the tag name of the element. * It will affect the local name and the namespace's prefix. */ public final void setName(String tname) { setTagName(tname); } /** Returns the concatenation of {@link Textual} children; never null. * Note: both <tag/> and <tag></tag> returns an * empty string. To tell the difference, check the number of children. * @see #getText(boolean) */ public final String getText() { if (_children.size() == 1) //optimize this case return getTextOfChild(_children.get(0)); final StringBuffer sb = new StringBuffer(256); for (final Iterator it = _children.iterator(); it.hasNext();) sb.append(getTextOfChild(it.next())); return sb.toString(); } /** Returns the concatenation of {@link Textual} children; never null. * * @param trim whether to trim before returning * @see #getText() */ public final String getText(boolean trim) { String t = getText(); return trim && t != null ? t.trim(): t; } //-- Attributable --// public final List<Attribute> getAttributeItems() { if (_attrs == null) _attrs = newAttrArray(); return _attrs; } /** Creates an empty list of attributes. */ protected List<Attribute> newAttrArray() { return new AttrArray(); } public final int getAttributeIndex (int indexFrom, String namespace, String name, int mode) { if (_attrs == null || indexFrom < 0 || indexFrom >= _attrs.size()) return -1; final Pattern ptn = (mode & FIND_BY_REGEX) != 0 ? Pattern.compile(name): null; final Iterator it = _attrs.listIterator(indexFrom); for (int j = indexFrom; it.hasNext(); ++j) if (match((Attribute)it.next(), namespace, name, ptn, mode)) return j; return -1; } public final int getAttributeIndex(int indexFrom, String tname) { return getAttributeIndex(indexFrom, null, tname, FIND_BY_TAGNAME); } public final Attribute getAttributeItem(String namespace, String name, int mode) { int j= getAttributeIndex(0, namespace, name, mode); return j >= 0 ? _attrs.get(j): null; } public final Attribute getAttributeItem(String tname) { int j= getAttributeIndex(0, tname); return j >= 0 ? _attrs.get(j): null; } public final List<Attribute> getAttributes(String namespace, String name, int mode) { if (_attrs == null) return Collections.emptyList(); Pattern ptn = (mode & FIND_BY_REGEX) != 0 ? Pattern.compile(name): null; final List<Attribute> list = new LinkedList<Attribute>(); for (Attribute attr: _attrs) if (match(attr, namespace, name, ptn, mode)) list.add(attr); return list; } public final Attribute setAttribute(Attribute attr) { int j = getAttributeIndex(0, attr.getTagName()); if (j >= 0) { return getAttributeItems().set(j, attr); } else { getAttributeItems().add(attr); return null; } } public final String getAttributeValue (String namespace, String name, int mode) { final Attribute attr = getAttributeItem(namespace, name, mode); return attr != null ? attr.getValue(): null; } public final String getAttributeValue(String tname) { Attribute attr = getAttributeItem(tname); return attr != null ? attr.getValue(): null; } public final Attribute setAttributeValue(String tname, String value) { Attribute attr = getAttributeItem(tname); if (attr != null) attr.setValue(value); else getAttributeItems().add(new Attribute(tname, value)); return attr; } //Cloneable// public Object clone() { Element elem = (Element)super.clone(); if (_addNamespaces != null) elem._addNamespaces = new LinkedHashMap<String, Namespace>(_addNamespaces); if (_attrs != null) { elem._attrs = elem.newAttrArray(); //NOTE: AttrArray is an inner class, so we must use the right //object to create the array. Here is 'elem'. for (Attribute attr: _attrs) elem._attrs.add((Attribute)attr.clone()); } return elem; } //-- Node --// public final short getNodeType() { return ELEMENT_NODE; } /** * Always null. Unlike other nodes, it is not the same as getText. */ public final String getNodeValue() { return null; } public final NamedNodeMap getAttributes() { return new AttrMap(); } public final boolean hasAttributes() { return _attrs != null && !_attrs.isEmpty(); } public final String getNamespaceURI() { return _ns.getURI(); } public final String getPrefix() { return _ns.getPrefix(); } public final void setPrefix(String prefix) { setNamespace(prefix, _ns.getURI()); } //-- Element --// public final NodeList getElementsByTagName(String tname) { return new FacadeNodeList( getElements(null, tname, FIND_BY_TAGNAME|FIND_RECURSIVE)); } public final NodeList getElementsByTagNameNS(String nsURI, String lname) { return new FacadeNodeList(getElements(nsURI, lname, FIND_RECURSIVE)); } public final Attr getAttributeNode(String tname) { return getAttributeItem(tname); } public final Attr getAttributeNodeNS(String nsURI, String lname) { return getAttributeItem(nsURI, lname, 0); } public final String getAttribute(String tname) { String val = getAttributeValue(tname); return val != null ? val: ""; //w3c spec } public final String getAttributeNS(String nsURI, String lname) { Attribute attr = getAttributeItem(nsURI, lname, 0); return attr != null ? attr.getValue(): ""; } public final void setAttribute(String tname, String value) { setAttributeValue(tname, value); } public final void setAttributeNS (String nsURI, String tname, String value) { int kp = tname.indexOf(':'); String prefix = kp >= 0 ? tname.substring(0, kp): ""; String lname = kp >= 0 ? tname.substring(kp + 1): tname; Attribute attr = getAttributeItem(nsURI, lname, 0); if (attr != null) { attr.setPrefix(prefix); //also change prefix attr.setValue(value); } else { getAttributeItems().add(new Attribute(nsURI, tname, value)); } } public final Attr setAttributeNode(Attr newAttr) { return setAttribute((Attribute)newAttr); } public final Attr setAttributeNodeNS(Attr newAttr) { Attribute attr = (Attribute)newAttr; int j = getAttributeIndex( 0, attr.getNamespace().getURI(), attr.getLocalName(), 0); if (j >= 0) { return getAttributeItems().set(j, (Attribute)newAttr); } else { getAttributeItems().add((Attribute)newAttr); return null; } } public final void removeAttribute(String tname) { int j = getAttributeIndex(0, tname); if (j >= 0) _attrs.remove(j); } public final void removeAttributeNS(String nsURI, String lname) { int j = getAttributeIndex(0, nsURI, lname, 0); if (j >= 0) _attrs.remove(j); } public final Attr removeAttributeNode(Attr oldAttr) { Attribute attr = (Attribute)oldAttr; int j = getAttributeIndex(0, attr.getTagName()); if (j >= 0) { return _attrs.remove(j); } else { throw new DOMException(DOMException.NOT_FOUND_ERR, getLocator()); } } public final boolean hasAttribute(String tname) { return getAttributeIndex(0, tname) >= 0; } public final boolean hasAttributeNS(String nsURI, String lname) { return getAttributeIndex(0, nsURI, lname, 0) >= 0; } public final String toString() { StringBuffer sb = new StringBuffer(64) .append("[Element: <").append(getTagName()); String uri = getNamespace().getURI(); if (uri.length() != 0) sb.append(" [").append(uri).append(']'); if (_attrs != null) { for (final Iterator it = _attrs.iterator(); it.hasNext();) { Attribute attr = (Attribute)it.next(); sb.append(' ').append(attr.getTagName()) .append("=\"").append(attr.getValue()).append('"'); } } return sb.append("/>]").toString(); } public TypeInfo getSchemaTypeInfo() { throw new UnsupportedOperationException("DOM Level 3"); } public void setIdAttribute(String name, boolean isId) throws DOMException { //Level 3 not yet } public void setIdAttributeNS(String namespaceURI, String localName, boolean isId) throws DOMException { //Level 3 not yet } public void setIdAttributeNode(Attr idAttr, boolean isId) throws DOMException { //Level 3 not yet } //-- AttrArray --// protected class AttrArray extends NotableLinkedList<Attribute> { protected AttrArray() { } //-- NotableLinkedList --// protected void onAdd(Attribute newElement, Attribute followingElement) { checkAdd(newElement, followingElement, false); } protected void onSet(Attribute newElement, Attribute replaced) { assert(replaced != null); checkAdd(newElement, replaced, true); } private void checkAdd(Attribute newItem, Attribute other, boolean replace) { //first, remove any existent with the same uri and name if (newItem.getOwner() != null) throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Attribute, "+newItem.toString()+", owned by other; detach or clone it", getLocator()); int j = getAttributeIndex(0, newItem.getTagName()); if (j >= 0 && (!replace || get(j) != other)) throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Attribute name, " + newItem.getTagName() +", is conflicts with existent one (" + j + ')', getLocator()); try { if (replace) onRemove(other); newItem.setOwner(Element.this); }catch(RuntimeException ex) { if (replace) { Attribute attrRep = other; if (attrRep.getOwner() == null) attrRep.setOwner(Element.this); //restore it } throw ex; } } protected void onRemove(Attribute item) { item.setOwner(null); } } protected class AttrMap implements NamedNodeMap { protected AttrMap() { } //-- NamedNodeMap --// public final int getLength() { return _attrs != null ? _attrs.size(): 0; } public final Node item(int index) { return index < 0 || index >= getLength() ? null: (Node)_attrs.get(index); } public final Node getNamedItem(String tname) { return getAttributeItem(tname); } public final Node getNamedItemNS(String nsURI, String lname) { return getAttributeItem(nsURI, lname, 0); } public final Node removeNamedItem(String tname) { int j = getAttributeIndex(0, tname); return j >= 0 ? (Node)_attrs.remove(j): null; } public final Node removeNamedItemNS(String nsURI, String lname) { int j = getAttributeIndex(0, nsURI, lname, 0); return j >= 0 ? (Node)_attrs.remove(j): null; } public final Node setNamedItem(Node node) { return setAttributeNode((Attr)node); } public final Node setNamedItemNS(Node node) { return setAttributeNodeNS((Attr)node); } } }