| /* DomNode.java -- |
| Copyright (C) 1999,2000,2001,2004 Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA |
| 02111-1307 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| package gnu.xml.dom; |
| |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.DOMException; |
| import org.w3c.dom.DOMImplementation; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.w3c.dom.Text; |
| import org.w3c.dom.UserDataHandler; |
| import org.w3c.dom.events.DocumentEvent; |
| import org.w3c.dom.events.Event; |
| import org.w3c.dom.events.EventException; |
| import org.w3c.dom.events.EventListener; |
| import org.w3c.dom.events.EventTarget; |
| import org.w3c.dom.events.MutationEvent; |
| import org.w3c.dom.traversal.NodeFilter; |
| import org.w3c.dom.traversal.NodeIterator; |
| |
| /** |
| * <p> "Node", "EventTarget", and "DocumentEvent" implementation. |
| * This provides most of the core DOM functionality; only more |
| * specialized features are provided by subclasses. Those subclasses may |
| * have some particular constraints they must implement, by overriding |
| * methods defined here. Such constraints are noted here in the method |
| * documentation. </p> |
| * |
| * <p> Note that you can create events with type names prefixed with "USER-", |
| * and pass them through this DOM. This lets you use the DOM event scheme |
| * for application specific purposes, although you must use a predefined event |
| * structure (such as MutationEvent) to pass data along with those events. |
| * Test for existence of this feature with the "USER-Events" DOM feature |
| * name.</p> |
| * |
| * <p> Other kinds of events you can send include the "html" events, |
| * like "load", "unload", "abort", "error", and "blur"; and the mutation |
| * events. If this DOM has been compiled with mutation event support |
| * enabled, it will send mutation events when you change parts of the |
| * tree; otherwise you may create and send such events yourself, but |
| * they won't be generated by the DOM itself. </p> |
| * |
| * <p> Note that there is a namespace-aware name comparison method, |
| * <em>nameAndTypeEquals</em>, which compares the names (and types) of |
| * two nodes in conformance with the "Namespaces in XML" specification. |
| * While mostly intended for use with elements and attributes, this should |
| * also be helpful for ProcessingInstruction nodes and some others which |
| * do not have namespace URIs. |
| * |
| * @author David Brownell |
| * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a> |
| */ |
| public abstract class DomNode |
| implements Node, NodeList, EventTarget, DocumentEvent, Cloneable, Comparable |
| { |
| |
| // package private |
| //final static String xmlNamespace = "http://www.w3.org/XML/1998/namespace"; |
| //final static String xmlnsURI = "http://www.w3.org/2000/xmlns/"; |
| |
| // tunable |
| // NKIDS_* affects arrays of children (which grow) |
| // (currently) fixed size: |
| // ANCESTORS_* is for event capture/bubbling, # ancestors |
| // NOTIFICATIONS_* is for per-node event delivery, # events |
| private static final int NKIDS_DELTA = 8; |
| private static final int ANCESTORS_INIT = 20; |
| private static final int NOTIFICATIONS_INIT = 10; |
| |
| // tunable: enable mutation events or not? Enabling it costs about |
| // 10-15% in DOM construction time, last time it was measured. |
| |
| // package private !!! |
| static final boolean reportMutations = true; |
| |
| // locking protocol changeable only within this class |
| private static final Object lockNode = new Object(); |
| |
| // NON-FINAL class data |
| |
| // Optimize event dispatch by not allocating memory each time |
| private static boolean dispatchDataLock; |
| private static DomNode[] ancestors = new DomNode[ANCESTORS_INIT]; |
| private static ListenerRecord[] notificationSet |
| = new ListenerRecord[NOTIFICATIONS_INIT]; |
| |
| // Ditto for the (most common) event object itself! |
| private static boolean eventDataLock; |
| private static DomEvent.DomMutationEvent mutationEvent |
| = new DomEvent.DomMutationEvent(null); |
| |
| // |
| // PER-INSTANCE DATA |
| // |
| |
| DomDocument owner; |
| DomNode parent; // parent node; |
| DomNode previous; // previous sibling node |
| DomNode next; // next sibling node |
| DomNode first; // first child node |
| DomNode last; // last child node |
| int index; // index of this node in its parent's children |
| int depth; // depth of the node in the document |
| int length; // number of children |
| final short nodeType; |
| |
| // Bleech ... "package private" so a builder can populate entity refs. |
| // writable during construction. DOM spec is nasty. |
| boolean readonly; |
| |
| // event registrations |
| private ListenerRecord[] listeners; |
| private int nListeners; |
| |
| // DOM Level 3 userData dictionary. |
| private HashMap userData; |
| private HashMap userDataHandlers; |
| |
| // |
| // Some of the methods here are declared 'final' because |
| // knowledge about their implementation is built into this |
| // class -- for both integrity and performance. |
| // |
| |
| /** |
| * Reduces space utilization for this node. |
| */ |
| public void compact() |
| { |
| if (listeners != null && listeners.length != nListeners) |
| { |
| if (nListeners == 0) |
| { |
| listeners = null; |
| } |
| else |
| { |
| ListenerRecord[] l = new ListenerRecord[nListeners]; |
| System.arraycopy(listeners, 0, l, 0, nListeners); |
| listeners = l; |
| } |
| } |
| } |
| |
| /** |
| * Constructs a node and associates it with its owner. Only |
| * Document and DocumentType nodes may be created with no owner, |
| * and DocumentType nodes get an owner as soon as they are |
| * associated with a document. |
| */ |
| protected DomNode(short nodeType, DomDocument owner) |
| { |
| this.nodeType = nodeType; |
| |
| if (owner == null) |
| { |
| // DOM calls never go down this path |
| if (nodeType != DOCUMENT_NODE && nodeType != DOCUMENT_TYPE_NODE) |
| { |
| throw new IllegalArgumentException ("no owner!"); |
| } |
| } |
| this.owner = owner; |
| } |
| |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns null; Element subclasses must override this method. |
| */ |
| public NamedNodeMap getAttributes() |
| { |
| return null; |
| } |
| |
| /** |
| * <b>DOM L2></b> |
| * Returns true iff this is an element node with attributes. |
| */ |
| public boolean hasAttributes() |
| { |
| return false; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns a list, possibly empty, of the children of this node. |
| * In this implementation, to conserve memory, nodes are the same |
| * as their list of children. This can have ramifications for |
| * subclasses, which may need to provide their own getLength method |
| * for reasons unrelated to the NodeList method of the same name. |
| */ |
| public NodeList getChildNodes() |
| { |
| return this; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns the first child of this node, or null if there are none. |
| */ |
| public Node getFirstChild() |
| { |
| return first; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns the last child of this node, or null if there are none. |
| */ |
| public Node getLastChild() |
| { |
| return last; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns true if this node has children. |
| */ |
| public boolean hasChildNodes() |
| { |
| return length != 0; |
| } |
| |
| |
| /** |
| * Exposes the internal "readonly" flag. In DOM, children of |
| * entities and entity references are readonly, as are the |
| * objects associated with DocumentType objets. |
| */ |
| public final boolean isReadonly() |
| { |
| return readonly; |
| } |
| |
| /** |
| * Sets the internal "readonly" flag so this subtree can't be changed. |
| * Subclasses need to override this method for any associated content |
| * that's not a child node, such as an element's attributes or the |
| * (few) declarations associated with a DocumentType. |
| */ |
| public void makeReadonly() |
| { |
| readonly = true; |
| for (DomNode child = first; child != null; child = child.next) |
| { |
| child.makeReadonly(); |
| } |
| } |
| |
| /** |
| * Used to adopt a node to a new document. |
| */ |
| void setOwner(DomDocument doc) |
| { |
| this.owner = doc; |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) |
| { |
| ctx.setOwner(doc); |
| } |
| } |
| |
| // just checks the node for inclusion -- may be called many |
| // times (docfrag) before anything is allowed to change |
| private void checkMisc(DomNode child) |
| { |
| if (readonly && !owner.building) |
| { |
| throw new DomDOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, |
| null, this, 0); |
| } |
| for (DomNode ctx = this; ctx != null; ctx = ctx.parent) |
| { |
| if (child == ctx) |
| { |
| throw new DomDOMException(DOMException.HIERARCHY_REQUEST_ERR, |
| "can't make ancestor into a child", |
| this, 0); |
| } |
| } |
| |
| DomDocument owner = (nodeType == DOCUMENT_NODE) ? (DomDocument) this : |
| this.owner; |
| DomDocument childOwner = child.owner; |
| short childNodeType = child.nodeType; |
| |
| if (childOwner != owner) |
| { |
| // new in DOM L2, this case -- patch it up later, in reparent() |
| if (!(childNodeType == DOCUMENT_TYPE_NODE && childOwner == null)) |
| { |
| throw new DomDOMException(DOMException.WRONG_DOCUMENT_ERR, |
| null, child, 0); |
| } |
| } |
| |
| // enforce various structural constraints |
| switch (nodeType) |
| { |
| case DOCUMENT_NODE: |
| switch (childNodeType) |
| { |
| case ELEMENT_NODE: |
| case PROCESSING_INSTRUCTION_NODE: |
| case COMMENT_NODE: |
| case DOCUMENT_TYPE_NODE: |
| return; |
| } |
| break; |
| |
| case ATTRIBUTE_NODE: |
| switch (childNodeType) |
| { |
| case TEXT_NODE: |
| case ENTITY_REFERENCE_NODE: |
| return; |
| } |
| break; |
| |
| case DOCUMENT_FRAGMENT_NODE: |
| case ENTITY_REFERENCE_NODE: |
| case ELEMENT_NODE: |
| case ENTITY_NODE: |
| switch (childNodeType) |
| { |
| case ELEMENT_NODE: |
| case TEXT_NODE: |
| case COMMENT_NODE: |
| case PROCESSING_INSTRUCTION_NODE: |
| case CDATA_SECTION_NODE: |
| case ENTITY_REFERENCE_NODE: |
| return; |
| } |
| break; |
| } |
| if (owner.checkingWellformedness) |
| { |
| throw new DomDOMException(DOMException.HIERARCHY_REQUEST_ERR, |
| "can't append " + |
| nodeTypeToString(childNodeType) + |
| " to node of type " + |
| nodeTypeToString(nodeType), |
| this, 0); |
| } |
| } |
| |
| // Here's hoping a good optimizer will detect the case when the |
| // next several methods are never called, and won't allocate |
| // object code space of any kind. (Case: not reporting any |
| // mutation events. We can also remove some static variables |
| // listed above.) |
| |
| private void insertionEvent(DomEvent.DomMutationEvent event, |
| DomNode target) |
| { |
| if (owner == null || owner.building) |
| { |
| return; |
| } |
| boolean doFree = false; |
| |
| if (event == null) |
| { |
| event = getMutationEvent(); |
| } |
| if (event != null) |
| { |
| doFree = true; |
| } |
| else |
| { |
| event = new DomEvent.DomMutationEvent(null); |
| } |
| event.initMutationEvent("DOMNodeInserted", |
| true /* bubbles */, false /* nocancel */, |
| this /* related */, null, null, null, (short) 0); |
| target.dispatchEvent(event); |
| |
| // XXX should really visit every descendant of 'target' |
| // and sent a DOMNodeInsertedIntoDocument event to it... |
| // bleech, there's no way to keep that acceptably fast. |
| |
| if (doFree) |
| { |
| event.target = null; |
| event.relatedNode = null; |
| event.currentNode = null; |
| eventDataLock = false; |
| } // else we created work for the GC |
| } |
| |
| private void removalEvent(DomEvent.DomMutationEvent event, |
| DomNode target) |
| { |
| if (owner == null || owner.building) |
| { |
| return; |
| } |
| boolean doFree = false; |
| |
| if (event == null) |
| { |
| event = getMutationEvent(); |
| } |
| if (event != null) |
| { |
| doFree = true; |
| } |
| else |
| { |
| event = new DomEvent.DomMutationEvent(null); |
| } |
| event.initMutationEvent("DOMNodeRemoved", |
| true /* bubbles */, false /* nocancel */, |
| this /* related */, null, null, null, (short) 0); |
| target.dispatchEvent(event); |
| |
| // XXX should really visit every descendant of 'target' |
| // and sent a DOMNodeRemovedFromDocument event to it... |
| // bleech, there's no way to keep that acceptably fast. |
| |
| event.target = null; |
| event.relatedNode = null; |
| event.currentNode = null; |
| if (doFree) |
| { |
| eventDataLock = false; |
| } |
| // else we created more work for the GC |
| } |
| |
| // |
| // Avoid creating lots of memory management work, by using a simple |
| // allocation strategy for the mutation event objects that get used |
| // at least once per tree modification. We can't use stack allocation, |
| // so we do the next simplest thing -- more or less, static allocation. |
| // Concurrent notifications should be rare, anyway. |
| // |
| // Returns the preallocated object, which needs to be carefully freed, |
| // or null to indicate the caller needs to allocate their own. |
| // |
| static private DomEvent.DomMutationEvent getMutationEvent() |
| { |
| synchronized (lockNode) |
| { |
| if (eventDataLock) |
| { |
| return null; |
| } |
| eventDataLock = true; |
| return mutationEvent; |
| } |
| } |
| |
| // NOTE: this is manually inlined in the insertion |
| // and removal event methods above; change in sync. |
| static private void freeMutationEvent() |
| { |
| // clear fields to enable GC |
| mutationEvent.clear(); |
| eventDataLock = false; |
| } |
| |
| void setDepth(int depth) |
| { |
| this.depth = depth; |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) |
| { |
| ctx.setDepth(depth + 1); |
| } |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Appends the specified node to this node's list of children. |
| * Document subclasses must override this to enforce the restrictions |
| * that there be only one element and document type child. |
| * |
| * <p> Causes a DOMNodeInserted mutation event to be reported. |
| * Will first cause a DOMNodeRemoved event to be reported if the |
| * parameter already has a parent. If the new child is a document |
| * fragment node, both events will be reported for each child of |
| * the fragment; the order in which children are removed and |
| * inserted is implementation-specific. |
| * |
| * <p> If this DOM has been compiled without mutation event support, |
| * these events will not be reported. |
| */ |
| public Node appendChild(Node newChild) |
| { |
| try |
| { |
| DomNode child = (DomNode) newChild; |
| |
| if (child.nodeType == DOCUMENT_FRAGMENT_NODE) |
| { |
| // Append all nodes in the fragment to this node |
| for (DomNode ctx = child.first; ctx != null; ctx = ctx.next) |
| { |
| checkMisc(ctx); |
| } |
| for (DomNode ctx = child.first; ctx != null; ) |
| { |
| DomNode ctxNext = ctx.next; |
| appendChild(ctx); |
| ctx = ctxNext; |
| } |
| } |
| else |
| { |
| checkMisc(child); |
| if (child.parent != null) |
| { |
| child.parent.removeChild(child); |
| } |
| child.parent = this; |
| child.index = length++; |
| child.setDepth(depth + 1); |
| child.next = null; |
| if (last == null) |
| { |
| first = child; |
| child.previous = null; |
| } |
| else |
| { |
| last.next = child; |
| child.previous = last; |
| } |
| last = child; |
| |
| if (reportMutations) |
| { |
| insertionEvent(null, child); |
| } |
| } |
| |
| return child; |
| } |
| catch (ClassCastException e) |
| { |
| throw new DomDOMException(DOMException.WRONG_DOCUMENT_ERR, |
| null, newChild, 0); |
| } |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Inserts the specified node in this node's list of children. |
| * Document subclasses must override this to enforce the restrictions |
| * that there be only one element and document type child. |
| * |
| * <p> Causes a DOMNodeInserted mutation event to be reported. Will |
| * first cause a DOMNodeRemoved event to be reported if the newChild |
| * parameter already has a parent. If the new child is a document |
| * fragment node, both events will be reported for each child of |
| * the fragment; the order in which children are removed and inserted |
| * is implementation-specific. |
| * |
| * <p> If this DOM has been compiled without mutation event support, |
| * these events will not be reported. |
| */ |
| public Node insertBefore(Node newChild, Node refChild) |
| { |
| if (refChild == null) |
| { |
| return appendChild(newChild); |
| } |
| |
| try |
| { |
| DomNode child = (DomNode) newChild; |
| DomNode ref = (DomNode) refChild; |
| |
| if (child.nodeType == DOCUMENT_FRAGMENT_NODE) |
| { |
| // Append all nodes in the fragment to this node |
| for (DomNode ctx = child.first; ctx != null; ctx = ctx.next) |
| { |
| checkMisc(ctx); |
| } |
| for (DomNode ctx = child.first; ctx != null; ) |
| { |
| DomNode ctxNext = ctx.next; |
| insertBefore(ctx, ref); |
| ctx = ctxNext; |
| } |
| } |
| else |
| { |
| checkMisc(child); |
| if (ref == null || ref.parent != this) |
| { |
| throw new DomDOMException(DOMException.NOT_FOUND_ERR, |
| null, ref, 0); |
| } |
| if (ref == child) |
| { |
| throw new DomDOMException(DOMException.HIERARCHY_REQUEST_ERR, |
| "can't insert node before itself", |
| ref, 0); |
| } |
| |
| if (child.parent != null) |
| { |
| child.parent.removeChild(child); |
| } |
| child.parent = this; |
| int i = ref.index; |
| child.setDepth(depth + 1); |
| child.next = ref; |
| if (ref.previous != null) |
| { |
| ref.previous.next = child; |
| } |
| child.previous = ref.previous; |
| ref.previous = child; |
| if (first == ref) |
| { |
| first = child; |
| } |
| // index renumbering |
| for (DomNode ctx = child; ctx != null; ctx = ctx.next) |
| { |
| ctx.index = i++; |
| } |
| |
| if (reportMutations) |
| { |
| insertionEvent(null, child); |
| } |
| } |
| |
| return child; |
| } |
| catch (ClassCastException e) |
| { |
| throw new DomDOMException(DOMException.WRONG_DOCUMENT_ERR, |
| null, newChild, 0); |
| } |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Replaces the specified node in this node's list of children. |
| * Document subclasses must override this to test the restrictions |
| * that there be only one element and document type child. |
| * |
| * <p> Causes DOMNodeRemoved and DOMNodeInserted mutation event to be |
| * reported. Will cause another DOMNodeRemoved event to be reported if |
| * the newChild parameter already has a parent. These events may be |
| * delivered in any order, except that the event reporting removal |
| * from such an existing parent will always be delivered before the |
| * event reporting its re-insertion as a child of some other node. |
| * The order in which children are removed and inserted is implementation |
| * specific. |
| * |
| * <p> If your application needs to depend on the in which those removal |
| * and insertion events are delivered, don't use this API. Instead, |
| * invoke the removeChild and insertBefore methods directly, to guarantee |
| * a specific delivery order. Similarly, don't use document fragments, |
| * Otherwise your application code may not work on a DOM which implements |
| * this method differently. |
| * |
| * <p> If this DOM has been compiled without mutation event support, |
| * these events will not be reported. |
| */ |
| public Node replaceChild(Node newChild, Node refChild) |
| { |
| try |
| { |
| DomNode child = (DomNode) newChild; |
| DomNode ref = (DomNode) refChild; |
| |
| DomEvent.DomMutationEvent event = getMutationEvent(); |
| boolean doFree = (event != null); |
| |
| if (child.nodeType == DOCUMENT_FRAGMENT_NODE) |
| { |
| // Append all nodes in the fragment to this node |
| for (DomNode ctx = child.first; ctx != null; ctx = ctx.next) |
| { |
| checkMisc(ctx); |
| } |
| if (ref == null || ref.parent != this) |
| { |
| throw new DomDOMException(DOMException.NOT_FOUND_ERR, |
| null, ref, 0); |
| } |
| |
| if (reportMutations) |
| { |
| removalEvent(event, ref); |
| } |
| length--; |
| length += child.length; |
| |
| if (child.length == 0) |
| { |
| // Removal |
| if (ref.previous != null) |
| { |
| ref.previous.next = ref.next; |
| } |
| if (ref.next != null) |
| { |
| ref.next.previous = ref.previous; |
| } |
| if (first == ref) |
| { |
| first = ref.next; |
| } |
| if (last == ref) |
| { |
| last = ref.previous; |
| } |
| } |
| else |
| { |
| int i = ref.index; |
| for (DomNode ctx = child.first; ctx != null; ctx = ctx.next) |
| { |
| // Insertion |
| ctx.parent = this; |
| ctx.index = i++; |
| ctx.setDepth(ref.depth); |
| if (ctx == child.first) |
| { |
| ctx.previous = ref.previous; |
| } |
| if (ctx == child.last) |
| { |
| ctx.next = ref.next; |
| } |
| } |
| if (first == ref) |
| { |
| first = child.first; |
| } |
| if (last == ref) |
| { |
| last = child.last; |
| } |
| } |
| } |
| else |
| { |
| checkMisc(child); |
| if (ref == null || ref.parent != this) |
| { |
| throw new DomDOMException(DOMException.NOT_FOUND_ERR, |
| null, ref, 0); |
| } |
| |
| if (reportMutations) |
| { |
| removalEvent(event, ref); |
| } |
| |
| if (child.parent != null) |
| { |
| child.parent.removeChild(child); |
| } |
| child.parent = this; |
| child.index = ref.index; |
| child.setDepth(ref.depth); |
| if (ref.previous != null) |
| { |
| ref.previous.next = child; |
| } |
| child.previous = ref.previous; |
| if (ref.next != null) |
| { |
| ref.next.previous = child; |
| } |
| child.next = ref.next; |
| if (first == ref) |
| { |
| first = child; |
| } |
| if (last == ref) |
| { |
| last = child; |
| } |
| |
| if (reportMutations) |
| { |
| insertionEvent(event, child); |
| } |
| if (doFree) |
| { |
| freeMutationEvent(); |
| } |
| } |
| ref.parent = null; |
| ref.index = 0; |
| ref.setDepth(0); |
| ref.previous = null; |
| ref.next = null; |
| |
| return ref; |
| } |
| catch (ClassCastException e) |
| { |
| throw new DomDOMException(DOMException.WRONG_DOCUMENT_ERR, |
| null, newChild, 0); |
| } |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Removes the specified child from this node's list of children, |
| * or else reports an exception. |
| * |
| * <p> Causes a DOMNodeRemoved mutation event to be reported. |
| * |
| * <p> If this DOM has been compiled without mutation event support, |
| * these events will not be reported. |
| */ |
| public Node removeChild(Node refChild) |
| { |
| try |
| { |
| DomNode ref = (DomNode) refChild; |
| |
| if (ref == null || ref.parent != this) |
| { |
| throw new DomDOMException(DOMException.NOT_FOUND_ERR, |
| null, ref, 0); |
| } |
| if (readonly && !owner.building) |
| { |
| throw new DomDOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, |
| null, this, 0); |
| } |
| |
| for (DomNode child = first; child != null; child = child.next) |
| { |
| if (child == ref) |
| { |
| if (reportMutations) |
| { |
| removalEvent(null, child); |
| } |
| |
| length--; |
| if (ref.previous != null) |
| { |
| ref.previous.next = ref.next; |
| } |
| if (ref.next != null) |
| { |
| ref.next.previous = ref.previous; |
| } |
| if (first == ref) |
| { |
| first = ref.next; |
| } |
| if (last == ref) |
| { |
| last = ref.previous; |
| } |
| // renumber indices |
| int i = 0; |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) |
| { |
| ctx.index = i++; |
| } |
| ref.parent = null; |
| ref.setDepth(0); |
| ref.index = 0; |
| ref.previous = null; |
| ref.next = null; |
| |
| return ref; |
| } |
| } |
| throw new DomDOMException(DOMException.NOT_FOUND_ERR, |
| "that's no child of mine", refChild, 0); |
| } |
| catch (ClassCastException e) |
| { |
| throw new DomDOMException(DOMException.WRONG_DOCUMENT_ERR, |
| null, refChild, 0); |
| } |
| } |
| |
| /** |
| * <b>DOM L1 (NodeList)</b> |
| * Returns the item with the specified index in this NodeList, |
| * else null. |
| */ |
| public Node item(int index) |
| { |
| DomNode child = first; |
| int count = 0; |
| while (child != null && count < index) |
| { |
| child = child.next; |
| count++; |
| } |
| return child; |
| } |
| |
| /** |
| * <b>DOM L1 (NodeList)</b> |
| * Returns the number of elements in this NodeList. |
| * (Note that many interfaces have a "Length" property, not just |
| * NodeList, and if a node subtype must implement one of those, |
| * it will also need to override getChildNodes.) |
| */ |
| public int getLength() |
| { |
| return length; |
| } |
| |
| /** |
| * Minimize extra space consumed by this node to hold children and event |
| * listeners. |
| */ |
| public void trimToSize() |
| { |
| if (listeners != null && listeners.length != nListeners) |
| { |
| ListenerRecord[] newKids = new ListenerRecord[length]; |
| System.arraycopy(listeners, 0, newKids, 0, nListeners); |
| listeners = newKids; |
| } |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns the previous sibling, if one is known. |
| */ |
| public Node getNextSibling() |
| { |
| return next; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns the previous sibling, if one is known. |
| */ |
| public Node getPreviousSibling() |
| { |
| return previous; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns the parent node, if one is known. |
| */ |
| public Node getParentNode() |
| { |
| return parent; |
| } |
| |
| /** |
| * <b>DOM L2</b> |
| * Consults the DOM implementation to determine if the requested |
| * feature is supported. DocumentType subclasses must override |
| * this method, and associate themselves directly with the |
| * DOMImplementation node used. (This method relies on being able |
| * to access the DOMImplementation from the owner document, but |
| * DocumentType nodes can be created without an owner.) |
| */ |
| public boolean isSupported(String feature, String version) |
| { |
| Document doc = owner; |
| DOMImplementation impl = null; |
| |
| if (doc == null && nodeType == DOCUMENT_NODE) |
| { |
| doc = (Document) this; |
| } |
| |
| if (doc == null) |
| { |
| // possible for DocumentType |
| throw new IllegalStateException ("unbound ownerDocument"); |
| } |
| |
| impl = doc.getImplementation(); |
| return impl.hasFeature(feature, version); |
| } |
| |
| /** |
| * <b>DOM L1 (modified in L2)</b> |
| * Returns the owner document. This is only null for Document nodes, |
| * and (new in L2) for DocumentType nodes which have not yet been |
| * associated with the rest of their document. |
| */ |
| final public Document getOwnerDocument() |
| { |
| return owner; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Does nothing; this must be overridden (along with the |
| * getNodeValue method) for nodes with a non-null defined value. |
| */ |
| public void setNodeValue(String value) |
| { |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns null; this must be overridden for nodes types with |
| * a defined value, along with the setNodeValue method. |
| */ |
| public String getNodeValue() |
| { |
| return null; |
| } |
| |
| /** This forces GCJ compatibility. |
| * Without this method GCJ is unable to compile to byte code. |
| */ |
| public final short getNodeType() |
| { |
| return nodeType; |
| } |
| |
| /** This forces GCJ compatibility. |
| * Without this method GCJ seems unable to natively compile GNUJAXP. |
| */ |
| public abstract String getNodeName(); |
| |
| /** |
| * <b>DOM L2</b> |
| * Does nothing; this must be overridden (along with the |
| * getPrefix method) for element and attribute nodes. |
| */ |
| public void setPrefix(String prefix) |
| { |
| } |
| |
| /** |
| * <b>DOM L2</b> |
| * Returns null; this must be overridden for element and |
| * attribute nodes. |
| */ |
| public String getPrefix() |
| { |
| return null; |
| } |
| |
| /** |
| * <b>DOM L2</b> |
| * Returns null; this must be overridden for element and |
| * attribute nodes. |
| */ |
| public String getNamespaceURI() |
| { |
| return null; |
| } |
| |
| /** |
| * <b>DOM L2</b> |
| * Returns the node name; this must be overridden for element and |
| * attribute nodes. |
| */ |
| public String getLocalName() |
| { |
| return null; |
| } |
| |
| /** |
| * <b>DOM L1</b> |
| * Returns a clone of this node which optionally includes cloned |
| * versions of child nodes. Clones are always mutable, except for |
| * entity reference nodes. |
| */ |
| public Node cloneNode(boolean deep) |
| { |
| DomNode node = (DomNode) clone(); |
| |
| if (deep) |
| { |
| DomDocument doc = (nodeType == DOCUMENT_NODE) ? |
| (DomDocument) node : node.owner; |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) |
| { |
| DomNode newChild = (DomNode) ctx.cloneNode(deep); |
| newChild.setOwner(doc); |
| node.appendChild(newChild); |
| } |
| } |
| |
| if (nodeType == ENTITY_REFERENCE_NODE) |
| { |
| node.makeReadonly(); |
| } |
| notifyUserDataHandlers(UserDataHandler.NODE_CLONED, this, node); |
| return node; |
| } |
| |
| void notifyUserDataHandlers(short op, Node src, Node dst) |
| { |
| if (userDataHandlers != null) |
| { |
| for (Iterator i = userDataHandlers.entrySet().iterator(); i.hasNext(); ) |
| { |
| Map.Entry entry = (Map.Entry) i.next(); |
| String key = (String) entry.getKey(); |
| UserDataHandler handler = (UserDataHandler) entry.getValue(); |
| Object data = userData.get(key); |
| handler.handle(op, key, data, src, dst); |
| } |
| } |
| } |
| |
| /** |
| * Clones this node; roughly equivalent to cloneNode(false). |
| * Element subclasses must provide a new implementation which |
| * invokes this method to handle the basics, and then arranges |
| * to clone any element attributes directly. Attribute subclasses |
| * must make similar arrangements, ensuring that existing ties to |
| * elements are broken by cloning. |
| */ |
| public Object clone() |
| { |
| try |
| { |
| DomNode node = (DomNode) super.clone(); |
| |
| node.parent = null; |
| node.depth = 0; |
| node.index = 0; |
| node.length = 0; |
| node.first = null; |
| node.last = null; |
| node.previous = null; |
| node.next = null; |
| |
| node.readonly = false; |
| node.listeners = null; |
| node.nListeners = 0; |
| return node; |
| |
| } |
| catch (CloneNotSupportedException x) |
| { |
| throw new Error("clone didn't work"); |
| } |
| } |
| |
| // the elements-by-tagname stuff is needed for both |
| // elements and documents ... this is in lieu of a |
| // common base class between Node and NodeNS. |
| |
| /** |
| * <b>DOM L1</b> |
| * Creates a NodeList giving array-style access to elements with |
| * the specified name. Access is fastest if indices change by |
| * small values, and the DOM is not modified. |
| */ |
| public NodeList getElementsByTagName(String tag) |
| { |
| return new ShadowList(null, tag); |
| } |
| |
| /** |
| * <b>DOM L2</b> |
| * Creates a NodeList giving array-style access to elements with |
| * the specified namespace and local name. Access is fastest if |
| * indices change by small values, and the DOM is not modified. |
| */ |
| public NodeList getElementsByTagNameNS(String namespace, String local) |
| { |
| return new ShadowList(namespace, local); |
| } |
| |
| |
| // |
| // This shadow class is GC-able even when the live list it shadows |
| // can't be, because of event registration hookups. Its finalizer |
| // makes that live list become GC-able. |
| // |
| final class ShadowList |
| implements NodeList |
| { |
| |
| private LiveNodeList liveList; |
| |
| ShadowList(String ns, String local) |
| { |
| liveList = new LiveNodeList(ns, local); |
| } |
| |
| public void finalize() |
| { |
| liveList.detach(); |
| liveList = null; |
| } |
| |
| public Node item(int index) |
| { |
| return liveList.item(index); |
| } |
| |
| public int getLength() |
| { |
| return liveList.getLength(); |
| } |
| } |
| |
| final class LiveNodeList |
| implements NodeList, EventListener, NodeFilter |
| { |
| |
| private final boolean matchAnyURI; |
| private final boolean matchAnyName; |
| private final String elementURI; |
| private final String elementName; |
| |
| private DomIterator current; |
| private int lastIndex; |
| |
| LiveNodeList(String uri, String name) |
| { |
| elementURI = uri; |
| elementName = name; |
| matchAnyURI = "*".equals(uri); |
| matchAnyName = "*".equals(name); |
| |
| DomNode.this.addEventListener("DOMNodeInserted", this, true); |
| DomNode.this.addEventListener("DOMNodeRemoved", this, true); |
| } |
| |
| void detach() |
| { |
| current.detach(); |
| current = null; |
| |
| DomNode.this.removeEventListener("DOMNodeInserted", this, true); |
| DomNode.this.removeEventListener("DOMNodeRemoved", this, true); |
| } |
| |
| public short acceptNode(Node element) |
| { |
| if (element == DomNode.this) |
| { |
| return FILTER_SKIP; |
| } |
| |
| // use namespace-aware matching ... |
| if (elementURI != null) |
| { |
| if (!(matchAnyURI |
| || elementURI.equals(element.getNamespaceURI()))) |
| { |
| return FILTER_SKIP; |
| } |
| if (!(matchAnyName |
| || elementName.equals(element.getLocalName()))) |
| { |
| return FILTER_SKIP; |
| } |
| |
| // ... or qName-based kind. |
| } |
| else |
| { |
| if (!(matchAnyName |
| || elementName.equals(element.getNodeName()))) |
| { |
| return FILTER_SKIP; |
| } |
| } |
| return FILTER_ACCEPT; |
| } |
| |
| private DomIterator createIterator() |
| { |
| return new DomIterator(DomNode.this, |
| NodeFilter.SHOW_ELEMENT, |
| this, /* filter */ |
| true /* expand entity refs */ |
| ); |
| } |
| |
| public void handleEvent(Event e) |
| { |
| MutationEvent mutation = (MutationEvent) e; |
| Node related = mutation.getRelatedNode(); |
| |
| // XXX if it's got children ... check all kids too, they |
| // will invalidate our saved index |
| |
| if (related.getNodeType() != Node.ELEMENT_NODE || |
| related.getNodeName() != elementName || |
| related.getNamespaceURI() != elementURI) |
| { |
| return; |
| } |
| |
| current = null; |
| } |
| |
| public Node item(int index) |
| { |
| if (current == null) |
| { |
| current = createIterator(); |
| lastIndex = -1; |
| } |
| |
| // last node or before? go backwards |
| if (index <= lastIndex) { |
| while (index != lastIndex) { |
| current.previousNode (); |
| lastIndex--; |
| } |
| Node ret = current.previousNode (); |
| current = null; |
| return ret; |
| } |
| |
| // somewhere after last node |
| while (++lastIndex != index) |
| current.nextNode (); |
| Node ret = current.nextNode (); |
| current = null; |
| return ret; |
| } |
| |
| public int getLength() |
| { |
| int retval = 0; |
| NodeIterator iter = createIterator(); |
| |
| while (iter.nextNode() != null) |
| { |
| retval++; |
| } |
| current = null; |
| return retval; |
| } |
| |
| } |
| |
| // |
| // EventTarget support |
| // |
| static final class ListenerRecord |
| { |
| |
| String type; |
| EventListener listener; |
| boolean useCapture; |
| |
| // XXX use JDK 1.2 java.lang.ref.WeakReference to listener, |
| // and we can both get rid of "shadow" classes and remove |
| // the need for applications to apply similar trix ... but |
| // JDK 1.2 support isn't generally available yet |
| |
| ListenerRecord(String type, EventListener listener, boolean useCapture) |
| { |
| this.type = type.intern(); |
| this.listener = listener; |
| this.useCapture = useCapture; |
| } |
| |
| boolean equals(ListenerRecord rec) |
| { |
| return listener == rec.listener |
| && useCapture == rec.useCapture |
| && type == rec.type; |
| } |
| |
| } |
| |
| /** |
| * <b>DOM L2 (Events)</b> |
| * Returns an instance of the specified type of event object. |
| * Understands about DOM Mutation, HTML, and UI events. |
| * |
| * <p>If the name of the event type begins with "USER-", then an object |
| * implementing the "Event" class will be returned; this provides a |
| * limited facility for application-defined events to use the DOM event |
| * infrastructure. Alternatively, use one of the standard DOM event |
| * classes and initialize it using use such a "USER-" event type name; |
| * or defin, instantiate, and initialize an application-specific subclass |
| * of DomEvent and pass that to dispatchEvent(). |
| * |
| * @param eventType Identifies the particular DOM feature module |
| * defining the type of event, such as "MutationEvents". |
| * <em>The event "name" is a different kind of "type".</em> |
| */ |
| public Event createEvent(String eventType) |
| { |
| eventType = eventType.toLowerCase(); |
| |
| if ("mutationevents".equals(eventType)) |
| { |
| return new DomEvent.DomMutationEvent(null); |
| } |
| |
| if ("htmlevents".equals(eventType) |
| || "events".equals(eventType) |
| || "user-events".equals(eventType)) |
| { |
| return new DomEvent(null); |
| } |
| |
| if ("uievents".equals(eventType)) |
| { |
| return new DomEvent.DomUIEvent(null); |
| } |
| |
| // mouse events |
| |
| throw new DomDOMException(DOMException.NOT_SUPPORTED_ERR, |
| eventType, null, 0); |
| } |
| |
| /** |
| * <b>DOM L2 (Events)</b> |
| * Registers an event listener's interest in a class of events. |
| */ |
| public final void addEventListener(String type, |
| EventListener listener, |
| boolean useCapture) |
| { |
| if (listeners == null) |
| { |
| listeners = new ListenerRecord[1]; |
| } |
| else if (nListeners == listeners.length) |
| { |
| ListenerRecord[] newListeners = |
| new ListenerRecord[listeners.length + NKIDS_DELTA]; |
| System.arraycopy(listeners, 0, newListeners, 0, nListeners); |
| listeners = newListeners; |
| } |
| |
| // prune duplicates |
| ListenerRecord record; |
| |
| record = new ListenerRecord(type, listener, useCapture); |
| for (int i = 0; i < nListeners; i++) |
| { |
| if (record.equals(listeners[i])) |
| { |
| return; |
| } |
| } |
| listeners [nListeners++] = record; |
| } |
| |
| // XXX this exception should be discarded from DOM |
| |
| // this class can be instantiated, unlike the one in the spec |
| static final class DomEventException |
| extends EventException |
| { |
| |
| DomEventException() |
| { |
| super(UNSPECIFIED_EVENT_TYPE_ERR, "unspecified event type"); |
| } |
| |
| } |
| |
| /** |
| * <b>DOM L2 (Events)</b> |
| * Delivers an event to all relevant listeners, returning true if the |
| * caller should perform their default action. Note that the event |
| * must have been provided by the createEvent() method on this |
| * class, else it can't be dispatched. |
| * |
| * @see #createEvent |
| * |
| * @exception NullPointerException When a null event is passed. |
| * @exception ClassCastException When the event wasn't provided by |
| * the createEvent method, or otherwise isn't a DomEvent. |
| * @exception EventException If the event type wasn't specified |
| */ |
| public final boolean dispatchEvent(Event event) |
| throws EventException |
| { |
| DomEvent e = (DomEvent) event; |
| DomNode[] ancestors = null; |
| int ancestorMax = 0; |
| boolean haveDispatchDataLock = false; |
| |
| if (e.type == null) |
| { |
| throw new DomEventException(); |
| } |
| |
| e.doDefault = true; |
| e.target = this; |
| |
| // |
| // Typical case: one nonrecursive dispatchEvent call at a time |
| // for this class. If that's our case, we can avoid allocating |
| // garbage, which is overall a big win. Even with advanced GCs |
| // that deal well with short-lived garbage, and wayfast allocators, |
| // it still helps. |
| // |
| // Remember -- EVERY mutation goes though here at least once. |
| // |
| // When populating a DOM tree, trying to send mutation events is |
| // the primary cost; this dominates the critical path. |
| // |
| try |
| { |
| DomNode current; |
| int index; |
| boolean haveAncestorRegistrations = false; |
| ListenerRecord[] notificationSet; |
| int ancestorLen; |
| |
| synchronized (lockNode) |
| { |
| if (!dispatchDataLock) |
| { |
| haveDispatchDataLock = dispatchDataLock = true; |
| notificationSet = DomNode.notificationSet; |
| ancestors = DomNode.ancestors; |
| } |
| else |
| { |
| notificationSet = new ListenerRecord[NOTIFICATIONS_INIT]; |
| ancestors = new DomNode[ANCESTORS_INIT]; |
| } |
| ancestorLen = ancestors.length; |
| } |
| |
| // XXX autogrow ancestors ... based on statistics |
| |
| // Climb to the top of this subtree and handle capture, letting |
| // each node (from the top down) capture until one stops it or |
| // until we get to this one. |
| |
| for (index = 0, current = parent; |
| current != null && index < ancestorLen; |
| index++, current = current.parent) |
| { |
| if (current.nListeners != 0) |
| { |
| haveAncestorRegistrations = true; |
| } |
| ancestors [index] = current; |
| } |
| if (current != null) |
| { |
| throw new RuntimeException("dispatchEvent capture stack size"); |
| } |
| |
| ancestorMax = index; |
| e.stop = false; |
| |
| if (haveAncestorRegistrations) |
| { |
| e.eventPhase = Event.CAPTURING_PHASE; |
| while (!e.stop && index-- > 0) |
| { |
| current = ancestors [index]; |
| if (current.nListeners != 0) |
| { |
| notifyNode(e, current, true, notificationSet); |
| } |
| } |
| } |
| |
| // Always deliver events to the target node (this) |
| // unless stopPropagation was called. If we saw |
| // no registrations yet (typical!), we never will. |
| if (!e.stop && nListeners != 0) |
| { |
| e.eventPhase = Event.AT_TARGET; |
| notifyNode (e, this, false, notificationSet); |
| } |
| else if (!haveAncestorRegistrations) |
| { |
| e.stop = true; |
| } |
| |
| // If the event bubbles and propagation wasn't halted, |
| // walk back up the ancestor list. Stop bubbling when |
| // any bubbled event handler stops it. |
| |
| if (!e.stop && e.bubbles) |
| { |
| e.eventPhase = Event.BUBBLING_PHASE; |
| for (index = 0; |
| !e.stop |
| && index < ancestorMax |
| && (current = ancestors[index]) != null; |
| index++) |
| { |
| if (current.nListeners != 0) |
| { |
| notifyNode(e, current, false, notificationSet); |
| } |
| } |
| } |
| e.eventPhase = 0; |
| |
| // Caller chooses whether to perform the default |
| // action based on return from this method. |
| return e.doDefault; |
| |
| } |
| finally |
| { |
| if (haveDispatchDataLock) |
| { |
| // synchronize to force write ordering |
| synchronized (lockNode) |
| { |
| // null out refs to ensure they'll be GC'd |
| for (int i = 0; i < ancestorMax; i++) |
| { |
| ancestors [i] = null; |
| } |
| // notificationSet handled by notifyNode |
| |
| dispatchDataLock = false; |
| } |
| } |
| } |
| } |
| |
| private void notifyNode(DomEvent e, |
| DomNode current, |
| boolean capture, |
| ListenerRecord[] notificationSet) |
| { |
| int count = 0; |
| |
| // do any of this set of listeners get notified? |
| for (int i = 0; i < current.nListeners; i++) |
| { |
| ListenerRecord rec = current.listeners[i]; |
| |
| if (rec.useCapture != capture) |
| { |
| continue; |
| } |
| if (!e.type.equals (rec.type)) |
| { |
| continue; |
| } |
| if (count >= notificationSet.length) |
| { |
| // very simple growth algorithm |
| int len = Math.max(notificationSet.length, 1); |
| ListenerRecord[] tmp = new ListenerRecord[len * 2]; |
| System.arraycopy(notificationSet, 0, tmp, 0, |
| notificationSet.length); |
| notificationSet = tmp; |
| } |
| notificationSet[count++] = rec; |
| } |
| |
| // Notify just those listeners |
| e.currentNode = current; |
| for (int i = 0; i < count; i++) |
| { |
| try |
| { |
| // Late in the DOM CR process (3rd or 4th CR?) the |
| // removeEventListener spec became asymmetric with respect |
| // to addEventListener ... effect is now immediate. |
| for (int j = 0; j < current.nListeners; j++) |
| { |
| if (current.listeners[j].equals(notificationSet[i])) |
| { |
| notificationSet[i].listener.handleEvent(e); |
| break; |
| } |
| } |
| |
| } |
| catch (Exception x) |
| { |
| // ignore all exceptions |
| } |
| notificationSet[i] = null; // free for GC |
| } |
| } |
| |
| /** |
| * <b>DOM L2 (Events)</b> |
| * Unregisters an event listener. |
| */ |
| public final void removeEventListener(String type, |
| EventListener listener, |
| boolean useCapture) |
| { |
| for (int i = 0; i < nListeners; i++) |
| { |
| if (listeners[i].listener != listener) |
| { |
| continue; |
| } |
| if (listeners[i].useCapture != useCapture) |
| { |
| continue; |
| } |
| if (!listeners[i].type.equals(type)) |
| { |
| continue; |
| } |
| |
| if (nListeners == 1) |
| { |
| listeners = null; |
| nListeners = 0; |
| } |
| else |
| { |
| for (int j = i + 1; j < nListeners; j++) |
| { |
| listeners[i++] = listeners[j++]; |
| } |
| listeners[--nListeners] = null; |
| } |
| break; |
| } |
| // no exceptions reported |
| } |
| |
| /** |
| * <b>DOM L1 (relocated in DOM L2)</b> |
| * In this node and all contained nodes (including attributes if |
| * relevant) merge adjacent text nodes. This is done while ignoring |
| * text which happens to use CDATA delimiters). |
| */ |
| public final void normalize() |
| { |
| // Suspend readonly status |
| boolean saved = readonly; |
| readonly = false; |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) |
| { |
| switch (ctx.nodeType) |
| { |
| case TEXT_NODE: |
| while (ctx.next != null && ctx.next.nodeType == TEXT_NODE) |
| { |
| Text text = (Text) ctx; |
| text.appendData(ctx.next.getNodeValue()); |
| removeChild(ctx.next); |
| } |
| break; |
| case ELEMENT_NODE: |
| NamedNodeMap attrs = ctx.getAttributes(); |
| int len = attrs.getLength(); |
| for (int i = 0; i < len; i++) |
| { |
| attrs.item(i).normalize(); |
| } |
| // Fall through |
| case DOCUMENT_NODE: |
| case DOCUMENT_FRAGMENT_NODE: |
| case ATTRIBUTE_NODE: |
| case ENTITY_REFERENCE_NODE: |
| ctx.normalize(); |
| break; |
| } |
| } |
| readonly = saved; |
| } |
| |
| /** |
| * Returns true iff node types match, and either (a) both nodes have no |
| * namespace and their getNodeName() values are the same, or (b) both |
| * nodes have the same getNamespaceURI() and same getLocalName() values. |
| * |
| * <p>Note that notion of a "Per-Element-Type" attribute name scope, as |
| * found in a non-normative appendix of the XML Namespaces specification, |
| * is not supported here. Your application must implement that notion, |
| * typically by not bothering to check nameAndTypeEquals for attributes |
| * without namespace URIs unless you already know their elements are |
| * nameAndTypeEquals. |
| */ |
| public boolean nameAndTypeEquals(Node other) |
| { |
| if (other == this) |
| { |
| return true; |
| } |
| // node types must match |
| if (nodeType != other.getNodeType()) |
| { |
| return false; |
| } |
| |
| // if both have namespaces, do a "full" comparision |
| // this is a "global" partition |
| String ns1 = this.getNamespaceURI(); |
| String ns2 = other.getNamespaceURI(); |
| |
| if (ns1 != null && ns2 != null) |
| { |
| return ns1.equals(ns2) && |
| getLocalName().equals(other.getLocalName()); |
| } |
| |
| // if neither has a namespace, this is a "no-namespace" name. |
| if (ns1 == null && ns2 == null) |
| { |
| if (!getNodeName().equals(other.getNodeName())) |
| { |
| return false; |
| } |
| // can test the non-normative "per-element-type" scope here. |
| // if this is an attribute node and both nodes have been bound |
| // to elements (!!), then return the nameAndTypeEquals() |
| // comparison of those elements. |
| return true; |
| } |
| |
| // otherwise they're unequal: one scoped, one not. |
| return false; |
| } |
| |
| // DOM Level 3 methods |
| |
| public String getBaseURI() |
| { |
| return (parent != null) ? parent.getBaseURI() : null; |
| } |
| |
| public short compareDocumentPosition(Node other) |
| throws DOMException |
| { |
| return (short) compareTo(other); |
| } |
| |
| /** |
| * DOM nodes have a natural ordering: document order. |
| */ |
| public final int compareTo(Object other) |
| { |
| if (other instanceof DomNode) |
| { |
| DomNode n1 = this; |
| DomNode n2 = (DomNode) other; |
| if (n1.owner != n2.owner) |
| { |
| return 0; |
| } |
| int d1 = n1.depth, d2 = n2.depth; |
| int delta = d1 - d2; |
| while (d1 > d2) |
| { |
| n1 = n1.parent; |
| d1--; |
| } |
| while (d2 > d1) |
| { |
| n2 = n2.parent; |
| d2--; |
| } |
| int c = compareTo2(n1, n2); |
| return (c != 0) ? c : delta; |
| } |
| return 0; |
| } |
| |
| /** |
| * Compare two nodes at the same depth. |
| */ |
| final int compareTo2(DomNode n1, DomNode n2) |
| { |
| if (n1 == n2 || n1.depth == 0 || n2.depth == 0) |
| { |
| return 0; |
| } |
| int c = compareTo2(n1.parent, n2.parent); |
| return (c != 0) ? c : n1.index - n2.index; |
| } |
| |
| public final String getTextContent() |
| throws DOMException |
| { |
| return getTextContent(true); |
| } |
| |
| final String getTextContent(boolean topLevel) |
| throws DOMException |
| { |
| switch (nodeType) |
| { |
| case ELEMENT_NODE: |
| case ENTITY_NODE: |
| case ENTITY_REFERENCE_NODE: |
| case DOCUMENT_FRAGMENT_NODE: |
| StringBuffer buffer = new StringBuffer(); |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) |
| { |
| String textContent = ctx.getTextContent(false); |
| if (textContent != null) |
| { |
| buffer.append(textContent); |
| } |
| } |
| return buffer.toString(); |
| case TEXT_NODE: |
| case CDATA_SECTION_NODE: |
| if (((Text) this).isElementContentWhitespace()) |
| { |
| return ""; |
| } |
| return getNodeValue(); |
| case ATTRIBUTE_NODE: |
| return getNodeValue(); |
| case COMMENT_NODE: |
| case PROCESSING_INSTRUCTION_NODE: |
| return topLevel ? getNodeValue() : ""; |
| default: |
| return null; |
| } |
| } |
| |
| public void setTextContent(String textContent) |
| throws DOMException |
| { |
| switch (nodeType) |
| { |
| case ELEMENT_NODE: |
| case ATTRIBUTE_NODE: |
| case ENTITY_NODE: |
| case ENTITY_REFERENCE_NODE: |
| case DOCUMENT_FRAGMENT_NODE: |
| for (DomNode ctx = first; ctx != null; ) |
| { |
| DomNode n = ctx.next; |
| removeChild(ctx); |
| ctx = n; |
| } |
| if (textContent != null) |
| { |
| Text text = owner.createTextNode(textContent); |
| appendChild(text); |
| } |
| break; |
| case TEXT_NODE: |
| case CDATA_SECTION_NODE: |
| case COMMENT_NODE: |
| case PROCESSING_INSTRUCTION_NODE: |
| setNodeValue(textContent); |
| break; |
| } |
| } |
| |
| public boolean isSameNode(Node other) |
| { |
| return this == other; |
| } |
| |
| public String lookupPrefix(String namespaceURI) |
| { |
| return (parent == null || parent == owner) ? null : |
| parent.lookupPrefix(namespaceURI); |
| } |
| |
| public boolean isDefaultNamespace(String namespaceURI) |
| { |
| return (parent == null || parent == owner) ? false : |
| parent.isDefaultNamespace(namespaceURI); |
| } |
| |
| public String lookupNamespaceURI(String prefix) |
| { |
| return (parent == null || parent == owner) ? null : |
| parent.lookupNamespaceURI(prefix); |
| } |
| |
| public boolean isEqualNode(Node arg) |
| { |
| if (this == arg) |
| { |
| return true; |
| } |
| if (arg == null) |
| { |
| return false; |
| } |
| if (nodeType != arg.getNodeType() || |
| !equal(getNodeName(), arg.getNodeName()) || |
| !equal(getLocalName(), arg.getLocalName()) || |
| !equal(getNamespaceURI(), arg.getNamespaceURI()) || |
| !equal(getPrefix(), arg.getPrefix()) || |
| !equal(getNodeValue(), arg.getNodeValue())) |
| { |
| return false; |
| } |
| // Children |
| Node argCtx = arg.getFirstChild(); |
| getFirstChild(); // because of DomAttr lazy children |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) |
| { |
| if (!ctx.isEqualNode(argCtx)) |
| { |
| return false; |
| } |
| argCtx = argCtx.getNextSibling(); |
| } |
| if (argCtx != null) |
| { |
| return false; |
| } |
| |
| // TODO Attr NamedNodeMap |
| // TODO DocumentType |
| return true; |
| } |
| |
| boolean equal(String arg1, String arg2) |
| { |
| return ((arg1 == null && arg2 == null) || |
| (arg1 != null && arg1.equals(arg2))); |
| } |
| |
| public Object getFeature(String feature, String version) |
| { |
| DOMImplementation impl = (nodeType == DOCUMENT_NODE) ? |
| ((Document) this).getImplementation() : owner.getImplementation(); |
| if (impl.hasFeature(feature, version)) |
| { |
| return this; |
| } |
| return null; |
| } |
| |
| public Object setUserData(String key, Object data, UserDataHandler handler) |
| { |
| if (userData == null) |
| { |
| userData = new HashMap(); |
| } |
| if (handler != null) |
| { |
| if (userDataHandlers == null) |
| { |
| userDataHandlers = new HashMap(); |
| } |
| userDataHandlers.put(key, handler); |
| } |
| return userData.put(key, data); |
| } |
| |
| public Object getUserData(String key) |
| { |
| if (userData == null) |
| { |
| return null; |
| } |
| return userData.get(key); |
| } |
| |
| public String toString() |
| { |
| String nodeName = getNodeName(); |
| String nodeValue = getNodeValue(); |
| StringBuffer buf = new StringBuffer(getClass().getName()); |
| buf.append('['); |
| if (nodeName != null) |
| { |
| buf.append(nodeName); |
| } |
| if (nodeValue != null) |
| { |
| if (nodeName != null) |
| { |
| buf.append('='); |
| } |
| buf.append('\''); |
| buf.append(encode(nodeValue)); |
| buf.append('\''); |
| } |
| buf.append(']'); |
| return buf.toString(); |
| } |
| |
| String encode(String value) |
| { |
| StringBuffer buf = null; |
| int len = value.length(); |
| for (int i = 0; i < len; i++) |
| { |
| char c = value.charAt(i); |
| if (c == '\n') |
| { |
| if (buf == null) |
| { |
| buf = new StringBuffer(value.substring(0, i)); |
| } |
| buf.append("\\n"); |
| } |
| else if (c == '\r') |
| { |
| if (buf == null) |
| { |
| buf = new StringBuffer(value.substring(0, i)); |
| } |
| buf.append("\\r"); |
| } |
| else if (buf != null) |
| { |
| buf.append(c); |
| } |
| } |
| return (buf != null) ? buf.toString() : value; |
| } |
| |
| String nodeTypeToString(short nodeType) |
| { |
| switch (nodeType) |
| { |
| case ELEMENT_NODE: |
| return "ELEMENT_NODE"; |
| case ATTRIBUTE_NODE: |
| return "ATTRIBUTE_NODE"; |
| case TEXT_NODE: |
| return "TEXT_NODE"; |
| case CDATA_SECTION_NODE: |
| return "CDATA_SECTION_NODE"; |
| case DOCUMENT_NODE: |
| return "DOCUMENT_NODE"; |
| case DOCUMENT_TYPE_NODE: |
| return "DOCUMENT_TYPE_NODE"; |
| case COMMENT_NODE: |
| return "COMMENT_NODE"; |
| case PROCESSING_INSTRUCTION_NODE: |
| return "PROCESSING_INSTRUCTION_NODE"; |
| case DOCUMENT_FRAGMENT_NODE: |
| return "DOCUMENT_FRAGMENT_NODE"; |
| case ENTITY_NODE: |
| return "ENTITY_NODE"; |
| case ENTITY_REFERENCE_NODE: |
| return "ENTITY_REFERENCE_NODE"; |
| case NOTATION_NODE: |
| return "NOTATION_NODE"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| |
| } |
| |