| /* 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., 51 Franklin Street, Fifth Floor, Boston, MA | |
| 02110-1301 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.HashSet; | |
| 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 HashSet 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() | |
| { | |
| } | |
| /** | |
| * 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; | |
| this.listeners = new HashSet(); | |
| } | |
| /** | |
| * <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; | |
| case DOCUMENT_TYPE_NODE: | |
| if (!owner.building) | |
| break; | |
| switch (childNodeType) | |
| { | |
| case COMMENT_NODE: | |
| case PROCESSING_INSTRUCTION_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() | |
| { | |
| } | |
| /** | |
| * <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; | |
| boolean building = doc.building; | |
| doc.building = true; // Permit certain structural rules | |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) | |
| { | |
| DomNode newChild = (DomNode) ctx.cloneNode(deep); | |
| newChild.setOwner(doc); | |
| node.appendChild(newChild); | |
| } | |
| doc.building = building; | |
| } | |
| 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 = new HashSet(); | |
| 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() | |
| { | |
| if (current != null) | |
| 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; | |
| } | |
| if (current != null) | |
| current.detach(); | |
| 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.detach(); | |
| current = null; | |
| return ret; | |
| } | |
| // somewhere after last node | |
| while (++lastIndex != index) | |
| current.nextNode (); | |
| Node ret = current.nextNode (); | |
| current.detach(); | |
| current = null; | |
| return ret; | |
| } | |
| public int getLength() | |
| { | |
| int retval = 0; | |
| NodeIterator iter = createIterator(); | |
| while (iter.nextNode() != null) | |
| { | |
| retval++; | |
| } | |
| iter.detach(); | |
| 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; | |
| } | |
| public boolean equals(Object o) | |
| { | |
| ListenerRecord rec = (ListenerRecord)o; | |
| return listener == rec.listener | |
| && useCapture == rec.useCapture | |
| && type == rec.type; | |
| } | |
| public int hashCode() | |
| { | |
| return listener.hashCode() ^ type.hashCode(); | |
| } | |
| } | |
| /** | |
| * <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) | |
| { | |
| // prune duplicates | |
| ListenerRecord record; | |
| record = new ListenerRecord(type, listener, useCapture); | |
| listeners.add(record); | |
| nListeners = listeners.size(); | |
| } | |
| // 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; | |
| } | |
| // 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. | |
| current = parent; | |
| if (current.depth >= ANCESTORS_INIT) | |
| { | |
| DomNode[] newants = new DomNode[current.depth + 1]; | |
| System.arraycopy(ancestors, 0, newants, 0, ancestors.length); | |
| ancestors = newants; | |
| ancestorLen = ancestors.length; | |
| } | |
| for (index = 0; index < ancestorLen; index++) | |
| { | |
| if (current == null || current.depth == 0) | |
| break; | |
| if (current.nListeners != 0) | |
| { | |
| haveAncestorRegistrations = true; | |
| } | |
| ancestors [index] = current; | |
| current = current.parent; | |
| } | |
| if (current.depth > 0) | |
| { | |
| 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; | |
| Iterator iter; | |
| iter = current.listeners.iterator(); | |
| // do any of this set of listeners get notified? | |
| while (iter.hasNext()) | |
| { | |
| ListenerRecord rec = (ListenerRecord)iter.next(); | |
| 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; | |
| } | |
| iter = null; | |
| // Notify just those listeners | |
| e.currentNode = current; | |
| for (int i = 0; i < count; i++) | |
| { | |
| try | |
| { | |
| iter = current.listeners.iterator(); | |
| // Late in the DOM CR process (3rd or 4th CR?) the | |
| // removeEventListener spec became asymmetric with respect | |
| // to addEventListener ... effect is now immediate. | |
| while (iter.hasNext()) | |
| { | |
| ListenerRecord rec = (ListenerRecord)iter.next(); | |
| if (rec.equals(notificationSet[i])) | |
| { | |
| notificationSet[i].listener.handleEvent(e); | |
| break; | |
| } | |
| } | |
| iter = null; | |
| } | |
| 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) | |
| { | |
| listeners.remove(new ListenerRecord(type, listener, useCapture)); | |
| nListeners = listeners.size(); | |
| // 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) | |
| { | |
| boolean saved2 = ctx.readonly; | |
| ctx.readonly = false; | |
| switch (ctx.nodeType) | |
| { | |
| case TEXT_NODE: | |
| case CDATA_SECTION_NODE: | |
| while (ctx.next != null && | |
| (ctx.next.nodeType == TEXT_NODE || | |
| ctx.next.nodeType == CDATA_SECTION_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++) | |
| { | |
| DomNode attr = (DomNode) attrs.item(i); | |
| boolean saved3 = attr.readonly; | |
| attr.readonly = false; | |
| attr.normalize(); | |
| attr.readonly = saved3; | |
| } | |
| // Fall through | |
| case DOCUMENT_NODE: | |
| case DOCUMENT_FRAGMENT_NODE: | |
| case ATTRIBUTE_NODE: | |
| case ENTITY_REFERENCE_NODE: | |
| ctx.normalize(); | |
| break; | |
| } | |
| ctx.readonly = saved2; | |
| } | |
| 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) && | |
| equal(getLocalName(), 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()) | |
| return false; | |
| switch (nodeType) | |
| { | |
| case ELEMENT_NODE: | |
| case ATTRIBUTE_NODE: | |
| if (!equal(getLocalName(), arg.getLocalName()) || | |
| !equal(getNamespaceURI(), arg.getNamespaceURI())) | |
| return false; | |
| break; | |
| case PROCESSING_INSTRUCTION_NODE: | |
| if (!equal(getNodeName(), arg.getNodeName()) || | |
| !equal(getNodeValue(), arg.getNodeValue())) | |
| return false; | |
| break; | |
| case COMMENT_NODE: | |
| case TEXT_NODE: | |
| case CDATA_SECTION_NODE: | |
| if (!equal(getNodeValue(), arg.getNodeValue())) | |
| return false; | |
| break; | |
| } | |
| // Children | |
| Node argCtx = arg.getFirstChild(); | |
| getFirstChild(); // because of DomAttr lazy children | |
| DomNode ctx = first; | |
| for (; ctx != null && argCtx != null; ctx = ctx.next) | |
| { | |
| if (nodeType == DOCUMENT_NODE) | |
| { | |
| // Ignore whitespace outside document element | |
| while (ctx != null && ctx.nodeType == TEXT_NODE) | |
| ctx = ctx.next; | |
| while (argCtx != null && ctx.getNodeType() == TEXT_NODE) | |
| argCtx = argCtx.getNextSibling(); | |
| if (ctx == null && argCtx != null) | |
| return false; | |
| else if (argCtx == null && ctx != null) | |
| return false; | |
| } | |
| if (!ctx.isEqualNode(argCtx)) | |
| return false; | |
| argCtx = argCtx.getNextSibling(); | |
| } | |
| if (ctx != null || argCtx != null) | |
| return false; | |
| // 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"; | |
| } | |
| } | |
| public void list(java.io.PrintStream out, int indent) | |
| { | |
| for (int i = 0; i < indent; i++) | |
| out.print(" "); | |
| out.println(toString()); | |
| for (DomNode ctx = first; ctx != null; ctx = ctx.next) | |
| ctx.list(out, indent + 1); | |
| } | |
| } | |