/* 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);
}
}
}