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.io.IOException;
0041: import java.net.MalformedURLException;
0042: import java.net.URL;
0043: import java.util.ArrayList;
0044: import java.util.Arrays;
0045: import java.util.Collections;
0046: import java.util.Comparator;
0047: import java.util.HashMap;
0048: import java.util.Iterator;
0049: import java.util.List;
0050: import java.util.Map;
0051: import java.util.Vector;
0052:
0053: import org.apache.commons.httpclient.HttpStatus;
0054: import org.apache.commons.httpclient.util.EncodingUtil;
0055: import org.apache.commons.lang.StringUtils;
0056: import org.apache.commons.logging.Log;
0057: import org.apache.commons.logging.LogFactory;
0058: import org.jaxen.JaxenException;
0059: import org.mozilla.javascript.Context;
0060: import org.mozilla.javascript.Function;
0061: import org.mozilla.javascript.Script;
0062: import org.mozilla.javascript.Scriptable;
0063:
0064: import com.gargoylesoftware.htmlunit.Assert;
0065: import com.gargoylesoftware.htmlunit.ElementNotFoundException;
0066: import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
0067: import com.gargoylesoftware.htmlunit.OnbeforeunloadHandler;
0068: import com.gargoylesoftware.htmlunit.Page;
0069: import com.gargoylesoftware.htmlunit.ScriptException;
0070: import com.gargoylesoftware.htmlunit.ScriptResult;
0071: import com.gargoylesoftware.htmlunit.SgmlPage;
0072: import com.gargoylesoftware.htmlunit.TextUtil;
0073: import com.gargoylesoftware.htmlunit.WebAssert;
0074: import com.gargoylesoftware.htmlunit.WebClient;
0075: import com.gargoylesoftware.htmlunit.WebRequestSettings;
0076: import com.gargoylesoftware.htmlunit.WebResponse;
0077: import com.gargoylesoftware.htmlunit.WebWindow;
0078: import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
0079: import com.gargoylesoftware.htmlunit.javascript.host.Event;
0080: import com.gargoylesoftware.htmlunit.javascript.host.Node;
0081: import com.gargoylesoftware.htmlunit.javascript.host.Window;
0082:
0083: /**
0084: * A representation of an HTML page returned from a server. This class is the
0085: * DOM Document implementation.
0086: *
0087: * @version $Revision: 2155 $
0088: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
0089: * @author Alex Nikiforoff
0090: * @author Noboru Sinohara
0091: * @author David K. Taylor
0092: * @author Andreas Hangler
0093: * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
0094: * @author Chris Erskine
0095: * @author Marc Guillemot
0096: * @author Ahmed Ashour
0097: */
0098: public final class HtmlPage extends SgmlPage implements Cloneable {
0099:
0100: private static final long serialVersionUID = 1779746292119944291L;
0101:
0102: private String originalCharset_;
0103: private Map idMap_ = new HashMap(); // a map of (id, List(HtmlElement))
0104: private Map nameMap_ = new HashMap(); // a map of (name, List(HtmlElement))
0105: private HtmlElement documentElement_;
0106: private HtmlElement elementWithFocus_;
0107:
0108: private final transient Log javascriptLog_ = LogFactory
0109: .getLog("com.gargoylesoftware.htmlunit.javascript");
0110:
0111: private List/* HtmlAttributeChangeListener */attributeListeners_;
0112: private final transient Object lock_ = new Object(); // used for synchronization
0113:
0114: /**
0115: * Create an instance of HtmlPage
0116: *
0117: * @param originatingUrl The url that was used to load this page.
0118: * @param webResponse The web response that was used to create this page
0119: * @param webWindow The window that this page is being loaded into.
0120: */
0121: public HtmlPage(final URL originatingUrl,
0122: final WebResponse webResponse, final WebWindow webWindow) {
0123: super (webResponse, webWindow);
0124: }
0125:
0126: /**
0127: * @return this page
0128: */
0129: public HtmlPage getPage() {
0130: return this ;
0131: }
0132:
0133: /**
0134: * Initialize this page.
0135: * @throws IOException If an IO problem occurs.
0136: * @throws FailingHttpStatusCodeException If the server returns a failing status code AND the property
0137: * {@link WebClient#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
0138: */
0139: public void initialize() throws IOException,
0140: FailingHttpStatusCodeException {
0141: loadFrames();
0142: getDocumentHtmlElement().setReadyState(READY_STATE_COMPLETE);
0143: if (!getWebClient().getBrowserVersion().isIE()) {
0144: executeEventHandlersIfNeeded(Event.TYPE_DOM_DOCUMENT_LOADED);
0145: }
0146: executeDeferredScriptsIfNeeded();
0147: executeEventHandlersIfNeeded(Event.TYPE_LOAD);
0148: executeRefreshIfNeeded();
0149: }
0150:
0151: /**
0152: * Clean up this page.
0153: * @throws IOException If an IO problem occurs.
0154: */
0155: public void cleanUp() throws IOException {
0156: executeEventHandlersIfNeeded(Event.TYPE_UNLOAD);
0157: deregisterFramesIfNeeded();
0158: }
0159:
0160: /**
0161: * Get the root element of this document.
0162: * @return The root element
0163: * @deprecated This method conflicts with the W3C DOM API since the return values are
0164: * different. Use getDocumentHtmlElement instead.
0165: */
0166: public HtmlElement getDocumentElement() {
0167: return getDocumentHtmlElement();
0168: }
0169:
0170: /**
0171: * Get the root HtmlElement of this document.
0172: * @return The root element
0173: */
0174: public HtmlElement getDocumentHtmlElement() {
0175: if (documentElement_ == null) {
0176: DomNode childNode = getFirstDomChild();
0177: while (childNode != null
0178: && !(childNode instanceof HtmlElement)) {
0179: childNode = childNode.getNextDomSibling();
0180: }
0181: documentElement_ = (HtmlElement) childNode;
0182: }
0183: return documentElement_;
0184: }
0185:
0186: /**
0187: * Return the charset used in the page.
0188: * The sources of this information are from 1).meta element which
0189: * http-equiv attribute value is 'content-type', or if not from
0190: * the response header.
0191: * @return the value of charset.
0192: */
0193: public String getPageEncoding() {
0194: if (originalCharset_ != null) {
0195: return originalCharset_;
0196: }
0197:
0198: final List list = getMetaTags("content-type");
0199: for (int i = 0; i < list.size(); i++) {
0200: final HtmlMeta meta = (HtmlMeta) list.get(i);
0201: final String contents = meta.getContentAttribute();
0202: final int pos = contents.toLowerCase().indexOf("charset=");
0203: if (pos >= 0) {
0204: originalCharset_ = contents.substring(pos + 8);
0205: getLog().debug(
0206: "Page Encoding detected: " + originalCharset_);
0207: return originalCharset_;
0208: }
0209: }
0210: if (originalCharset_ == null) {
0211: originalCharset_ = getWebResponse().getContentCharSet();
0212: }
0213: return originalCharset_;
0214: }
0215:
0216: /**
0217: * Create a new HTML element with the given tag name.
0218: *
0219: * @param tagName The tag name, preferably in lowercase
0220: * @return the new HTML element.
0221: * @deprecated This method conflicts with the W3C DOM API since the return values are
0222: * different. Use createHtmlElement instead.
0223: */
0224: public HtmlElement createElement(final String tagName) {
0225: return createHtmlElement(tagName);
0226: }
0227:
0228: /**
0229: * Create a new HTML element with the given tag name.
0230: *
0231: * @param tagName The tag name, preferably in lowercase
0232: * @return the new HTML element.
0233: */
0234: public HtmlElement createHtmlElement(final String tagName) {
0235: final String tagLower = tagName.toLowerCase();
0236: return HTMLParser.getFactory(tagLower).createElement(this ,
0237: tagLower, null);
0238: }
0239:
0240: /**
0241: * Create a new HTML element with the given namespace and qualified name.
0242: *
0243: * @param namespaceURI the URI that identifies an XML namespace.
0244: * @param qualifiedName The qualified name of the element type to instantiate
0245: * @return the new HTML element.
0246: * @deprecated This method conflicts with the W3C DOM API since the return values are
0247: * different. Use createHtmlElementNS instead.
0248: */
0249: public HtmlElement createElementNS(final String namespaceURI,
0250: final String qualifiedName) {
0251: return createHtmlElementNS(namespaceURI, qualifiedName);
0252: }
0253:
0254: /**
0255: * Create a new HtmlElement with the given namespace and qualified name.
0256: *
0257: * @param namespaceURI the URI that identifies an XML namespace.
0258: * @param qualifiedName The qualified name of the element type to instantiate
0259: * @return the new HTML element.
0260: */
0261: public HtmlElement createHtmlElementNS(final String namespaceURI,
0262: final String qualifiedName) {
0263: final String tagLower = qualifiedName.toLowerCase().substring(
0264: qualifiedName.indexOf(':') + 1);
0265: return HTMLParser.getFactory(tagLower).createElementNS(this ,
0266: namespaceURI, qualifiedName, null);
0267: }
0268:
0269: /**
0270: * Return the HtmlAnchor with the specified name
0271: *
0272: * @param name The name to search by
0273: * @return See above
0274: * @throws ElementNotFoundException If the anchor could not be found.
0275: */
0276: public HtmlAnchor getAnchorByName(final String name)
0277: throws ElementNotFoundException {
0278: return (HtmlAnchor) getDocumentHtmlElement()
0279: .getOneHtmlElementByAttribute("a", "name", name);
0280: }
0281:
0282: /**
0283: * Return the {@link HtmlAnchor} with the specified href
0284: *
0285: * @param href The string to search by
0286: * @return The HtmlAnchor
0287: * @throws ElementNotFoundException If the anchor could not be found.
0288: */
0289: public HtmlAnchor getAnchorByHref(final String href)
0290: throws ElementNotFoundException {
0291: return (HtmlAnchor) getDocumentHtmlElement()
0292: .getOneHtmlElementByAttribute("a", "href", href);
0293: }
0294:
0295: /**
0296: * Return a list of all anchors contained in this page.
0297: * @return the list of {@link HtmlAnchor} in this page.
0298: */
0299: public List getAnchors() {
0300: return getDocumentHtmlElement().getHtmlElementsByTagNames(
0301: Collections.singletonList("a"));
0302: }
0303:
0304: /**
0305: * Return the first anchor that contains the specified text.
0306: * @param text The text to search for
0307: * @return The first anchor that was found.
0308: * @throws ElementNotFoundException If no anchors are found with the specified text
0309: */
0310: public HtmlAnchor getFirstAnchorByText(final String text)
0311: throws ElementNotFoundException {
0312: Assert.notNull("text", text);
0313:
0314: final Iterator iterator = getAnchors().iterator();
0315: while (iterator.hasNext()) {
0316: final HtmlAnchor anchor = (HtmlAnchor) iterator.next();
0317: if (text.equals(anchor.asText())) {
0318: return anchor;
0319: }
0320: }
0321: throw new ElementNotFoundException("a", "<text>", text);
0322: }
0323:
0324: /**
0325: * Return the first form that matches the specified name
0326: * @param name The name to search for
0327: * @return The first form.
0328: * @exception ElementNotFoundException If no forms match the specified result.
0329: */
0330: public HtmlForm getFormByName(final String name)
0331: throws ElementNotFoundException {
0332: final List forms = getDocumentHtmlElement()
0333: .getHtmlElementsByAttribute("form", "name", name);
0334: if (forms.size() == 0) {
0335: throw new ElementNotFoundException("form", "name", name);
0336: } else {
0337: return (HtmlForm) forms.get(0);
0338: }
0339: }
0340:
0341: /**
0342: * Return a list of all the forms in the page.
0343: * @return All the forms.
0344: */
0345: public List getForms() {
0346: return getDocumentHtmlElement().getHtmlElementsByTagNames(
0347: Arrays.asList(new String[] { "form" }));
0348: }
0349:
0350: /**
0351: * Given a relative url (ie /foo), return a fully qualified url based on
0352: * the url that was used to load this page
0353: *
0354: * @param relativeUrl The relative url
0355: * @return See above
0356: * @exception MalformedURLException If an error occurred when creating a URL object
0357: */
0358: public URL getFullyQualifiedUrl(String relativeUrl)
0359: throws MalformedURLException {
0360:
0361: final List baseElements = getDocumentHtmlElement()
0362: .getHtmlElementsByTagName("base");
0363: URL baseUrl;
0364: if (baseElements.isEmpty()) {
0365: baseUrl = getWebResponse().getUrl();
0366: } else {
0367: if (baseElements.size() > 1) {
0368: notifyIncorrectness("Multiple 'base' detected, only the first is used.");
0369: }
0370: final HtmlBase htmlBase = (HtmlBase) baseElements.get(0);
0371: boolean insideHead = false;
0372: for (DomNode parent = htmlBase.getParentDomNode(); parent != null; parent = parent
0373: .getParentDomNode()) {
0374: if (parent instanceof HtmlHead) {
0375: insideHead = true;
0376: break;
0377: }
0378: }
0379:
0380: //http://www.w3.org/TR/1999/REC-html401-19991224/struct/links.html#edef-BASE
0381: if (!insideHead) {
0382: notifyIncorrectness("Element 'base' must appear in <head>, it is ignored.");
0383: }
0384:
0385: final String href = htmlBase.getHrefAttribute();
0386: if (!insideHead || StringUtils.isEmpty(href)) {
0387: baseUrl = getWebResponse().getUrl();
0388: } else {
0389: try {
0390: baseUrl = new URL(href);
0391: } catch (final MalformedURLException e) {
0392: notifyIncorrectness("Invalid base url: \"" + href
0393: + "\", ignoring it");
0394: baseUrl = getWebResponse().getUrl();
0395: }
0396: }
0397: }
0398:
0399: // to handle http: and http:/ in FF (Bug 1714767)
0400: if (getWebClient().getBrowserVersion().isNetscape()) {
0401: boolean incorrectnessNotified = false;
0402: while (relativeUrl.startsWith("http:")
0403: && !relativeUrl.startsWith("http://")) {
0404: if (!incorrectnessNotified) {
0405: notifyIncorrectness("Incorrect URL \""
0406: + relativeUrl + "\" has been corrected");
0407: incorrectnessNotified = true;
0408: }
0409: relativeUrl = "http:/" + relativeUrl.substring(5);
0410: }
0411: }
0412:
0413: return WebClient.expandUrl(baseUrl, relativeUrl);
0414: }
0415:
0416: /**
0417: * Given a target attribute value, resolve the target using a base target for the page.
0418: *
0419: * @param elementTarget The target specified as an attribute of the element.
0420: * @return The resolved target to use for the element.
0421: */
0422: public String getResolvedTarget(final String elementTarget) {
0423: final List baseElements = getDocumentHtmlElement()
0424: .getHtmlElementsByTagNames(
0425: Collections.singletonList("base"));
0426: final String resolvedTarget;
0427: if (baseElements.isEmpty()) {
0428: resolvedTarget = elementTarget;
0429: } else if (elementTarget != null && elementTarget.length() > 0) {
0430: resolvedTarget = elementTarget;
0431: } else {
0432: final HtmlBase htmlBase = (HtmlBase) baseElements.get(0);
0433: resolvedTarget = htmlBase.getTargetAttribute();
0434: }
0435: return resolvedTarget;
0436: }
0437:
0438: /**
0439: * Return a list of ids (strings) that correspond to the tabbable elements
0440: * in this page. Return them in the same order specified in {@link #getTabbableElements}
0441: *
0442: * @return The list of id's
0443: */
0444: public List getTabbableElementIds() {
0445: final List list = new ArrayList(getTabbableElements());
0446: final int listSize = list.size();
0447:
0448: for (int i = 0; i < listSize; i++) {
0449: list.set(i, ((HtmlElement) list.get(i))
0450: .getAttributeValue("id"));
0451: }
0452:
0453: return Collections.unmodifiableList(list);
0454: }
0455:
0456: /**
0457: * Returns a list of all elements that are tabbable in the order that will
0458: * be used for tabbing.<p>
0459: *
0460: * The rules for determining tab order are as follows:
0461: * <ol>
0462: * <li>Those elements that support the tabindex attribute and assign a
0463: * positive value to it are navigated first. Navigation proceeds from the
0464: * element with the lowest tabindex value to the element with the highest
0465: * value. Values need not be sequential nor must they begin with any
0466: * particular value. Elements that have identical tabindex values should
0467: * be navigated in the order they appear in the character stream.
0468: * <li>Those elements that do not support the tabindex attribute or
0469: * support it and assign it a value of "0" are navigated next. These
0470: * elements are navigated in the order they appear in the character
0471: * stream.
0472: * <li>Elements that are disabled do not participate in the tabbing
0473: * order.
0474: * </ol>
0475: * Additionally, the value of tabindex must be within 0 and 32767. Any
0476: * values outside this range will be ignored.<p>
0477: *
0478: * The following elements support the <tt>tabindex</tt> attribute: A, AREA, BUTTON,
0479: * INPUT, OBJECT, SELECT, and TEXTAREA.<p>
0480: *
0481: * @return A list containing all the tabbable elements in proper tab order.
0482: */
0483: public List getTabbableElements() {
0484: final List tags = Arrays.asList(new Object[] { "a", "area",
0485: "button", "input", "object", "select", "textarea" });
0486: final List tabbableElements = new ArrayList();
0487: final Iterator iterator = getAllHtmlChildElements();
0488: while (iterator.hasNext()) {
0489: final HtmlElement element = (HtmlElement) iterator.next();
0490: final String tagName = element.getTagName();
0491: if (tags.contains(tagName)) {
0492: final boolean disabled = element
0493: .isAttributeDefined("disabled");
0494: if (!disabled
0495: && element.getTabIndex() != HtmlElement.TAB_INDEX_OUT_OF_BOUNDS) {
0496: tabbableElements.add(element);
0497: }
0498: }
0499: }
0500: Collections.sort(tabbableElements, createTabOrderComparator());
0501: return Collections.unmodifiableList(tabbableElements);
0502: }
0503:
0504: private Comparator createTabOrderComparator() {
0505: return new Comparator() {
0506: public int compare(final Object object1,
0507: final Object object2) {
0508:
0509: final HtmlElement element1 = (HtmlElement) object1;
0510: final HtmlElement element2 = (HtmlElement) object2;
0511:
0512: final Short i1 = element1.getTabIndex();
0513: final Short i2 = element2.getTabIndex();
0514:
0515: final short index1;
0516: if (i1 != null) {
0517: index1 = i1.shortValue();
0518: } else {
0519: index1 = -1;
0520: }
0521:
0522: final short index2;
0523: if (i2 != null) {
0524: index2 = i2.shortValue();
0525: } else {
0526: index2 = -1;
0527: }
0528:
0529: final int result;
0530: if (index1 > 0 && index2 > 0) {
0531: result = index1 - index2;
0532: } else if (index1 > 0) {
0533: result = -1;
0534: } else if (index2 > 0) {
0535: result = +1;
0536: } else if (index1 == index2) {
0537: result = 0;
0538: } else {
0539: result = index2 - index1;
0540: }
0541:
0542: return result;
0543: }
0544: };
0545: }
0546:
0547: /**
0548: * Returns the HTML element that is assigned to the specified access key. An
0549: * access key (aka mnemonic key) is used for keyboard navigation of the
0550: * page.<p>
0551: *
0552: * Only the following HTML elements may have <tt>accesskey</tt>s defined: A, AREA,
0553: * BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA.
0554: *
0555: * @param accessKey The key to look for
0556: * @return The HTML element that is assigned to the specified key or null
0557: * if no elements can be found that match the specified key.
0558: */
0559: public HtmlElement getHtmlElementByAccessKey(final char accessKey) {
0560: final List elements = getHtmlElementsByAccessKey(accessKey);
0561: if (elements.isEmpty()) {
0562: return null;
0563: } else {
0564: return (HtmlElement) elements.get(0);
0565: }
0566: }
0567:
0568: /**
0569: * Returns all the HTML elements that are assigned to the specified access key. An
0570: * access key (aka mnemonic key) is used for keyboard navigation of the
0571: * page.<p>
0572: *
0573: * The HTML specification seems to indicate that one accesskey cannot be used
0574: * for multiple elements however Internet Explorer does seem to support this.
0575: * It's worth noting that Mozilla does not support multiple elements with one
0576: * access key so you are making your html browser specific if you rely on this
0577: * feature.<p>
0578: *
0579: * Only the following html elements may have <tt>accesskey</tt>s defined: A, AREA,
0580: * BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA.
0581: *
0582: * @param accessKey The key to look for
0583: * @return A list of HTML elements that are assigned to the specified accesskey.
0584: */
0585: public List getHtmlElementsByAccessKey(final char accessKey) {
0586: final List elements = new ArrayList();
0587:
0588: final String searchString = ("" + accessKey).toLowerCase();
0589: final List acceptableTagNames = Arrays.asList(new Object[] {
0590: "a", "area", "button", "input", "label", "legend",
0591: "textarea" });
0592:
0593: final Iterator iterator = getAllHtmlChildElements();
0594: while (iterator.hasNext()) {
0595: final HtmlElement element = (HtmlElement) iterator.next();
0596: if (acceptableTagNames.contains(element.getTagName())) {
0597: final String accessKeyAttribute = element
0598: .getAttributeValue("accesskey");
0599: if (searchString.equalsIgnoreCase(accessKeyAttribute)) {
0600: elements.add(element);
0601: }
0602: }
0603: }
0604:
0605: return elements;
0606: }
0607:
0608: /**
0609: * Many html elements are "tabbable" and can have a "tabindex" attribute
0610: * that determines the order in which the components are navigated when
0611: * pressing the tab key. To ensure good usability for keyboard navigation,
0612: * all tabbable elements should have the tabindex attribute set.<p>
0613: *
0614: * Assert that all tabbable elements have a valid value set for "tabindex".
0615: * If they don't then throw an exception as per {@link
0616: * WebClient#assertionFailed(String)}
0617: *
0618: * @deprecated
0619: * @see WebAssert#assertAllTabIndexAttributesSet(HtmlPage)
0620: */
0621: public void assertAllTabIndexAttributesSet() {
0622: WebAssert.assertAllTabIndexAttributesSet(this );
0623: }
0624:
0625: /**
0626: * Many html components can have an "accesskey" attribute which defines a
0627: * hot key for keyboard navigation. Assert that all access keys (mnemonics)
0628: * in this page are unique. If they aren't then throw an exception as per
0629: * {@link WebClient#assertionFailed(String)}
0630: *
0631: * @deprecated
0632: * @see WebAssert#assertAllAccessKeyAttributesUnique(HtmlPage)
0633: */
0634: public void assertAllAccessKeyAttributesUnique() {
0635: WebAssert.assertAllAccessKeyAttributesUnique(this );
0636: }
0637:
0638: /**
0639: * Each html element can have an id attribute and by definition, all ids
0640: * must be unique within the document. <p>
0641: *
0642: * Assert that all ids in this page are unique. If they aren't then throw
0643: * an exception as per {@link WebClient#assertionFailed(String)}
0644: *
0645: * @deprecated
0646: * @see WebAssert#assertAllIdAttributesUnique(HtmlPage)
0647: */
0648: public void assertAllIdAttributesUnique() {
0649: WebAssert.assertAllIdAttributesUnique(this );
0650: }
0651:
0652: /**
0653: * Execute the specified javascript within the page.
0654: * The usage would be similar to what can be achieved to execute javascript in the current page
0655: * by entering a "javascript:...some js code..." in the url field of a "normal" browser.
0656: * <p>
0657: * <b>Note: </b> the provided code won't be executed if JavaScript has been disabled on the WebClient
0658: * (see {@link WebClient#isJavaScriptEnabled()}.
0659: * @param sourceCode The javascript code to execute.
0660: * @return A ScriptResult which will contain both the current page (which may be different than
0661: * the previous page) and a javascript result object.
0662: */
0663: public ScriptResult executeJavaScript(final String sourceCode) {
0664:
0665: return executeJavaScriptIfPossible(sourceCode,
0666: "injected script", 1);
0667: }
0668:
0669: /**
0670: * <p>
0671: * Execute the specified javascript if a javascript engine was successfully
0672: * instantiated. If this javascript causes the current page to be reloaded
0673: * (through location="" or form.submit()) then return the new page. Otherwise
0674: * return the current page.
0675: * </p>
0676: * <p><b>Please note:</b> Although this method is public, it is not intended for
0677: * general execution of javascript. Users of HtmlUnit should interact with the pages
0678: * as a user would by clicking on buttons or links and having the javascript event
0679: * handlers execute as needed..
0680: * </p>
0681: *
0682: * @param sourceCode The javascript code to execute.
0683: * @param sourceName The name for this chunk of code. This name will be displayed
0684: * in any error messages.
0685: * @param htmlElement The html element for which this script is being executed.
0686: * This element will be the context during the javascript execution. If null,
0687: * the context will default to the window.
0688: * @return A ScriptResult which will contain both the current page (which may be different than
0689: * the previous page and a javascript result object.
0690: * @deprecated use {@link #executeJavaScript(String)} instead
0691: */
0692: public ScriptResult executeJavaScriptIfPossible(
0693: final String sourceCode, final String sourceName,
0694: final HtmlElement htmlElement) {
0695:
0696: return executeJavaScriptIfPossible(sourceCode, sourceName, 1);
0697: }
0698:
0699: /**
0700: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0701: * <p>
0702: * Execute the specified javascript if a javascript engine was successfully
0703: * instantiated. If this javascript causes the current page to be reloaded
0704: * (through location="" or form.submit()) then return the new page. Otherwise
0705: * return the current page.
0706: * </p>
0707: * <p><b>Please note:</b> Although this method is public, it is not intended for
0708: * general execution of javascript. Users of HtmlUnit should interact with the pages
0709: * as a user would by clicking on buttons or links and having the javascript event
0710: * handlers execute as needed..
0711: * </p>
0712: *
0713: * @param sourceCode The javascript code to execute.
0714: * @param sourceName The name for this chunk of code. This name will be displayed
0715: * in any error messages.
0716: * @param startLine the line at which the script source starts
0717: * @return A ScriptResult which will contain both the current page (which may be different than
0718: * the previous page and a javascript result object.
0719: */
0720: public ScriptResult executeJavaScriptIfPossible(String sourceCode,
0721: final String sourceName, final int startLine) {
0722:
0723: if (!getWebClient().isJavaScriptEnabled()) {
0724: return new ScriptResult(null, this );
0725: }
0726:
0727: final String prefix = "javascript:";
0728: final int prefixLength = prefix.length();
0729:
0730: if (sourceCode.length() > prefixLength
0731: && sourceCode.substring(0, prefixLength)
0732: .equalsIgnoreCase(prefix)) {
0733: sourceCode = sourceCode.substring(prefixLength);
0734: }
0735:
0736: final WebWindow window = getEnclosingWindow();
0737: getWebClient().pushClearFirstWindow();
0738: final Object result = getWebClient().getJavaScriptEngine()
0739: .execute(this , sourceCode, sourceName, startLine);
0740:
0741: WebWindow firstWindow = getWebClient().popFirstWindow();
0742: if (firstWindow == null) {
0743: firstWindow = window;
0744: }
0745:
0746: return new ScriptResult(result, firstWindow.getEnclosedPage());
0747: }
0748:
0749: /**
0750: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0751: *
0752: * Execute a Function in the given context.
0753: *
0754: * @param function The javascript Function to call.
0755: * @param thisObject The "this" object to be used during invocation.
0756: * @param args The arguments to pass into the call.
0757: * @param htmlElementScope The html element for which this script is being executed.
0758: * This element will be the context during the javascript execution. If null,
0759: * the context will default to the page.
0760: * @return A ScriptResult which will contain both the current page (which may be different than
0761: * the previous page and a javascript result object.
0762: */
0763: public ScriptResult executeJavaScriptFunctionIfPossible(
0764: final Function function, final Scriptable this Object,
0765: final Object[] args, final DomNode htmlElementScope) {
0766:
0767: final WebWindow window = getEnclosingWindow();
0768: getWebClient().pushClearFirstWindow();
0769:
0770: if (!getWebClient().isJavaScriptEnabled()) {
0771: return new ScriptResult(null, this );
0772: }
0773:
0774: final JavaScriptEngine engine = getWebClient()
0775: .getJavaScriptEngine();
0776: final Object result = engine.callFunction(this , function,
0777: this Object, args, htmlElementScope);
0778:
0779: WebWindow firstWindow = getWebClient().popFirstWindow();
0780: if (firstWindow == null) {
0781: firstWindow = window;
0782: }
0783: return new ScriptResult(result, firstWindow.getEnclosedPage());
0784: }
0785:
0786: /**
0787: * Return the log object for this element.
0788: * @return The log object for this element.
0789: */
0790: protected Log getJsLog() {
0791: return javascriptLog_;
0792: }
0793:
0794: /**
0795: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0796: *
0797: * @param srcAttribute The source attribute from the script tag.
0798: * @param charset The charset attribute from the script tag.
0799: */
0800: void loadExternalJavaScriptFile(final String srcAttribute,
0801: final String charset) {
0802: if (getWebClient().isJavaScriptEnabled()) {
0803: final URL scriptURL;
0804: try {
0805: scriptURL = getFullyQualifiedUrl(srcAttribute);
0806: if (scriptURL.getProtocol().equals("javascript")) {
0807: getLog().info(
0808: "Ignoring script src [" + srcAttribute
0809: + "]");
0810: return;
0811: }
0812: } catch (final MalformedURLException e) {
0813: getLog().error(
0814: "Unable to build url for script src tag ["
0815: + srcAttribute + "]");
0816: if (getWebClient().isThrowExceptionOnScriptError()) {
0817: throw new ScriptException(this , e);
0818: }
0819: return;
0820: }
0821:
0822: final Script script = loadJavaScriptFromUrl(scriptURL,
0823: charset);
0824: if (script != null) {
0825: getWebClient().getJavaScriptEngine().execute(this ,
0826: script);
0827: }
0828: }
0829: }
0830:
0831: /**
0832: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
0833: *
0834: * Return true if a script with the specified type and language attributes
0835: * is actually JavaScript.
0836: * According to <a href="http://www.w3.org/TR/REC-html40/types.html#h-6.7">W3C recommendation</a>
0837: * are content types case insensitive.
0838: * @param typeAttribute The type attribute specified in the script tag.
0839: * @param languageAttribute The language attribute specified in the script tag.
0840: * @return true if the script is javascript
0841: */
0842: public static boolean isJavaScript(final String typeAttribute,
0843: final String languageAttribute) {
0844: // Unless otherwise specified, we have to assume that any script is javascript
0845: final boolean isJavaScript;
0846: if (languageAttribute != null
0847: && languageAttribute.length() != 0) {
0848: isJavaScript = TextUtil.startsWithIgnoreCase(
0849: languageAttribute, "javascript");
0850: } else if (typeAttribute != null && typeAttribute.length() != 0) {
0851: isJavaScript = typeAttribute
0852: .equalsIgnoreCase("text/javascript");
0853: } else {
0854: isJavaScript = true;
0855: }
0856:
0857: return isJavaScript;
0858: }
0859:
0860: /**
0861: * Loads JavaScript from the specified URL. This method may return <tt>null</tt> if
0862: * there is a problem loading the code from the specified URL.
0863: *
0864: * @param url the url of the script
0865: * @param charset the charset to use to read the text
0866: * @return the content of the file
0867: */
0868: private Script loadJavaScriptFromUrl(final URL url,
0869: final String charset) {
0870: String scriptEncoding = charset;
0871: getPageEncoding();
0872:
0873: WebResponse webResponse;
0874: try {
0875: final WebRequestSettings requestSettings = new WebRequestSettings(
0876: url);
0877: webResponse = getWebClient().loadWebResponse(
0878: requestSettings);
0879: } catch (final IOException e) {
0880: getLog().error(
0881: "Error loading JavaScript from ["
0882: + url.toExternalForm() + "].", e);
0883: return null;
0884: }
0885:
0886: final JavaScriptEngine javaScriptEngine = getWebClient()
0887: .getJavaScriptEngine();
0888: final Script cachedScript = javaScriptEngine
0889: .getCachedScript(webResponse);
0890: if (cachedScript != null) {
0891: return cachedScript;
0892: }
0893:
0894: getWebClient().printContentIfNecessary(webResponse);
0895: getWebClient().throwFailingHttpStatusCodeExceptionIfNecessary(
0896: webResponse);
0897:
0898: final int statusCode = webResponse.getStatusCode();
0899: final boolean successful = (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES);
0900: if (!successful) {
0901: return null;
0902: }
0903:
0904: final String contentType = webResponse.getContentType();
0905: if (!contentType.equalsIgnoreCase("text/javascript")
0906: && !contentType
0907: .equalsIgnoreCase("application/x-javascript")) {
0908: getLog().warn(
0909: "Expected content type of 'text/javascript' or 'application/x-javascript' for "
0910: + "remotely loaded JavaScript element at '"
0911: + url + "', " + "but got '" + contentType
0912: + "'.");
0913: }
0914:
0915: if (StringUtils.isEmpty(scriptEncoding)) {
0916: final String contentCharset = webResponse
0917: .getContentCharSet();
0918: if (!contentCharset.equals(TextUtil.DEFAULT_CHARSET)) {
0919: scriptEncoding = contentCharset;
0920: } else if (!originalCharset_
0921: .equals(TextUtil.DEFAULT_CHARSET)) {
0922: scriptEncoding = originalCharset_;
0923: } else {
0924: scriptEncoding = TextUtil.DEFAULT_CHARSET;
0925: }
0926: }
0927:
0928: final byte[] data = webResponse.getResponseBody();
0929: final String scriptCode = EncodingUtil.getString(data, 0,
0930: data.length, scriptEncoding);
0931:
0932: final Script script = javaScriptEngine.compile(this ,
0933: scriptCode, url.toExternalForm(), 1);
0934: javaScriptEngine.cacheScript(webResponse, script);
0935: return script;
0936: }
0937:
0938: /**
0939: * Return the title of this page or an empty string if the title wasn't specified.
0940: *
0941: * @return the title of this page or an empty string if the title wasn't specified.
0942: */
0943: public String getTitleText() {
0944: final HtmlTitle titleElement = getTitleElement();
0945: if (titleElement != null) {
0946: return titleElement.asText();
0947: }
0948: return "";
0949: }
0950:
0951: /**
0952: * Set the text for the title of this page. If there is not a title element
0953: * on this page, then one has to be generated.
0954: * @param message The new text
0955: */
0956: public void setTitleText(final String message) {
0957: HtmlTitle titleElement = getTitleElement();
0958: if (titleElement == null) {
0959: getLog().debug("No title element, creating one");
0960: final HtmlHead head = (HtmlHead) getFirstChildElement(
0961: getDocumentHtmlElement(), HtmlHead.class);
0962: if (head == null) {
0963: // perhaps should we create head too?
0964: throw new IllegalStateException(
0965: "Headelement was not defined for this page");
0966: }
0967: titleElement = new HtmlTitle(null, HtmlTitle.TAG_NAME,
0968: this , Collections.EMPTY_MAP);
0969: if (head.getFirstDomChild() != null) {
0970: head.getFirstDomChild().insertBefore(titleElement);
0971: } else {
0972: head.appendDomChild(titleElement);
0973: }
0974: }
0975:
0976: titleElement.setNodeValue(message);
0977: }
0978:
0979: /**
0980: * Get the first child of startElement that is an instance of the given class.
0981: * @param startElement The parent element
0982: * @param clazz The class to search for.
0983: * @return <code>null</code> if no child found
0984: */
0985: private HtmlElement getFirstChildElement(
0986: final HtmlElement startElement, final Class clazz) {
0987: final Iterator iterator = startElement
0988: .getChildElementsIterator();
0989: while (iterator.hasNext()) {
0990: final HtmlElement element = (HtmlElement) iterator.next();
0991: if (clazz.isInstance(element)) {
0992: return element;
0993: }
0994: }
0995:
0996: return null;
0997: }
0998:
0999: /**
1000: * Get the title element for this page. Returns null if one is not found.
1001: *
1002: * @return the title element for this page or null if this is not one.
1003: */
1004: private HtmlTitle getTitleElement() {
1005: final HtmlHead head = (HtmlHead) getFirstChildElement(
1006: getDocumentHtmlElement(), HtmlHead.class);
1007: if (head != null) {
1008: return (HtmlTitle) getFirstChildElement(head,
1009: HtmlTitle.class);
1010: }
1011:
1012: return null;
1013: }
1014:
1015: /**
1016: * Look for and execute any appropriate event handlers. Look for body
1017: * and frame tags.
1018: * @param eventType either {@link Event#TYPE_LOAD}, {@link Event#TYPE_UNLOAD}, or {@link Event#TYPE_BEFORE_UNLOAD}.
1019: * @return true if user accepted onbeforeunload (not relevant to other events).
1020: */
1021: private boolean executeEventHandlersIfNeeded(final String eventType) {
1022: if (!getWebClient().isJavaScriptEnabled()) {
1023: return true;
1024: }
1025:
1026: final Window jsWindow = (Window) getEnclosingWindow()
1027: .getScriptObject();
1028: if (jsWindow != null) {
1029: final HtmlElement element = getDocumentHtmlElement();
1030: final Event event = new Event(element, eventType);
1031: element.fireEvent(event);
1032: if (!isOnbeforeunloadAccepted(this , event)) {
1033: return false;
1034: }
1035: }
1036:
1037: // the event of the contained frames or iframe tags
1038: final List frames = getDocumentHtmlElement()
1039: .getHtmlElementsByTagNames(
1040: Arrays
1041: .asList(new String[] { "frame",
1042: "iframe" }));
1043: for (final Iterator iter = frames.iterator(); iter.hasNext();) {
1044: final BaseFrame frame = (BaseFrame) iter.next();
1045: final Function frameTagEventHandler = frame
1046: .getEventHandler("on" + eventType);
1047: if (frameTagEventHandler != null) {
1048: getLog().debug(
1049: "Executing on" + eventType + " handler for "
1050: + frame);
1051: final Event event = new Event(frame, eventType);
1052: ((Node) frame.getScriptObject()).executeEvent(event);
1053: if (!isOnbeforeunloadAccepted(frame.getPage(), event)) {
1054: return false;
1055: }
1056: }
1057: }
1058: return true;
1059: }
1060:
1061: private boolean isOnbeforeunloadAccepted(final HtmlPage page,
1062: final Event event) {
1063: if (event.jsxGet_type().equals(Event.TYPE_BEFORE_UNLOAD)
1064: && event.jsxGet_returnValue() != null) {
1065: final OnbeforeunloadHandler handler = getWebClient()
1066: .getOnbeforeunloadHandler();
1067: if (handler == null) {
1068: getLog()
1069: .warn(
1070: "document.onbeforeunload() returned a string in event.returnValue,"
1071: + " but no onbeforeunload handler installed.");
1072: } else {
1073: final String message = Context.toString(event
1074: .jsxGet_returnValue());
1075: return handler.handleEvent(page, message);
1076: }
1077: }
1078: return true;
1079: }
1080:
1081: /**
1082: * If a refresh has been specified either through a meta tag or an http
1083: * response header, then perform that refresh.
1084: * @throws IOException if an IO problem occurs
1085: */
1086: private void executeRefreshIfNeeded() throws IOException {
1087: // If this page is not in a frame then a refresh has already happened,
1088: // most likely through the javascript onload handler, so we don't do a
1089: // second refresh.
1090: final WebWindow window = getEnclosingWindow();
1091: if (window == null) {
1092: return;
1093: }
1094:
1095: final String refreshString = getRefreshStringOrNull();
1096: if (refreshString == null || refreshString.length() == 0) {
1097: return;
1098: }
1099:
1100: final int time;
1101: final URL url;
1102:
1103: int index = refreshString.indexOf(";");
1104: final boolean timeOnly = (index == -1);
1105:
1106: if (timeOnly) {
1107: // Format: <meta http-equiv='refresh' content='10'>
1108: try {
1109: time = Integer.parseInt(refreshString);
1110: } catch (final NumberFormatException e) {
1111: getLog().error(
1112: "Malformed refresh string (no ';' but not a number): "
1113: + refreshString, e);
1114: return;
1115: }
1116: url = getWebResponse().getUrl();
1117: } else {
1118: // Format: <meta http-equiv='refresh' content='10;url=http://www.blah.com'>
1119: try {
1120: time = Integer.parseInt(refreshString.substring(0,
1121: index).trim());
1122: } catch (final NumberFormatException e) {
1123: getLog().error(
1124: "Malformed refresh string (no valid number before ';') "
1125: + refreshString, e);
1126: return;
1127: }
1128: index = refreshString.indexOf("URL=", index);
1129: if (index == -1) {
1130: index = refreshString.indexOf("url=", index);
1131: }
1132: if (index == -1) {
1133: getLog().error(
1134: "Malformed refresh string (found ';' but no 'url='): "
1135: + refreshString);
1136: return;
1137: }
1138: final StringBuffer buffer = new StringBuffer(refreshString
1139: .substring(index + 4));
1140: if (buffer.toString().trim().length() == 0) {
1141: //content='10; URL=' is treated as content='10'
1142: url = getWebResponse().getUrl();
1143: } else {
1144: if (buffer.charAt(0) == '"' || buffer.charAt(0) == 0x27) {
1145: buffer.deleteCharAt(0);
1146: }
1147: if (buffer.charAt(buffer.length() - 1) == '"'
1148: || buffer.charAt(buffer.length() - 1) == 0x27) {
1149: buffer.deleteCharAt(buffer.length() - 1);
1150: }
1151: final String urlString = buffer.toString();
1152: try {
1153: url = getFullyQualifiedUrl(urlString);
1154: } catch (final MalformedURLException e) {
1155: getLog().error(
1156: "Malformed url in refresh string: "
1157: + refreshString, e);
1158: throw e;
1159: }
1160: }
1161: }
1162:
1163: getWebClient().getRefreshHandler().handleRefresh(this , url,
1164: time);
1165: }
1166:
1167: /**
1168: * Return an auto-refresh string if specified. This will look in both the meta
1169: * tags (taking care of <noscript> if any) and inside the http response headers.
1170: * @return the auto-refresh string.
1171: */
1172: private String getRefreshStringOrNull() {
1173: final Iterator iterator = getMetaTags("refresh").iterator();
1174: final boolean javaScriptEnabled = getWebClient()
1175: .isJavaScriptEnabled();
1176: while (iterator.hasNext()) {
1177: final HtmlMeta meta = (HtmlMeta) iterator.next();
1178: if ((!javaScriptEnabled || getFirstParent(meta,
1179: HtmlNoScript.TAG_NAME) == null)) {
1180: return meta.getContentAttribute();
1181: }
1182: }
1183: return getWebResponse().getResponseHeaderValue("Refresh");
1184: }
1185:
1186: /**
1187: * Executes any deferred scripts, if necessary.
1188: */
1189: private void executeDeferredScriptsIfNeeded() {
1190: if (!getWebClient().isJavaScriptEnabled()) {
1191: return;
1192: }
1193: if (!getWebClient().getBrowserVersion().isIE()) {
1194: return;
1195: }
1196: final List scripts = getDocumentHtmlElement()
1197: .getHtmlElementsByTagName("script");
1198: for (final Iterator i = scripts.iterator(); i.hasNext();) {
1199: final HtmlScript script = (HtmlScript) i.next();
1200: final String defer = script.getDeferAttribute();
1201: if (defer != HtmlElement.ATTRIBUTE_NOT_DEFINED) {
1202: script.executeScriptIfNeeded(true);
1203: }
1204: }
1205: }
1206:
1207: /**
1208: * Gets the first parent with the given node name
1209: * @param node the node to start with
1210: * @param nodeName the name of the search node
1211: * @return <code>null</code> if no parent found with this name
1212: */
1213: private DomNode getFirstParent(final DomNode node,
1214: final String nodeName) {
1215: DomNode parent = node.getParentDomNode();
1216: while (parent != null) {
1217: if (parent.getNodeName().equals(nodeName)) {
1218: return parent;
1219: }
1220: parent = parent.getParentDomNode();
1221: }
1222: return null;
1223: }
1224:
1225: /**
1226: * Deregister frames that are no longer in use.
1227: */
1228: public void deregisterFramesIfNeeded() {
1229: for (final Iterator iter = getFrames().iterator(); iter
1230: .hasNext();) {
1231: final WebWindow window = (WebWindow) iter.next();
1232: getWebClient().deregisterWebWindow(window);
1233: if (window.getEnclosedPage() instanceof HtmlPage) {
1234: final HtmlPage page = (HtmlPage) window
1235: .getEnclosedPage();
1236: if (page != null) {
1237: // seems quite silly, but for instance if the src attribute of an iframe is not
1238: // set, the error only occurs when leaving the page
1239: page.deregisterFramesIfNeeded();
1240: }
1241: }
1242: }
1243: }
1244:
1245: /**
1246: * Return a list containing all the frames (from frame and iframe tags) in this page.
1247: * @return a list of {@link FrameWindow}
1248: */
1249: public List getFrames() {
1250: final List list = new ArrayList();
1251: final WebWindow enclosingWindow = getEnclosingWindow();
1252: for (final Iterator iter = getWebClient().getWebWindows()
1253: .iterator(); iter.hasNext();) {
1254: final WebWindow window = (WebWindow) iter.next();
1255: // quite strange but for a TopLevelWindow parent == self
1256: if (enclosingWindow == window.getParentWindow()
1257: && enclosingWindow != window) {
1258: list.add(window);
1259: }
1260: }
1261: return list;
1262: }
1263:
1264: /**
1265: * Returns the first frame contained in this page with the specified name.
1266: * @param name The name to search for
1267: * @return The first frame found.
1268: * @exception ElementNotFoundException If no frame exist in this page with the specified name.
1269: */
1270: public FrameWindow getFrameByName(final String name)
1271: throws ElementNotFoundException {
1272: final List frames = getFrames();
1273: for (final Iterator iter = frames.iterator(); iter.hasNext();) {
1274: final FrameWindow frame = (FrameWindow) iter.next();
1275: if (frame.getName().equals(name)) {
1276: return frame;
1277: }
1278: }
1279:
1280: throw new ElementNotFoundException("frame or iframe", "name",
1281: name);
1282: }
1283:
1284: /**
1285: * Simulate pressing an access key. This may change the focus, may click buttons and may invoke
1286: * javascript.
1287: *
1288: * @param accessKey The key that will be pressed.
1289: * @return The element that has the focus after pressing this access key or null if no element
1290: * has the focus.
1291: * @throws IOException If an io error occurs during the processing of this access key. This
1292: * would only happen if the access key triggered a button which in turn caused a page load.
1293: */
1294: public HtmlElement pressAccessKey(final char accessKey)
1295: throws IOException {
1296: final HtmlElement element = getHtmlElementByAccessKey(accessKey);
1297: if (element != null) {
1298: element.focus();
1299: final Page newPage;
1300: if (element instanceof HtmlAnchor) {
1301: newPage = ((HtmlAnchor) element).click();
1302: } else if (element instanceof HtmlArea) {
1303: newPage = ((HtmlArea) element).click();
1304: } else if (element instanceof HtmlButton) {
1305: newPage = ((HtmlButton) element).click();
1306: } else if (element instanceof HtmlInput) {
1307: newPage = ((HtmlInput) element).click();
1308: } else if (element instanceof HtmlLabel) {
1309: newPage = ((HtmlLabel) element).click();
1310: } else if (element instanceof HtmlLegend) {
1311: newPage = ((HtmlLegend) element).click();
1312: } else if (element instanceof HtmlTextArea) {
1313: newPage = ((HtmlTextArea) element).click();
1314: } else {
1315: newPage = this ;
1316: }
1317:
1318: if (newPage != this && getElementWithFocus() == element) {
1319: // The page was reloaded therefore no element on this page will have the focus.
1320: getElementWithFocus().blur();
1321: }
1322: }
1323:
1324: return getElementWithFocus();
1325: }
1326:
1327: /**
1328: * Move the focus to the next element in the tab order. To determine the specified tab
1329: * order, refer to {@link HtmlPage#getTabbableElements()}
1330: *
1331: * @return The element that has focus after calling this method.
1332: */
1333: public HtmlElement tabToNextElement() {
1334: final List elements = getTabbableElements();
1335: if (elements.isEmpty()) {
1336: moveFocusToElement(null);
1337: return null;
1338: }
1339:
1340: final HtmlElement elementToGiveFocus;
1341: final HtmlElement elementWithFocus = getElementWithFocus();
1342: if (elementWithFocus == null) {
1343: elementToGiveFocus = (HtmlElement) elements.get(0);
1344: } else {
1345: final int index = elements.indexOf(elementWithFocus);
1346: if (index == -1) {
1347: // The element with focus isn't on this page
1348: elementToGiveFocus = (HtmlElement) elements.get(0);
1349: } else {
1350: if (index == elements.size() - 1) {
1351: elementToGiveFocus = (HtmlElement) elements.get(0);
1352: } else {
1353: elementToGiveFocus = (HtmlElement) elements
1354: .get(index + 1);
1355: }
1356: }
1357: }
1358:
1359: moveFocusToElement(elementToGiveFocus);
1360: return elementToGiveFocus;
1361: }
1362:
1363: /**
1364: * Move the focus to the previous element in the tab order. To determine the specified tab
1365: * order, refer to {@link HtmlPage#getTabbableElements()}
1366: *
1367: * @return The element that has focus after calling this method.
1368: */
1369: public HtmlElement tabToPreviousElement() {
1370: final List elements = getTabbableElements();
1371: if (elements.isEmpty()) {
1372: moveFocusToElement(null);
1373: return null;
1374: }
1375:
1376: final HtmlElement elementToGiveFocus;
1377: final HtmlElement elementWithFocus = getElementWithFocus();
1378: if (elementWithFocus == null) {
1379: elementToGiveFocus = (HtmlElement) elements.get(elements
1380: .size() - 1);
1381: } else {
1382: final int index = elements.indexOf(elementWithFocus);
1383: if (index == -1) {
1384: // The element with focus isn't on this page
1385: elementToGiveFocus = (HtmlElement) elements
1386: .get(elements.size() - 1);
1387: } else {
1388: if (index == 0) {
1389: elementToGiveFocus = (HtmlElement) elements
1390: .get(elements.size() - 1);
1391: } else {
1392: elementToGiveFocus = (HtmlElement) elements
1393: .get(index - 1);
1394: }
1395: }
1396: }
1397:
1398: moveFocusToElement(elementToGiveFocus);
1399: return elementToGiveFocus;
1400: }
1401:
1402: /**
1403: * Returns the HTML element with the specified ID. If more than one element
1404: * has this ID (not allowed by the HTML spec), then this method returns the
1405: * first one.
1406: *
1407: * @param id the ID value to search by
1408: * @return the HTML element with the specified ID
1409: * @throws ElementNotFoundException if no element was found that matches the id
1410: */
1411: public HtmlElement getHtmlElementById(final String id)
1412: throws ElementNotFoundException {
1413: final List elements = (List) idMap_.get(id);
1414: if (elements != null) {
1415: return (HtmlElement) elements.get(0);
1416: }
1417: throw new ElementNotFoundException("*", "id", id);
1418: }
1419:
1420: /**
1421: * Returns the HTML elements with the specified name attribute. If there are no elements
1422: * with the specified name, this method returns an empty list. Please note that
1423: * the lists returned by this method are immutable.
1424: *
1425: * @param name the name value to search by
1426: * @return the HTML elements with the specified name attribute
1427: */
1428: public List getHtmlElementsByName(final String name) {
1429: final List list = (List) nameMap_.get(name);
1430: if (list != null) {
1431: return Collections.unmodifiableList(list);
1432: } else {
1433: return Collections.EMPTY_LIST;
1434: }
1435: }
1436:
1437: /**
1438: * Returns the HTML elements with the specified string for their name or ID. If there are
1439: * no elements with the specified name or ID, this method returns an empty list. Please note
1440: * that lists returned by this method are immutable.
1441: *
1442: * @param idAndOrName the value to search for
1443: * @return the HTML elements with the specified string for their name or ID
1444: */
1445: public List getHtmlElementsByIdAndOrName(final String idAndOrName) {
1446: final List list1 = (List) idMap_.get(idAndOrName);
1447: final List list2 = (List) nameMap_.get(idAndOrName);
1448: final List list = new ArrayList();
1449: if (list1 != null) {
1450: list.addAll(list1);
1451: }
1452: if (list2 != null) {
1453: list.addAll(list2);
1454: }
1455: return Collections.unmodifiableList(list);
1456: }
1457:
1458: /**
1459: * Adds an element to the ID and name maps, if necessary.
1460: * @param element the element to be added to the ID and name maps
1461: */
1462: void addMappedElement(final HtmlElement element) {
1463: addMappedElement(element, false);
1464: }
1465:
1466: /**
1467: * Adds an element to the ID and name maps, if necessary.
1468: * @param element the element to be added to the ID and name maps
1469: * @param recurse indicates if children must be added too
1470: */
1471: void addMappedElement(final HtmlElement element,
1472: final boolean recurse) {
1473: if (isDescendant(element)) {
1474: addElement(idMap_, element, "id", recurse);
1475: addElement(nameMap_, element, "name", recurse);
1476: }
1477: }
1478:
1479: /**
1480: * Checks whether the specified element is descendant of this HtmlPage or not.
1481: */
1482: private boolean isDescendant(final HtmlElement element) {
1483: for (DomNode parent = element; parent != null; parent = parent
1484: .getParentDomNode()) {
1485: if (parent == this ) {
1486: return true;
1487: }
1488: }
1489: return false;
1490: }
1491:
1492: private void addElement(final Map map, final HtmlElement element,
1493: final String attribute, final boolean recurse) {
1494: final String value = element.getAttributeValue(attribute);
1495: if (!StringUtils.isEmpty(value)) {
1496: List elements = (List) map.get(value);
1497: if (elements == null) {
1498: elements = new Vector();
1499: elements.add(element);
1500: map.put(value, elements);
1501: } else if (!elements.contains(element)) {
1502: elements.add(element);
1503: }
1504: }
1505: if (recurse) {
1506: for (final Iterator i = element.getChildElementsIterator(); i
1507: .hasNext();) {
1508: final HtmlElement child = (HtmlElement) i.next();
1509: addElement(map, child, attribute, true);
1510: }
1511: }
1512: }
1513:
1514: /**
1515: * Removes an element from the ID and name maps, if necessary.
1516: * @param element the element to be removed from the ID and name maps
1517: */
1518: void removeMappedElement(final HtmlElement element) {
1519: removeMappedElement(element, false, false);
1520: }
1521:
1522: /**
1523: * Removes an element and optionally its children from the ID and name maps, if necessary.
1524: * @param element the element to be removed from the ID and name maps
1525: * @param recurse indicates if children must be removed too
1526: * @param descendant indicates of the element was descendant of this HtmlPage, but now its parent might be null.
1527: */
1528: void removeMappedElement(final HtmlElement element,
1529: final boolean recurse, final boolean descendant) {
1530: if (descendant || isDescendant(element)) {
1531: removeElement(idMap_, element, "id", recurse);
1532: removeElement(nameMap_, element, "name", recurse);
1533: }
1534: }
1535:
1536: private void removeElement(final Map map,
1537: final HtmlElement element, final String att,
1538: final boolean recurse) {
1539: final String value = element.getAttributeValue(att);
1540: if (!StringUtils.isEmpty(value)) {
1541: final List elements = (List) map.remove(value);
1542: if (elements != null && elements.size() != 1) {
1543: elements.remove(element);
1544: map.put(value, elements);
1545: }
1546: }
1547: if (recurse) {
1548: for (final Iterator i = element.getChildElementsIterator(); i
1549: .hasNext();) {
1550: final HtmlElement child = (HtmlElement) i.next();
1551: removeElement(map, child, att, true);
1552: }
1553: }
1554: }
1555:
1556: /**
1557: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
1558: *
1559: * @param node the node that has just been added to the document.
1560: */
1561: void notifyNodeAdded(final DomNode node) {
1562: if (node instanceof HtmlElement) {
1563: boolean insideNoScript = false;
1564: if (getWebClient().isJavaScriptEnabled()) {
1565: for (DomNode parent = node.getParentDomNode(); parent != null; parent = parent
1566: .getParentDomNode()) {
1567: if (parent instanceof HtmlNoScript) {
1568: insideNoScript = true;
1569: break;
1570: }
1571: }
1572: }
1573: if (!insideNoScript) {
1574: addMappedElement((HtmlElement) node, true);
1575: }
1576: }
1577: node.onAddedToPage();
1578: }
1579:
1580: /**
1581: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
1582: *
1583: * @param node the node that has just been removed from the tree
1584: */
1585: void notifyNodeRemoved(final DomNode node) {
1586: if (node instanceof HtmlElement) {
1587: removeMappedElement((HtmlElement) node, true, true);
1588: }
1589: }
1590:
1591: /**
1592: * Loads the content of the contained frames. This is done after the page is completely
1593: * loaded to allow script contained in the frames to reference elements from the
1594: * page located after the closing </frame> tag.
1595: * @throws FailingHttpStatusCodeException If the server returns a failing status code AND the property
1596: * {@link WebClient#setThrowExceptionOnFailingStatusCode(boolean)} is set to true.
1597: */
1598: void loadFrames() throws FailingHttpStatusCodeException {
1599: final List frameTags = Arrays.asList(new String[] { "frame",
1600: "iframe" });
1601: final List frames = getDocumentHtmlElement()
1602: .getHtmlElementsByTagNames(frameTags);
1603: for (final Iterator iter = frames.iterator(); iter.hasNext();) {
1604: final BaseFrame frame = (BaseFrame) iter.next();
1605: // test if the frame should really be loaded:
1606: // if a script has already changed its content, it should be skipped
1607: // use == and not equals(...) to identify initial content (versus url set to "about:blank")
1608: if (frame.getEnclosedPage().getWebResponse().getUrl() == WebClient.URL_ABOUT_BLANK) {
1609: frame.loadInnerPage();
1610: }
1611: }
1612: }
1613:
1614: /**
1615: * {@inheritDoc}
1616: */
1617: public String asXml() {
1618: return getDocumentHtmlElement().asXml();
1619: }
1620:
1621: /**
1622: * Gives a basic representation for debugging purposes
1623: * @return a basic representation
1624: */
1625: public String toString() {
1626: final StringBuffer buffer = new StringBuffer();
1627: buffer.append("HtmlPage(");
1628: buffer.append(getWebResponse().getUrl());
1629: buffer.append(")@");
1630: buffer.append(hashCode());
1631: return buffer.toString();
1632: }
1633:
1634: /**
1635: * Move the focus to the specified component. This will trigger any relevant javascript
1636: * event handlers.
1637: *
1638: * @param newElement The element that will receive the focus, use <code>null</code> to remove focus from any element
1639: * @return true if the specified element now has the focus.
1640: * @see #getElementWithFocus()
1641: * @see #tabToNextElement()
1642: * @see #tabToPreviousElement()
1643: * @see #pressAccessKey(char)
1644: * @see #assertAllTabIndexAttributesSet()
1645: */
1646: public boolean moveFocusToElement(final HtmlElement newElement) {
1647: if (elementWithFocus_ == newElement) {
1648: // nothing to do
1649: return true;
1650: } else if (newElement != null && newElement.getPage() != this ) {
1651: throw new IllegalArgumentException(
1652: "Can't move focus to an element from an other page");
1653: }
1654:
1655: if (elementWithFocus_ != null) {
1656: elementWithFocus_.fireEvent(Event.TYPE_BLUR);
1657: }
1658:
1659: elementWithFocus_ = newElement;
1660: if (newElement != null) {
1661: newElement.fireEvent(Event.TYPE_FOCUS);
1662: }
1663:
1664: // If a page reload happened as a result of the focus change then obviously this
1665: // element will not have the focus because its page has gone away.
1666: return this == getEnclosingWindow().getEnclosedPage();
1667: }
1668:
1669: /**
1670: * Return the element with the focus or null if no element has the focus.
1671: * @return The element with focus or null.
1672: * @see #moveFocusToElement(HtmlElement)
1673: */
1674: public HtmlElement getElementWithFocus() {
1675: return elementWithFocus_;
1676: }
1677:
1678: /**
1679: * Gets the meta tag for a given http-equiv value.
1680: * @param httpEquiv the http-equiv value
1681: * @return a list of {@link HtmlMeta}
1682: */
1683: protected List getMetaTags(final String httpEquiv) {
1684: final String nameLC = httpEquiv.toLowerCase();
1685: final List tags = getDocumentHtmlElement()
1686: .getHtmlElementsByTagNames(
1687: Collections.singletonList("meta"));
1688: for (final Iterator iter = tags.iterator(); iter.hasNext();) {
1689: final HtmlMeta element = (HtmlMeta) iter.next();
1690: if (!nameLC.equals(element.getHttpEquivAttribute()
1691: .toLowerCase())) {
1692: iter.remove();
1693: }
1694: }
1695: return tags;
1696: }
1697:
1698: /**
1699: * Select the specified radio button in the page (outside any <form>).
1700: *
1701: * @param radioButtonInput The radio Button
1702: */
1703: void setCheckedRadioButton(
1704: final HtmlRadioButtonInput radioButtonInput) {
1705: try {
1706: //May be done in single xpath search?
1707: final List pageInputs = getByXPath("//input[lower-case(@type)='radio' "
1708: + "and @name='"
1709: + radioButtonInput.getNameAttribute() + "']");
1710: final List formInputs = getByXPath("//form//input[lower-case(@type)='radio' "
1711: + "and @name='"
1712: + radioButtonInput.getNameAttribute() + "']");
1713:
1714: pageInputs.removeAll(formInputs);
1715:
1716: for (final Iterator iterator = pageInputs.iterator(); iterator
1717: .hasNext();) {
1718: final HtmlRadioButtonInput input = (HtmlRadioButtonInput) iterator
1719: .next();
1720: if (input == radioButtonInput) {
1721: input.setAttributeValue("checked", "checked");
1722: } else {
1723: input.removeAttribute("checked");
1724: }
1725: }
1726: } catch (final JaxenException e) {
1727: getLog().error(e);
1728: }
1729: }
1730:
1731: /**
1732: * Creates a clone of this instance, and clears cached state
1733: * to be not shared with the original.
1734: *
1735: * @return a clone of this instance.
1736: */
1737: protected Object clone() {
1738: try {
1739: final HtmlPage result = (HtmlPage) super .clone();
1740: result.documentElement_ = null;
1741: result.elementWithFocus_ = null;
1742: result.idMap_ = new HashMap();
1743: result.nameMap_ = new HashMap();
1744: return result;
1745: } catch (final CloneNotSupportedException e) {
1746: throw new IllegalStateException("Clone not supported");
1747: }
1748: }
1749:
1750: /**
1751: * Override cloneNode to add cloned elements to the clone, not to the original.
1752: * {@inheritDoc}
1753: * @deprecated
1754: */
1755: public DomNode cloneNode(final boolean deep) {
1756: final HtmlPage result = (HtmlPage) super .cloneDomNode(deep);
1757: if (deep) {
1758: // fix up idMap_ and result's idMap_s
1759: final Iterator it = result.getAllHtmlChildElements();
1760: while (it.hasNext()) {
1761: final HtmlElement child = (HtmlElement) it.next();
1762: removeMappedElement(child);
1763: result.addMappedElement(child);
1764: }
1765: }
1766: return result;
1767: }
1768:
1769: /**
1770: * Adds an HtmlAttributeChangeListener to the listener list.
1771: * The listener is registered for all attributes of all HtmlElements contained in this page.
1772: *
1773: * @param listener the attribute change listener to be added.
1774: * @see #removeHtmlAttributeChangeListener(HtmlAttributeChangeListener)
1775: */
1776: public void addHtmlAttributeChangeListener(
1777: final HtmlAttributeChangeListener listener) {
1778: Assert.notNull("listener", listener);
1779: synchronized (lock_) {
1780: if (attributeListeners_ == null) {
1781: attributeListeners_ = new ArrayList();
1782: }
1783: if (!attributeListeners_.contains(listener)) {
1784: attributeListeners_.add(listener);
1785: }
1786: }
1787: }
1788:
1789: /**
1790: * Removes an HtmlAttributeChangeListener from the listener list.
1791: * This method should be used to remove HtmlAttributeChangeListener that were registered
1792: * for all attributes of all HtmlElements contained in this page.
1793: *
1794: * @param listener the attribute change listener to be removed.
1795: * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
1796: */
1797: public void removeHtmlAttributeChangeListener(
1798: final HtmlAttributeChangeListener listener) {
1799: Assert.notNull("listener", listener);
1800: synchronized (lock_) {
1801: if (attributeListeners_ != null) {
1802: attributeListeners_.remove(listener);
1803: }
1804: }
1805: }
1806:
1807: /**
1808: * Notifies all registered listeners for the given event to add an attribute.
1809: * @param event the event to fire
1810: */
1811: void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
1812: final List listeners = safeGetAttributeListeners();
1813: if (listeners != null) {
1814: for (final Iterator iterator = listeners.iterator(); iterator
1815: .hasNext();) {
1816: final HtmlAttributeChangeListener listener = (HtmlAttributeChangeListener) iterator
1817: .next();
1818: listener.attributeAdded(event);
1819: }
1820: }
1821: }
1822:
1823: /**
1824: * Notifies all registered listeners for the given event to replace an attribute.
1825: * @param event the event to fire
1826: */
1827: void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
1828: final List listeners = safeGetAttributeListeners();
1829: if (listeners != null) {
1830: for (final Iterator iterator = listeners.iterator(); iterator
1831: .hasNext();) {
1832: final HtmlAttributeChangeListener listener = (HtmlAttributeChangeListener) iterator
1833: .next();
1834: listener.attributeReplaced(event);
1835: }
1836: }
1837: }
1838:
1839: /**
1840: * Notifies all registered listeners for the given event to remove an attribute.
1841: * @param event the event to fire
1842: */
1843: void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
1844: final List listeners = safeGetAttributeListeners();
1845: if (listeners != null) {
1846: for (final Iterator iterator = listeners.iterator(); iterator
1847: .hasNext();) {
1848: final HtmlAttributeChangeListener listener = (HtmlAttributeChangeListener) iterator
1849: .next();
1850: listener.attributeRemoved(event);
1851: }
1852: }
1853: }
1854:
1855: private List safeGetAttributeListeners() {
1856: synchronized (lock_) {
1857: if (attributeListeners_ != null) {
1858: return new ArrayList(attributeListeners_);
1859: } else {
1860: return null;
1861: }
1862: }
1863: }
1864:
1865: /**
1866: * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br/>
1867: *
1868: * @return true if the OnbeforeunloadHandler has accepted to change the page.
1869: */
1870: public boolean isOnbeforeunloadAccepted() {
1871: return executeEventHandlersIfNeeded(Event.TYPE_BEFORE_UNLOAD);
1872: }
1873: }
|