0001: /*
0002: * Copyright (c) 2002-2008 Gargoyle Software Inc. All rights reserved.
0003: *
0004: * Redistribution and use in source and binary forms, with or without
0005: * modification, are permitted provided that the following conditions are met:
0006: *
0007: * 1. Redistributions of source code must retain the above copyright notice,
0008: * this list of conditions and the following disclaimer.
0009: * 2. Redistributions in binary form must reproduce the above copyright notice,
0010: * this list of conditions and the following disclaimer in the documentation
0011: * and/or other materials provided with the distribution.
0012: * 3. The end-user documentation included with the redistribution, if any, must
0013: * include the following acknowledgment:
0014: *
0015: * "This product includes software developed by Gargoyle Software Inc.
0016: * (http://www.GargoyleSoftware.com/)."
0017: *
0018: * Alternately, this acknowledgment may appear in the software itself, if
0019: * and wherever such third-party acknowledgments normally appear.
0020: * 4. The name "Gargoyle Software" must not be used to endorse or promote
0021: * products derived from this software without prior written permission.
0022: * For written permission, please contact info@GargoyleSoftware.com.
0023: * 5. Products derived from this software may not be called "HtmlUnit", nor may
0024: * "HtmlUnit" appear in their name, without prior written permission of
0025: * Gargoyle Software Inc.
0026: *
0027: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
0028: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
0029: * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
0030: * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
0031: * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
0032: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
0033: * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
0034: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
0035: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
0036: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0037: */
0038: package com.gargoylesoftware.htmlunit.html;
0039:
0040: import java.beans.PropertyChangeListener;
0041: import java.beans.PropertyChangeSupport;
0042: import java.io.PrintWriter;
0043: import java.io.Serializable;
0044: import java.io.StringWriter;
0045: import java.util.ArrayList;
0046: import java.util.Iterator;
0047: import java.util.List;
0048: import java.util.NoSuchElementException;
0049:
0050: import org.apache.commons.logging.Log;
0051: import org.apache.commons.logging.LogFactory;
0052: import org.jaxen.JaxenException;
0053: import org.jaxen.Navigator;
0054: import org.mozilla.javascript.Function;
0055: import org.mozilla.javascript.ScriptableObject;
0056:
0057: import com.gargoylesoftware.htmlunit.Assert;
0058: import com.gargoylesoftware.htmlunit.IncorrectnessListener;
0059: import com.gargoylesoftware.htmlunit.Page;
0060: import com.gargoylesoftware.htmlunit.html.xpath.HtmlUnitXPath;
0061: import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
0062:
0063: /**
0064: * Base class for nodes in the HTML DOM tree. This class is modeled after the
0065: * W3C DOM specification, but does not implement it.
0066: *
0067: * @version $Revision: 2132 $
0068: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
0069: * @author <a href="mailto:gudujarlson@sf.net">Mike J. Bresnahan</a>
0070: * @author David K. Taylor
0071: * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
0072: * @author Chris Erskine
0073: * @author Mike Williams
0074: * @author Marc Guillemot
0075: * @author Denis N. Antonioli
0076: * @author Daniel Gredler
0077: * @author Ahmed Ashour
0078: * @author Rodney Gitzel
0079: */
0080: public abstract class DomNode implements Cloneable, Serializable {
0081:
0082: /**
0083: * Node type constant for the <code>Document</code> node.
0084: * @deprecated use {@link org.w3c.dom.Node#DOCUMENT_NODE} instead.
0085: */
0086: public static final short DOCUMENT_NODE = 9;
0087:
0088: /**
0089: * Node type constant for <code>Element</code> nodes.
0090: * @deprecated use {@link org.w3c.dom.Node#ELEMENT_NODE} instead.
0091: */
0092: public static final short ELEMENT_NODE = 1;
0093:
0094: /**
0095: * Node type constant for <code>Text</code> nodes.
0096: * @deprecated use {@link org.w3c.dom.Node#TEXT_NODE} instead.
0097: */
0098: public static final short TEXT_NODE = 3;
0099:
0100: /**
0101: * Node type constant for <code>Attribute</code> nodes.
0102: * @deprecated use {@link org.w3c.dom.Node#ATTRIBUTE_NODE} instead.
0103: */
0104: public static final short ATTRIBUTE_NODE = 2;
0105:
0106: /**
0107: * Node type constant for <code>Comment</code> nodes.
0108: * @deprecated use {@link org.w3c.dom.Node#COMMENT_NODE} instead.
0109: */
0110: public static final short COMMENT_NODE = 8;
0111:
0112: /** A ready state constant for IE (state 1). */
0113: public static final String READY_STATE_UNINITIALIZED = "uninitialized";
0114:
0115: /** A ready state constant for IE (state 2). */
0116: public static final String READY_STATE_LOADING = "loading";
0117:
0118: /** A ready state constant for IE (state 3). */
0119: public static final String READY_STATE_LOADED = "loaded";
0120:
0121: /** A ready state constant for IE (state 4). */
0122: public static final String READY_STATE_INTERACTIVE = "interactive";
0123:
0124: /** A ready state constant for IE (state 5). */
0125: public static final String READY_STATE_COMPLETE = "complete";
0126:
0127: /** The owning page of this node. */
0128: private final Page page_;
0129:
0130: /** The parent node. */
0131: private DomNode parent_;
0132:
0133: /**
0134: * The previous sibling. The first child's <code>previousSibling</code> points
0135: * to the end of the list
0136: */
0137: private DomNode previousSibling_;
0138:
0139: /**
0140: * The next sibling. The last child's <code>nextSibling</code> is <code>null</code>
0141: */
0142: private DomNode nextSibling_;
0143:
0144: /** Start of the child list */
0145: private DomNode firstChild_;
0146:
0147: /**
0148: * This is the JavaScript object corresponding to this DOM node. It may
0149: * be null if there isn't a corresponding JavaScript object.
0150: */
0151: private transient ScriptableObject scriptObject_;
0152:
0153: /** The ready state is is an IE-only value that is available to a large number of elements. */
0154: private String readyState_;
0155:
0156: /** We do lazy initialization on this since the vast majority of HtmlElement instances won't need it. */
0157: private PropertyChangeSupport propertyChangeSupport_;
0158:
0159: /** The name of the "element" property. Used when watching property change events. */
0160: public static final String PROPERTY_ELEMENT = "element";
0161:
0162: /**
0163: * The line number in the source page where the DOM node starts.
0164: */
0165: private int startLineNumber_;
0166:
0167: /**
0168: * The column number in the source page where the DOM node starts.
0169: */
0170: private int startColumnNumber_;
0171:
0172: /**
0173: * The line number in the source page where the DOM node ends.
0174: */
0175: private int endLineNumber_;
0176:
0177: /**
0178: * The column number in the source page where the DOM node ends.
0179: */
0180: private int endColumnNumber_;
0181:
0182: private List/* DomChangeListener */domListeners_;
0183: private final transient Object domListeners_lock_ = new Object();
0184:
0185: /**
0186: * Never call this, used for Serialization.
0187: * @deprecated
0188: */
0189: protected DomNode() {
0190: this (null);
0191: }
0192:
0193: /**
0194: * Creates an instance.
0195: * @param page The page which contains this node.
0196: */
0197: protected DomNode(final Page page) {
0198: readyState_ = READY_STATE_LOADING;
0199: page_ = page;
0200: startLineNumber_ = 0;
0201: startColumnNumber_ = 0;
0202: endLineNumber_ = 0;
0203: endColumnNumber_ = 0;
0204: }
0205:
0206: /**
0207: * Set the line and column numbers in the source page where the
0208: * DOM node starts.
0209: *
0210: * @param startLineNumber The line number where the DOM node starts.
0211: * @param startColumnNumber The column number where the DOM node starts.
0212: */
0213: void setStartLocation(final int startLineNumber,
0214: final int startColumnNumber) {
0215: startLineNumber_ = startLineNumber;
0216: startColumnNumber_ = startColumnNumber;
0217: }
0218:
0219: /**
0220: * Set the line and column numbers in the source page where the
0221: * DOM node ends.
0222: *
0223: * @param endLineNumber The line number where the DOM node ends.
0224: * @param endColumnNumber The column number where the DOM node ends.
0225: */
0226: void setEndLocation(final int endLineNumber,
0227: final int endColumnNumber) {
0228: endLineNumber_ = endLineNumber;
0229: endColumnNumber_ = endColumnNumber;
0230: }
0231:
0232: /**
0233: * Get the line number in the source page where the DOM node starts.
0234: *
0235: * @return See above.
0236: */
0237: public int getStartLineNumber() {
0238: return startLineNumber_;
0239: }
0240:
0241: /**
0242: * Get the column number in the source page where the DOM node starts.
0243: *
0244: * @return See above.
0245: */
0246: public int getStartColumnNumber() {
0247: return startColumnNumber_;
0248: }
0249:
0250: /**
0251: * Get the line number in the source page where the DOM node ends.
0252: *
0253: * @return See above.
0254: */
0255: public int getEndLineNumber() {
0256: return endLineNumber_;
0257: }
0258:
0259: /**
0260: * Get the column number in the source page where the DOM node ends.
0261: *
0262: * @return See above.
0263: */
0264: public int getEndColumnNumber() {
0265: return endColumnNumber_;
0266: }
0267:
0268: /**
0269: * Return the HtmlPage that contains this node
0270: *
0271: * @return See above
0272: */
0273: public HtmlPage getPage() {
0274: return (HtmlPage) page_;
0275: }
0276:
0277: /**
0278: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0279: * Returns the Page interface, should be removed, and use {@link #getPage()} instead.
0280: *
0281: * @return the Page interface.
0282: */
0283: public Page getNativePage() {
0284: return page_;
0285: }
0286:
0287: /**
0288: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0289: *
0290: * Set the javascript object that corresponds to this node. This is not
0291: * guaranteed to be set even if there is a javascript object for this
0292: * DOM node.
0293: * @param scriptObject The javascript object.
0294: */
0295: public void setScriptObject(final ScriptableObject scriptObject) {
0296: scriptObject_ = scriptObject;
0297: }
0298:
0299: /**
0300: * Get the last child node.
0301: * @return The last child node or null if the current node has
0302: * no children.
0303: * @deprecated This method conflicts with the W3C DOM API since the return values are
0304: * different. Use getLastDomChild instead.
0305: */
0306: public DomNode getLastChild() {
0307: return getLastDomChild();
0308: }
0309:
0310: /**
0311: * Get the last child DomNode.
0312: * @return The last child node or null if the current node has
0313: * no children.
0314: */
0315: public DomNode getLastDomChild() {
0316: if (firstChild_ != null) {
0317: // last child is stored as the previous sibling of first child
0318: return firstChild_.previousSibling_;
0319: } else {
0320: return null;
0321: }
0322: }
0323:
0324: /**
0325: * @return the parent of this node, which may be <code>null</code> if this
0326: * is the root node
0327: * @deprecated This method conflicts with the W3C DOM API since the return values are
0328: * different. Use getParentDomNode instead.
0329: */
0330: public DomNode getParentNode() {
0331: return getParentDomNode();
0332: }
0333:
0334: /**
0335: * @return the parent DomNode of this node, which may be <code>null</code> if this
0336: * is the root node
0337: */
0338: public DomNode getParentDomNode() {
0339: return parent_;
0340: }
0341:
0342: /**
0343: * Set the parent node
0344: * @param parent the parent node
0345: */
0346: protected void setParentNode(final DomNode parent) {
0347: parent_ = parent;
0348: }
0349:
0350: /**
0351: * @return the previous sibling of this node, or <code>null</code> if this is
0352: * the first node
0353: * @deprecated This method conflicts with the W3C DOM API since the return values are
0354: * different. Use getPreviousDomSibling instead.
0355: */
0356: public DomNode getPreviousSibling() {
0357: return getPreviousDomSibling();
0358: }
0359:
0360: /**
0361: * @return the previous sibling of this node, or <code>null</code> if this is
0362: * the first node
0363: */
0364: public DomNode getPreviousDomSibling() {
0365: if (parent_ == null || this == parent_.firstChild_) {
0366: // previous sibling of first child points to last child
0367: return null;
0368: } else {
0369: return previousSibling_;
0370: }
0371: }
0372:
0373: /**
0374: * @return the next sibling
0375: * @deprecated This method conflicts with the W3C DOM API since the return values are
0376: * different. Use getNextDomSibling instead.
0377: */
0378: public DomNode getNextSibling() {
0379: return getNextDomSibling();
0380: }
0381:
0382: /**
0383: * @return the next sibling
0384: */
0385: public DomNode getNextDomSibling() {
0386: return nextSibling_;
0387: }
0388:
0389: /**
0390: * @return the previous sibling
0391: * @deprecated This method conflicts with the W3C DOM API since the return values are
0392: * different. Use getFirstDomChild instead.
0393: */
0394: public DomNode getFirstChild() {
0395: return getFirstDomChild();
0396: }
0397:
0398: /**
0399: * @return the previous sibling
0400: */
0401: public DomNode getFirstDomChild() {
0402: return firstChild_;
0403: }
0404:
0405: /**
0406: * Returns <tt>true</tt> if this node is an ancestor of the specified node.
0407: *
0408: * @param node the node to check
0409: * @return <tt>true</tt> if this node is an ancestor of the specified node
0410: */
0411: public boolean isAncestorOf(DomNode node) {
0412: while (node != null) {
0413: if (node == this ) {
0414: return true;
0415: }
0416: node = node.getParentDomNode();
0417: }
0418: return false;
0419: }
0420:
0421: /** @param previous set the previousSibling field value */
0422: protected void setPreviousSibling(final DomNode previous) {
0423: previousSibling_ = previous;
0424: }
0425:
0426: /** @param next set the nextSibling field value */
0427: protected void setNextSibling(final DomNode next) {
0428: nextSibling_ = next;
0429: }
0430:
0431: /**
0432: * Get the type of the current node.
0433: * @return The node type
0434: */
0435: public abstract short getNodeType();
0436:
0437: /**
0438: * Get the name for the current node.
0439: * @return The node name
0440: */
0441: public abstract String getNodeName();
0442:
0443: /**
0444: * The namespace URI of this node, or null if it is unspecified (see ). This is not a
0445: * computed value that is the result of a namespace lookup based on an examination of the
0446: * namespace declarations in scope. It is merely the namespace URI given at creation time.
0447: * For nodes of any type other than ELEMENT_NODE and ATTRIBUTE_NODE and nodes created with
0448: * a DOM Level 1 method, such as Document.createElement(), this is always null.
0449: * @return The URI that identifies an XML namespace.
0450: */
0451: public String getNamespaceURI() {
0452: return null;
0453: }
0454:
0455: /**
0456: * Returns the local part of the qualified name of this node. For nodes of any
0457: * type other than ELEMENT_NODE and ATTRIBUTE_NODE and nodes created with a DOM Level 1
0458: * method, such as Document.createElement(), this is always null.
0459: * @return The local name (without prefix).
0460: */
0461: public String getLocalName() {
0462: return null;
0463: }
0464:
0465: /**
0466: * The namespace prefix of this node, or null if it is unspecified.
0467: * @return The Namespace prefix.
0468: */
0469: public String getPrefix() {
0470: return null;
0471: }
0472:
0473: /**
0474: * Set the namespace prefix of this node, or null if it is unspecified. When it is defined
0475: * to be null, setting it has no effect, including if the node is read-only. Note that setting
0476: * this attribute, when permitted, changes the nodeName attribute, which holds the qualified
0477: * name, as well as the tagName and name attributes of the Element and Attr interfaces, when
0478: * applicable. Setting the prefix to null makes it unspecified, setting it to an empty string
0479: * is implementation dependent. Note also that changing the prefix of an attribute that is
0480: * known to have a default value, does not make a new attribute with the default value and the
0481: * original prefix appear, since the namespaceURI and localName do not change. For nodes of
0482: * any type other than ELEMENT_NODE and ATTRIBUTE_NODE and nodes created with a DOM Level 1
0483: * method, such as createElement from the Document interface, this is always null.
0484: * @param prefix The namespace prefix of this node, or null if it is unspecified.
0485: */
0486: public void setPrefix(final String prefix) {
0487: }
0488:
0489: /**
0490: * Return whether this node has any attributes.
0491: *
0492: * @return true if the node has attributes, false otherwise.
0493: */
0494: public boolean hasAttributes() {
0495: return false;
0496: }
0497:
0498: /**
0499: * Returns a flag indicating whether or not this node itself results
0500: * in any space taken up in the browser windows; for instance, "<b>"
0501: * affects the specified text, but does not use up any space itself
0502: *
0503: * @return The flag
0504: */
0505: protected boolean isRenderedVisible() {
0506: return false;
0507: }
0508:
0509: /**
0510: * Returns a flag indicating whether or not this node should
0511: * have any leading and trailing whitespace removed when asText()
0512: * is called; mostly this should be true, but must be false for
0513: * such things as text formatting tags
0514: *
0515: * @return The flag
0516: */
0517: protected boolean isTrimmedText() {
0518: return true;
0519: }
0520:
0521: /**
0522: * Returns a text representation of this element that represents what would
0523: * be visible to the user if this page was shown in a web browser. For
0524: * example, a single-selection select element would return the currently selected
0525: * value as text.
0526: *
0527: * @return The element as text.
0528: */
0529: public String asText() {
0530: String text = getChildrenAsText();
0531: text = reduceWhitespace(text);
0532:
0533: if (isTrimmedText()) {
0534: text = text.trim();
0535: }
0536:
0537: return text;
0538: }
0539:
0540: /**
0541: * Return a text string that represents all the child elements as they
0542: * would be visible in a web browser
0543: *
0544: * @return See above
0545: * @see #asText()
0546: */
0547: protected final String getChildrenAsText() {
0548: final StringBuffer buffer = new StringBuffer();
0549: final Iterator childIterator = getChildIterator();
0550:
0551: if (!childIterator.hasNext()) {
0552: return "";
0553: }
0554: boolean previousNodeWasText = false;
0555: final StringBuffer textBuffer = new StringBuffer();
0556: while (childIterator.hasNext()) {
0557: final DomNode node = (DomNode) childIterator.next();
0558: if (node instanceof DomText) {
0559: textBuffer.append(((DomText) node).getData());
0560: previousNodeWasText = true;
0561: } else {
0562: if (previousNodeWasText) {
0563: // Whitespace between adjacent text nodes should reamin as a single
0564: // space. So, append raw adjacent text and reduce it as a whole.
0565: buffer.append(reduceWhitespace(textBuffer
0566: .toString()));
0567: textBuffer.setLength(0);
0568: previousNodeWasText = false;
0569: }
0570:
0571: if (node.isRenderedVisible()) {
0572: buffer.append(" ");
0573: buffer.append(node.asText());
0574: buffer.append(" ");
0575: } else if (node.getNodeName().equals("p")) {
0576: // this is a bit kludgey, but we can't add the space
0577: // inside the node's asText(), since it doesn't belong
0578: // with the contents of the 'p' tag
0579: buffer.append(" ");
0580: buffer.append(node.asText());
0581: } else {
0582: buffer.append(node.asText());
0583: }
0584: }
0585: }
0586: if (previousNodeWasText) {
0587: // we ended with text
0588: buffer.append(textBuffer.toString());
0589: }
0590:
0591: return buffer.toString();
0592: }
0593:
0594: /**
0595: * Removes extra whitespace from a string similar to what a browser does
0596: * when it displays text.
0597: * @param text The text to clean up.
0598: * @return The cleaned up text.
0599: */
0600: protected static String reduceWhitespace(final String text) {
0601: final StringBuffer buffer = new StringBuffer(text.length());
0602: final int length = text.length();
0603: boolean whitespace = false;
0604: for (int i = 0; i < length; i++) {
0605: final char ch = text.charAt(i);
0606:
0607: // Translate non-breaking space to regular space.
0608: if (ch == (char) 160) {
0609: buffer.append(' ');
0610: whitespace = false;
0611: } else {
0612: if (whitespace) {
0613: if (!Character.isWhitespace(ch)) {
0614: buffer.append(ch);
0615: whitespace = false;
0616: }
0617: } else {
0618: if (Character.isWhitespace(ch)) {
0619: whitespace = true;
0620: buffer.append(' ');
0621: } else {
0622: buffer.append(ch);
0623: }
0624: }
0625: }
0626: }
0627: return buffer.toString();
0628: }
0629:
0630: /**
0631: * Return the log object for this element.
0632: * @return The log object for this element.
0633: */
0634: protected final Log getLog() {
0635: return LogFactory.getLog(getClass());
0636: }
0637:
0638: /**
0639: * Return a string representation of the xml document from this element and all
0640: * it's children (recursively).
0641: *
0642: * @return The xml string.
0643: */
0644: public String asXml() {
0645: final StringWriter stringWriter = new StringWriter();
0646: final PrintWriter printWriter = new PrintWriter(stringWriter);
0647: printXml("", printWriter);
0648: printWriter.close();
0649: return stringWriter.toString();
0650: }
0651:
0652: /**
0653: * recursively write the XML data for the node tree starting at <code>node</code>
0654: *
0655: * @param indent white space to indent child nodes
0656: * @param printWriter writer where child nodes are written
0657: */
0658: protected void printXml(final String indent,
0659: final PrintWriter printWriter) {
0660: printWriter.println(indent + this );
0661: printChildrenAsXml(indent, printWriter);
0662: }
0663:
0664: /**
0665: * recursively write the XML data for the node tree starting at <code>node</code>
0666: *
0667: * @param indent white space to indent child nodes
0668: * @param printWriter writer where child nodes are written
0669: */
0670: protected void printChildrenAsXml(final String indent,
0671: final PrintWriter printWriter) {
0672: DomNode child = getFirstDomChild();
0673: while (child != null) {
0674: child.printXml(indent + " ", printWriter);
0675: child = child.getNextDomSibling();
0676: }
0677: }
0678:
0679: /**
0680: * Get the value for the current node.
0681: * @return The node value
0682: */
0683: public String getNodeValue() {
0684: return null;
0685: }
0686:
0687: /**
0688: * @param x The new value
0689: */
0690: public void setNodeValue(final String x) {
0691: // Default behavior is to do nothing, overridden in some subclasses
0692: }
0693:
0694: /**
0695: * Make a clone of this node
0696: *
0697: * @param deep if <code>true</code>, the clone will be propagated to the whole subtree
0698: * below this one. Otherwise, the new node will not have any children. The page reference
0699: * will always be the same as this node's.
0700: * @return a new node
0701: * @deprecated This method conflicts with the W3C DOM API since the return values are
0702: * different. Use cloneDomNode instead.
0703: */
0704: public DomNode cloneNode(final boolean deep) {
0705: return cloneDomNode(deep);
0706: }
0707:
0708: /**
0709: * Make a clone of this node
0710: *
0711: * @param deep if <code>true</code>, the clone will be propagated to the whole subtree
0712: * below this one. Otherwise, the new node will not have any children. The page reference
0713: * will always be the same as this node's.
0714: * @return a new node
0715: */
0716: public DomNode cloneDomNode(final boolean deep) {
0717: final DomNode newnode;
0718: try {
0719: newnode = (DomNode) clone();
0720: } catch (final CloneNotSupportedException e) {
0721: throw new IllegalStateException(
0722: "Clone not supported for node [" + this + "]");
0723: }
0724:
0725: newnode.parent_ = null;
0726: newnode.nextSibling_ = null;
0727: newnode.previousSibling_ = null;
0728: newnode.firstChild_ = null;
0729: newnode.scriptObject_ = null;
0730:
0731: // if deep, clone the kids too.
0732: if (deep) {
0733: for (DomNode child = firstChild_; child != null; child = child.nextSibling_) {
0734: newnode.appendDomChild(child.cloneDomNode(true));
0735: }
0736: }
0737: return newnode;
0738: }
0739:
0740: /**
0741: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0742: *
0743: * The logic of when and where the js object is created needs a clean up: functions using
0744: * a js object of a dom node should not have to look if they should create it first
0745: * Return the javascript object that corresponds to this node.
0746: * @return The javascript object that corresponds to this node building it if necessary.
0747: */
0748: public ScriptableObject getScriptObject() {
0749: if (scriptObject_ == null) {
0750: if (this == page_) {
0751: throw new IllegalStateException(
0752: "No script object associated with the Page");
0753: }
0754: scriptObject_ = ((SimpleScriptable) ((DomNode) page_)
0755: .getScriptObject()).makeScriptableFor(this );
0756: }
0757: return scriptObject_;
0758: }
0759:
0760: /**
0761: * append a child node to the end of the current list
0762: * @param node the node to append
0763: * @return the node added
0764: * @deprecated This method conflicts with the W3C DOM API since the return values are
0765: * different. Use appendDomChild instead.
0766: */
0767: public DomNode appendChild(final DomNode node) {
0768: return appendDomChild(node);
0769: }
0770:
0771: /**
0772: * append a child node to the end of the current list
0773: * @param node the node to append
0774: * @return the node added
0775: */
0776: public DomNode appendDomChild(final DomNode node) {
0777: if (node instanceof DomDocumentFragment) {
0778: final DomDocumentFragment fragment = (DomDocumentFragment) node;
0779: for (final Iterator iterator = fragment.getChildIterator(); iterator
0780: .hasNext();) {
0781: final DomNode child = (DomNode) iterator.next();
0782: appendDomChild(child);
0783: }
0784: } else {
0785: //clean up the new node, in case it is being moved
0786: if (node != this ) {
0787: node.basicRemove();
0788: }
0789: if (firstChild_ == null) {
0790: firstChild_ = node;
0791: firstChild_.previousSibling_ = node;
0792: } else {
0793: final DomNode last = getLastDomChild();
0794:
0795: last.nextSibling_ = node;
0796: node.previousSibling_ = last;
0797: node.nextSibling_ = null; //safety first
0798: firstChild_.previousSibling_ = node; //new last node
0799: }
0800: node.parent_ = this ;
0801:
0802: //TODO: should be
0803: // if (!(this instanceof DomDocumentFragment) && getPage() instanceof HtmlPage)
0804: if (!(this instanceof DomDocumentFragment)
0805: && (page_ instanceof HtmlPage || this instanceof HtmlPage)) {
0806: getPage().notifyNodeAdded(node);
0807: }
0808: fireNodeAdded(this , node);
0809: }
0810: return node;
0811: }
0812:
0813: /**
0814: * Inserts a new child node before this node into the child relationship this node is a
0815: * part of. If the specified node is this node, this method is a no-op.
0816: *
0817: * @param newNode the new node to insert
0818: * @throws IllegalStateException if this node is not a child of any other node
0819: */
0820: public void insertBefore(final DomNode newNode)
0821: throws IllegalStateException {
0822:
0823: if (previousSibling_ == null) {
0824: throw new IllegalStateException();
0825: }
0826:
0827: if (newNode == this ) {
0828: return;
0829: }
0830:
0831: //clean up the new node, in case it is being moved
0832: newNode.basicRemove();
0833:
0834: if (parent_.firstChild_ == this ) {
0835: parent_.firstChild_ = newNode;
0836: } else {
0837: previousSibling_.nextSibling_ = newNode;
0838: }
0839: newNode.previousSibling_ = previousSibling_;
0840: newNode.nextSibling_ = this ;
0841: previousSibling_ = newNode;
0842: newNode.parent_ = parent_;
0843:
0844: getPage().notifyNodeAdded(newNode);
0845: fireNodeAdded(this , newNode);
0846: }
0847:
0848: /**
0849: * Removes this node from all relationships with other nodes.
0850: * @throws IllegalStateException if this node is not a child of any other node
0851: */
0852: public void remove() throws IllegalStateException {
0853: if (previousSibling_ == null) {
0854: throw new IllegalStateException();
0855: }
0856: final DomNode exParent = parent_;
0857: basicRemove();
0858:
0859: if (getNativePage() instanceof HtmlPage) {
0860: getPage().notifyNodeRemoved(this );
0861: }
0862:
0863: fireNodeDeleted(exParent, this );
0864: //ask ex-parent to fire event (because we don't have parent now)
0865: exParent.fireNodeDeleted(exParent, this );
0866: }
0867:
0868: /**
0869: * Cuts off all relationships this node has with siblings and parents.
0870: */
0871: private void basicRemove() {
0872: if (parent_ != null && parent_.firstChild_ == this ) {
0873: parent_.firstChild_ = nextSibling_;
0874: } else if (previousSibling_ != null
0875: && previousSibling_.nextSibling_ == this ) {
0876: previousSibling_.nextSibling_ = nextSibling_;
0877: }
0878: if (nextSibling_ != null
0879: && nextSibling_.previousSibling_ == this ) {
0880: nextSibling_.previousSibling_ = previousSibling_;
0881: }
0882: if (parent_ != null && this == parent_.getLastDomChild()) {
0883: parent_.firstChild_.previousSibling_ = previousSibling_;
0884: }
0885:
0886: nextSibling_ = null;
0887: previousSibling_ = null;
0888: parent_ = null;
0889: }
0890:
0891: /**
0892: * Replaces this node with another node. If the specified node is this node, this
0893: * method is a no-op.
0894: *
0895: * @param newNode the node to replace this one
0896: * @throws IllegalStateException if this node is not a child of any other node
0897: */
0898: public void replace(final DomNode newNode)
0899: throws IllegalStateException {
0900: if (newNode != this ) {
0901: insertBefore(newNode);
0902: remove();
0903: }
0904: }
0905:
0906: /**
0907: * Lifecycle method invoked whenever a node is added to a page. Intended to
0908: * be overridden by nodes which need to perform custom logic when they are
0909: * added to a page. This method is recursive, so if you override it, please
0910: * be sure to call <tt>super.onAddedToPage()</tt>.
0911: */
0912: protected void onAddedToPage() {
0913: if (firstChild_ != null) {
0914: for (final Iterator i = getChildIterator(); i.hasNext();) {
0915: final DomNode child = (DomNode) i.next();
0916: child.onAddedToPage();
0917: }
0918: }
0919: }
0920:
0921: /**
0922: * Lifecycle method invoked after a node and all its children have been added to a page, during
0923: * parsing of the HTML. Intended to be overridden by nodes which need to perform custom logic
0924: * after they and all their child nodes have been processed by the HTML parser. This method is
0925: * not recursive, and the default implementation is empty, so there is no need to call
0926: * <tt>super.onAllChildrenAddedToPage()</tt> if you implement this method.
0927: */
0928: protected void onAllChildrenAddedToPage() {
0929: // Empty by default.
0930: }
0931:
0932: /**
0933: * @return an iterator over the children of this node
0934: */
0935: public Iterator getChildIterator() {
0936: return new ChildIterator();
0937: }
0938:
0939: // TODO: remove event handlers methods! Nothing to do in DomNode!
0940: /**
0941: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0942: * Return a Function to be executed when a given event occurs.
0943: * @param eventName Name of event such as "onclick" or "onblur", etc.
0944: * @return A rhino javascript executable Function, or null if no event
0945: * handler has been defined
0946: */
0947: public Function getEventHandler(final String eventName) {
0948: return null;
0949: }
0950:
0951: /**
0952: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0953: * Register a Function as an event handler.
0954: * @param eventName Name of event such as "onclick" or "onblur", etc.
0955: * @param eventHandler A rhino javascript executable Function
0956: */
0957: public void setEventHandler(final String eventName,
0958: final Function eventHandler) {
0959: }
0960:
0961: /**
0962: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0963: * Register a snippet of javascript code as an event handler. The javascript code will
0964: * be wrapped inside a unique function declaration which provides one argument named
0965: * "event"
0966: * @param eventName Name of event such as "onclick" or "onblur", etc.
0967: * @param jsSnippet executable javascript code
0968: */
0969: public void setEventHandler(final String eventName,
0970: final String jsSnippet) {
0971: }
0972:
0973: /**
0974: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0975: * Removes the specified event handler.
0976: * @param eventName Name of the event such as "onclick" or "onblur", etc.
0977: */
0978: public void removeEventHandler(final String eventName) {
0979: }
0980:
0981: /**
0982: * Add a property change listener to this node.
0983: * @param listener The new listener.
0984: * @deprecated Not used
0985: */
0986: //deprecated after 1.11
0987: public final synchronized void addPropertyChangeListener(
0988: final PropertyChangeListener listener) {
0989: Assert.notNull("listener", listener);
0990: if (propertyChangeSupport_ == null) {
0991: propertyChangeSupport_ = new PropertyChangeSupport(this );
0992: }
0993: propertyChangeSupport_.addPropertyChangeListener(listener);
0994: }
0995:
0996: /**
0997: * Remove a property change listener from this node.
0998: * @param listener The listener.
0999: * @deprecated Not used
1000: */
1001: //deprecated after 1.11
1002: public final synchronized void removePropertyChangeListener(
1003: final PropertyChangeListener listener) {
1004: Assert.notNull("listener", listener);
1005: if (propertyChangeSupport_ != null) {
1006: propertyChangeSupport_
1007: .removePropertyChangeListener(listener);
1008: }
1009: }
1010:
1011: /**
1012: * Fire a property change event
1013: * @param propertyName The name of the property.
1014: * @param oldValue The old value.
1015: * @param newValue The new value.
1016: * @deprecated Not used
1017: */
1018: //deprecated after 1.11
1019: protected final synchronized void firePropertyChange(
1020: final String propertyName, final Object oldValue,
1021: final Object newValue) {
1022:
1023: if (propertyChangeSupport_ != null) {
1024: propertyChangeSupport_.firePropertyChange(propertyName,
1025: oldValue, newValue);
1026: }
1027: }
1028:
1029: /**
1030: * an iterator over all children of this node
1031: */
1032: protected class ChildIterator implements Iterator {
1033:
1034: private DomNode nextNode_ = firstChild_;
1035: private DomNode currentNode_ = null;
1036:
1037: /** @return whether there is a next object */
1038: public boolean hasNext() {
1039: return nextNode_ != null;
1040: }
1041:
1042: /** @return the next object */
1043: public Object next() {
1044: if (nextNode_ != null) {
1045: currentNode_ = nextNode_;
1046: nextNode_ = nextNode_.nextSibling_;
1047: return currentNode_;
1048: } else {
1049: throw new NoSuchElementException();
1050: }
1051: }
1052:
1053: /** remove the current object */
1054: public void remove() {
1055: if (currentNode_ == null) {
1056: throw new IllegalStateException();
1057: }
1058: currentNode_.remove();
1059: }
1060: }
1061:
1062: /**
1063: * Return an iterator that will recursively iterate over every child element
1064: * below this one.
1065: * @return The iterator.
1066: */
1067: public Iterator getAllHtmlChildElements() {
1068: return new DescendantElementsIterator();
1069: }
1070:
1071: /**
1072: * An iterator over all HtmlElement descendants in document order.
1073: */
1074: protected class DescendantElementsIterator implements Iterator {
1075:
1076: private HtmlElement nextElement_ = getFirstChildElement(DomNode.this );
1077:
1078: /** @return is there a next one? */
1079: public boolean hasNext() {
1080: return nextElement_ != null;
1081: }
1082:
1083: /** @return the next one */
1084: public Object next() {
1085: return nextElement();
1086: }
1087:
1088: /** @throws UnsupportedOperationException always */
1089: public void remove() throws UnsupportedOperationException {
1090: throw new UnsupportedOperationException();
1091: }
1092:
1093: /** @return is there a next one? */
1094: public HtmlElement nextElement() {
1095: final HtmlElement result = nextElement_;
1096: setNextElement();
1097: return result;
1098: }
1099:
1100: private void setNextElement() {
1101: HtmlElement next = getFirstChildElement(nextElement_);
1102: if (next == null) {
1103: next = getNextDomSibling(nextElement_);
1104: }
1105: if (next == null) {
1106: next = getNextElementUpwards(nextElement_);
1107: }
1108: nextElement_ = next;
1109: }
1110:
1111: private HtmlElement getNextElementUpwards(
1112: final DomNode startingNode) {
1113: if (startingNode == DomNode.this ) {
1114: return null;
1115: }
1116:
1117: final DomNode parent = startingNode.getParentDomNode();
1118: if (parent == DomNode.this ) {
1119: return null;
1120: }
1121:
1122: DomNode next = parent.getNextDomSibling();
1123: while (next != null && next instanceof HtmlElement == false) {
1124: next = next.getNextDomSibling();
1125: }
1126:
1127: if (next == null) {
1128: return getNextElementUpwards(parent);
1129: } else {
1130: return (HtmlElement) next;
1131: }
1132: }
1133:
1134: private HtmlElement getFirstChildElement(final DomNode parent) {
1135: if (parent instanceof HtmlNoScript
1136: && getPage().getWebClient().isJavaScriptEnabled()) {
1137: return null;
1138: }
1139: DomNode node = parent.getFirstDomChild();
1140: while (node != null && node instanceof HtmlElement == false) {
1141: node = node.getNextDomSibling();
1142: }
1143: return (HtmlElement) node;
1144: }
1145:
1146: private HtmlElement getNextDomSibling(final HtmlElement element) {
1147: DomNode node = element.getNextDomSibling();
1148: while (node != null && node instanceof HtmlElement == false) {
1149: node = node.getNextDomSibling();
1150: }
1151: return (HtmlElement) node;
1152: }
1153: }
1154:
1155: /**
1156: * Return this node's ready state (IE only).
1157: * @return This node's ready state.
1158: */
1159: public String getReadyState() {
1160: return readyState_;
1161: }
1162:
1163: /**
1164: * Sets this node's ready state (IE only).
1165: * @param state This node's ready state.
1166: */
1167: public void setReadyState(final String state) {
1168: readyState_ = state;
1169: }
1170:
1171: /**
1172: * Remove all the children of this node.
1173: *
1174: */
1175: public void removeAllChildren() {
1176: if (getFirstDomChild() == null) {
1177: return;
1178: }
1179: final Iterator it = getChildIterator();
1180: DomNode child;
1181: while (it.hasNext()) {
1182: child = (DomNode) it.next();
1183: child.removeAllChildren();
1184: it.remove();
1185: }
1186: }
1187:
1188: /**
1189: * Facility to evaluate an xpath from the current node. The current node is considered as the
1190: * document root for the evaluation therefore parent nodes can't be reached.
1191: * @param xpathExpr the xpath expression
1192: * @return See {@link XPath#selectNodes(Object)}
1193: * @throws JaxenException if the xpath expression can't be parsed/evaluated
1194: */
1195: public List getByXPath(final String xpathExpr)
1196: throws JaxenException {
1197: if (xpathExpr == null) {
1198: throw new NullPointerException(
1199: "Null is not a valid xpath expression");
1200: }
1201:
1202: final Navigator navigator = HtmlUnitXPath
1203: .buildSubtreeNavigator(this );
1204: final HtmlUnitXPath xpath = new HtmlUnitXPath(xpathExpr,
1205: navigator);
1206: return xpath.selectNodes(this );
1207: }
1208:
1209: /**
1210: * Facility to evaluate an xpath from the current node and get the first result.
1211: * The current node is considered as the document root for the evaluation
1212: * therefore parent nodes can't be reached.
1213: * @param xpathExpr the xpath expression
1214: * @return <code>null</code> if no result is found, the first one otherwise
1215: * @throws JaxenException if the xpath expression can't be parsed/evaluated
1216: */
1217: public Object getFirstByXPath(final String xpathExpr)
1218: throws JaxenException {
1219: final List results = getByXPath(xpathExpr);
1220: if (results.isEmpty()) {
1221: return null;
1222: } else {
1223: return results.get(0);
1224: }
1225: }
1226:
1227: /**
1228: * Facility to notify the registered {@link IncorrectnessListener} of something that is not fully correct.
1229: * @param message the notification
1230: */
1231: protected void notifyIncorrectness(final String message) {
1232: final IncorrectnessListener incorrectnessListener = getPage()
1233: .getWebClient().getIncorrectnessListener();
1234: incorrectnessListener.notify(message, this );
1235: }
1236:
1237: /**
1238: * Adds a DomChangeListener to the listener list.
1239: * The listener is registered for all children nodes of this DomNode,
1240: * as well as the descendant nodes.
1241: *
1242: * @param listener the dom structure change listener to be added.
1243: * @see #removeDomChangeListener(DomChangeListener)
1244: */
1245: public void addDomChangeListener(final DomChangeListener listener) {
1246: Assert.notNull("listener", listener);
1247: synchronized (domListeners_lock_) {
1248: if (domListeners_ == null) {
1249: domListeners_ = new ArrayList();
1250: }
1251: if (!domListeners_.contains(listener)) {
1252: domListeners_.add(listener);
1253: }
1254: }
1255: }
1256:
1257: /**
1258: * Removes an DomChangeListener from the listener list.
1259: * This method should be used to remove DomChangeListener that were registered
1260: * for all children nodes and descendant nodes of this DomNode.
1261: *
1262: * @param listener the dom structure change listener to be removed.
1263: * @see #addDomChangeListener(DomChangeListener)
1264: */
1265: public void removeDomChangeListener(final DomChangeListener listener) {
1266: Assert.notNull("listener", listener);
1267: synchronized (domListeners_lock_) {
1268: if (domListeners_ != null) {
1269: domListeners_.remove(listener);
1270: }
1271: }
1272: }
1273:
1274: /**
1275: * Support for reporting DOM changes. This method can be called when a node has been added and it
1276: * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
1277: *
1278: * Note that this method recursively calls this node's parent's {@link #fireNodeAdded(DomNode, DomNode)}.
1279: *
1280: * @param parentNode the parent of the node that was added.
1281: * @param addedNode the node that was added.
1282: */
1283: protected void fireNodeAdded(final DomNode parentNode,
1284: final DomNode addedNode) {
1285: final List listeners = safeGetDomListeners();
1286: if (listeners != null) {
1287: final DomChangeEvent event = new DomChangeEvent(parentNode,
1288: addedNode);
1289: for (final Iterator iterator = listeners.iterator(); iterator
1290: .hasNext();) {
1291: final DomChangeListener listener = (DomChangeListener) iterator
1292: .next();
1293: listener.nodeAdded(event);
1294: }
1295: }
1296: if (parent_ != null) {
1297: parent_.fireNodeAdded(parentNode, addedNode);
1298: }
1299: }
1300:
1301: /**
1302: * Support for reporting DOM changes. This method can be called when a node has been deleted and it
1303: * will send the appropriate {@link DomChangeEvent} to any registered {@link DomChangeListener}s.
1304: *
1305: * Note that this method recursively calls this node's parent's {@link #fireNodeDeleted(DomNode, DomNode)}.
1306: *
1307: * @param parentNode the parent of the node that was deleted.
1308: * @param deletedNode the node that was deleted.
1309: */
1310: protected void fireNodeDeleted(final DomNode parentNode,
1311: final DomNode deletedNode) {
1312: final List listeners = safeGetDomListeners();
1313: if (listeners != null) {
1314: final DomChangeEvent event = new DomChangeEvent(parentNode,
1315: deletedNode);
1316: for (final Iterator iterator = listeners.iterator(); iterator
1317: .hasNext();) {
1318: final DomChangeListener listener = (DomChangeListener) iterator
1319: .next();
1320: listener.nodeDeleted(event);
1321: }
1322: }
1323: if (parent_ != null) {
1324: parent_.fireNodeDeleted(parentNode, deletedNode);
1325: }
1326: }
1327:
1328: private List safeGetDomListeners() {
1329: synchronized (domListeners_lock_) {
1330: if (domListeners_ != null) {
1331: return new ArrayList(domListeners_);
1332: } else {
1333: return null;
1334: }
1335: }
1336: }
1337:
1338: }
|