All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gargoylesoftware.htmlunit.html.HtmlPage Maven / Gradle / Ivy

Go to download

Vaadin is a web application framework for Rich Internet Applications (RIA). Vaadin enables easy development and maintenance of fast and secure rich web applications with a stunning look and feel and a wide browser support. It features a server-side architecture with the majority of the logic running on the server. Ajax technology is used at the browser-side to ensure a rich and interactive user experience.

There is a newer version: 1.2.0
Show newest version
/*
 * Copyright (c) 2002-2011 Gargoyle Software Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gargoylesoftware.htmlunit.html;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.Script;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpStatus;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.DOMException;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.w3c.dom.ranges.Range;

import com.gargoylesoftware.htmlunit.BrowserVersionFeatures;
import com.gargoylesoftware.htmlunit.Cache;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
import com.gargoylesoftware.htmlunit.OnbeforeunloadHandler;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.TextUtil;
import com.gargoylesoftware.htmlunit.TopLevelWindow;
import com.gargoylesoftware.htmlunit.WebAssert;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.HTMLParser.HtmlUnitDOMBuilder;
import com.gargoylesoftware.htmlunit.html.impl.SelectableTextInput;
import com.gargoylesoftware.htmlunit.html.impl.SimpleRange;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptErrorListener;
import com.gargoylesoftware.htmlunit.javascript.PostponedAction;
import com.gargoylesoftware.htmlunit.javascript.host.Event;
import com.gargoylesoftware.htmlunit.javascript.host.Node;
import com.gargoylesoftware.htmlunit.javascript.host.Window;
import com.gargoylesoftware.htmlunit.protocol.javascript.JavaScriptURLConnection;

/**
 * A representation of an HTML page returned from a server.
 * 

* This class provides different methods to access the page's content like * {@link #getForms()}, {@link #getAnchors()}, {@link #getElementById(String)}, ... as well as the * very powerful inherited methods {@link #getByXPath(String)} and {@link #getFirstByXPath(String)} * for fine grained user specific access to child nodes. *

*

* Child elements allowing user interaction provide methods for this purpose like {@link HtmlAnchor#click()}, * {@link HtmlInput#type(String)}, {@link HtmlOption#setSelected(boolean)}, ... *

*

* HtmlPage instances should not be instantiated directly. They will be returned by {@link WebClient#getPage(String)} * when the content type of the server's response is text/html (or one of its variations).
*
* Example:
*
* * final HtmlPage page = webClient.{@link WebClient#getPage(String) getPage}("http://mywebsite/some/page.html"); * *

* * @version $Revision: 6383 $ * @author Mike Bowler * @author Alex Nikiforoff * @author Noboru Sinohara * @author David K. Taylor * @author Andreas Hangler * @author Christian Sell * @author Chris Erskine * @author Marc Guillemot * @author Ahmed Ashour * @author Daniel Gredler * @author Dmitri Zoubkov * @author Sudhan Moghe * @author Ethan Glasser-Camp * @author Tom Anderson * @author Ronald Brill */ public class HtmlPage extends SgmlPage { private static final Log LOG = LogFactory.getLog(HtmlPage.class); private HtmlUnitDOMBuilder builder_; private String originalCharset_; private Map> idMap_ = new HashMap>(); private Map> nameMap_ = new HashMap>(); private HtmlElement elementWithFocus_; private int parserCount_; private int snippetParserCount_; private int inlineSnippetParserCount_; private List attributeListeners_; private final Object lock_ = new String(); // used for synchronization private List selectionRanges_ = new ArrayList< Range >(3); private final List afterLoadActions_ = new ArrayList(); private boolean cleaning_; private HtmlBase base_; private URL baseUrl_; /** * Creates an instance of HtmlPage. * An HtmlPage instance is normally retrieved with {@link WebClient#getPage(String)}. * * @param originatingUrl the URL that was used to load this page * @param webResponse the web response that was used to create this page * @param webWindow the window that this page is being loaded into */ public HtmlPage(final URL originatingUrl, final WebResponse webResponse, final WebWindow webWindow) { super(webResponse, webWindow); } /** * {@inheritDoc} */ @Override public HtmlPage getPage() { return this; } /** * {@inheritDoc} */ @Override public boolean hasCaseSensitiveTagNames() { return false; } /** * Initialize this page. * @throws IOException if an IO problem occurs * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property * {@link WebClient#setThrowExceptionOnFailingStatusCode(boolean)} is set to true. */ @Override public void initialize() throws IOException, FailingHttpStatusCodeException { final WebWindow enclosingWindow = getEnclosingWindow(); if (getWebResponse().getWebRequest().getUrl() == WebClient.URL_ABOUT_BLANK) { // a frame contains first a faked "about:blank" before its real content specified by src gets loaded if (enclosingWindow instanceof FrameWindow && !((FrameWindow) enclosingWindow).getFrameElement().isContentLoaded()) { return; } // save the URL that should be used to resolve relative URLs in this page if (enclosingWindow instanceof TopLevelWindow) { final TopLevelWindow topWindow = (TopLevelWindow) enclosingWindow; final WebWindow openerWindow = topWindow.getOpener(); if (openerWindow != null && openerWindow.getEnclosedPage() != null) { baseUrl_ = openerWindow.getEnclosedPage().getWebResponse() .getWebRequest().getUrl(); } } } loadFrames(); setReadyState(READY_STATE_COMPLETE); getDocumentElement().setReadyState(READY_STATE_COMPLETE); if (getWebClient().getBrowserVersion().hasFeature(BrowserVersionFeatures.EVENT_DOM_CONTENT_LOADED)) { executeEventHandlersIfNeeded(Event.TYPE_DOM_DOCUMENT_LOADED); } executeDeferredScriptsIfNeeded(); setReadyStateOnDeferredScriptsIfNeeded(); executeEventHandlersIfNeeded(Event.TYPE_LOAD); final List actions = new ArrayList(afterLoadActions_); afterLoadActions_.clear(); try { for (final PostponedAction action : actions) { action.execute(); } } catch (final IOException e) { throw e; } catch (final Exception e) { throw new RuntimeException(e); } executeRefreshIfNeeded(); } /** * Adds an action that should be executed once the page has been loaded. * @param action the action */ void addAfterLoadAction(final PostponedAction action) { afterLoadActions_.add(action); } /** * Clean up this page. */ @Override public void cleanUp() { //To avoid endless recursion caused by window.close() in onUnload if (cleaning_) { return; } cleaning_ = true; executeEventHandlersIfNeeded(Event.TYPE_UNLOAD); deregisterFramesIfNeeded(); cleaning_ = false; } /** * {@inheritDoc} */ @Override public HtmlElement getDocumentElement() { return (HtmlElement) super.getDocumentElement(); } /** * Returns the body element (or frameset element), or null if it does not yet exist. * @return the body element (or frameset element), or null if it does not yet exist */ public HtmlElement getBody() { final HtmlElement doc = getDocumentElement(); if (doc != null) { for (final DomNode node : doc.getChildren()) { if (node instanceof HtmlBody || node instanceof HtmlFrameSet) { return (HtmlElement) node; } } } return null; } /** * {@inheritDoc} */ @Override public Document getOwnerDocument() { return null; } /** * {@inheritDoc} * Not yet implemented. */ public org.w3c.dom.Node importNode(final org.w3c.dom.Node importedNode, final boolean deep) { throw new UnsupportedOperationException("HtmlPage.importNode is not yet implemented."); } /** * {@inheritDoc} */ public DomNodeList getElementsByTagName(final String tagName) { return new XPathDomNodeList(this, "//*[local-name()='" + tagName + "']"); } /** * {@inheritDoc} * Not yet implemented. */ public DomNodeList getElementsByTagNameNS(final String namespaceURI, final String localName) { throw new UnsupportedOperationException("HtmlPage.getElementsByTagNameNS is not yet implemented."); } /** * {@inheritDoc} */ public HtmlElement getElementById(final String elementId) { try { return getHtmlElementById(elementId); } catch (final ElementNotFoundException e) { return null; } } /** * {@inheritDoc} * Not yet implemented. */ public String getInputEncoding() { throw new UnsupportedOperationException("HtmlPage.getInputEncoding is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public String getXmlEncoding() { throw new UnsupportedOperationException("HtmlPage.getXmlEncoding is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public boolean getXmlStandalone() { throw new UnsupportedOperationException("HtmlPage.getXmlStandalone is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public void setXmlStandalone(final boolean xmlStandalone) throws DOMException { throw new UnsupportedOperationException("HtmlPage.setXmlStandalone is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public String getXmlVersion() { throw new UnsupportedOperationException("HtmlPage.getXmlVersion is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public void setXmlVersion(final String xmlVersion) throws DOMException { throw new UnsupportedOperationException("HtmlPage.setXmlVersion is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public boolean getStrictErrorChecking() { throw new UnsupportedOperationException("HtmlPage.getStrictErrorChecking is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public void setStrictErrorChecking(final boolean strictErrorChecking) { throw new UnsupportedOperationException("HtmlPage.setStrictErrorChecking is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public String getDocumentURI() { throw new UnsupportedOperationException("HtmlPage.getDocumentURI is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public void setDocumentURI(final String documentURI) { throw new UnsupportedOperationException("HtmlPage.setDocumentURI is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public org.w3c.dom.Node adoptNode(final org.w3c.dom.Node source) throws DOMException { throw new UnsupportedOperationException("HtmlPage.adoptNode is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public DOMConfiguration getDomConfig() { throw new UnsupportedOperationException("HtmlPage.getDomConfig is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public org.w3c.dom.Node renameNode(final org.w3c.dom.Node newNode, final String namespaceURI, final String qualifiedName) throws DOMException { throw new UnsupportedOperationException("HtmlPage.renameNode is not yet implemented."); } /** * {@inheritDoc} */ @Override public String getPageEncoding() { if (originalCharset_ == null) { originalCharset_ = getWebResponse().getContentCharset(); } return originalCharset_; } /** * {@inheritDoc} * @param tagName the tag name, preferably in lowercase */ @Override public HtmlElement createElement(String tagName) { if (tagName.indexOf(':') == -1) { tagName = tagName.toLowerCase(); } return HTMLParser.getFactory(tagName).createElement(this, tagName, null); } /** * {@inheritDoc} */ @Override public HtmlElement createElementNS(final String namespaceURI, final String qualifiedName) { return HTMLParser.getElementFactory(namespaceURI, qualifiedName) .createElementNS(this, namespaceURI, qualifiedName, null); } /** * {@inheritDoc} * Not yet implemented. */ public Attr createAttributeNS(final String namespaceURI, final String qualifiedName) { throw new UnsupportedOperationException("HtmlPage.createAttributeNS is not yet implemented."); } /** * {@inheritDoc} */ public Comment createComment(final String data) { return new DomComment(this, data); } /** * {@inheritDoc} */ public Text createTextNode(final String data) { return new DomText(this, data); } /** * {@inheritDoc} */ public CDATASection createCDATASection(final String data) { return new DomCDataSection(this, data); } /** * {@inheritDoc} */ public DocumentFragment createDocumentFragment() { return new DomDocumentFragment(this); } /** * {@inheritDoc} * Not yet implemented. */ public DOMImplementation getImplementation() { throw new UnsupportedOperationException("HtmlPage.getImplementation is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public EntityReference createEntityReference(final String id) { throw new UnsupportedOperationException("HtmlPage.createEntityReference is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ public ProcessingInstruction createProcessingInstruction(final String namespaceURI, final String qualifiedName) { throw new UnsupportedOperationException("HtmlPage.createProcessingInstruction is not yet implemented."); } /** * Returns the {@link HtmlAnchor} with the specified name. * * @param name the name to search by * @return the {@link HtmlAnchor} with the specified name * @throws ElementNotFoundException if the anchor could not be found */ public HtmlAnchor getAnchorByName(final String name) throws ElementNotFoundException { return getDocumentElement().getOneHtmlElementByAttribute("a", "name", name); } /** * Returns the {@link HtmlAnchor} with the specified href. * * @param href the string to search by * @return the HtmlAnchor * @throws ElementNotFoundException if the anchor could not be found */ public HtmlAnchor getAnchorByHref(final String href) throws ElementNotFoundException { return getDocumentElement().getOneHtmlElementByAttribute("a", "href", href); } /** * Returns a list of all anchors contained in this page. * @return the list of {@link HtmlAnchor} in this page */ public List getAnchors() { return getDocumentElement().getHtmlElementsByTagName("a"); } /** * Returns the first anchor with the specified text. * @param text the text to search for * @return the first anchor that was found * @throws ElementNotFoundException if no anchors are found with the specified text */ public HtmlAnchor getAnchorByText(final String text) throws ElementNotFoundException { WebAssert.notNull("text", text); for (final HtmlAnchor anchor : getAnchors()) { if (text.equals(anchor.asText())) { return anchor; } } throw new ElementNotFoundException("a", "", text); } /** * Returns the first form that matches the specified name. * @param name the name to search for * @return the first form * @exception ElementNotFoundException If no forms match the specified result. */ public HtmlForm getFormByName(final String name) throws ElementNotFoundException { final List forms = getDocumentElement().getElementsByAttribute("form", "name", name); if (forms.size() == 0) { throw new ElementNotFoundException("form", "name", name); } return forms.get(0); } /** * Returns a list of all the forms in this page. * @return all the forms in this page */ public List getForms() { return getDocumentElement().getHtmlElementsByTagName("form"); } /** * Given a relative URL (ie /foo), returns a fully-qualified URL based on * the URL that was used to load this page. * * @param relativeUrl the relative URL * @return the fully-qualified URL for the specified relative URL * @exception MalformedURLException if an error occurred when creating a URL object */ public URL getFullyQualifiedUrl(String relativeUrl) throws MalformedURLException { URL baseUrl; if (base_ == null) { baseUrl = getWebResponse().getWebRequest().getUrl(); final WebWindow window = getEnclosingWindow(); final boolean frame = (window != window.getTopWindow()); if (frame) { final boolean frameSrcIsNotSet = (baseUrl == WebClient.URL_ABOUT_BLANK); final boolean frameSrcIsJs = "javascript".equals(baseUrl.getProtocol()); final boolean jsFrameUseParentUrl = getWebClient().getBrowserVersion() .hasFeature(BrowserVersionFeatures.JS_FRAME_RESOLVE_URL_WITH_PARENT_WINDOW); if (frameSrcIsNotSet || (frameSrcIsJs && jsFrameUseParentUrl)) { baseUrl = ((HtmlPage) window.getTopWindow().getEnclosedPage()).getWebResponse() .getWebRequest().getUrl(); } } else if (baseUrl_ != null) { baseUrl = baseUrl_; } } else { boolean insideHead = false; for (DomNode parent = base_.getParentNode(); parent != null; parent = parent.getParentNode()) { if (parent instanceof HtmlHead) { insideHead = true; break; } } //http://www.w3.org/TR/1999/REC-html401-19991224/struct/links.html#edef-BASE if (!insideHead) { notifyIncorrectness("Element 'base' must appear in , it is ignored."); } final String href = base_.getHrefAttribute(); if (!insideHead || StringUtils.isEmpty(href)) { baseUrl = getWebResponse().getWebRequest().getUrl(); } else { try { baseUrl = new URL(href); } catch (final MalformedURLException e) { notifyIncorrectness("Invalid base url: \"" + href + "\", ignoring it"); baseUrl = getWebResponse().getWebRequest().getUrl(); } } } // to handle http: and http:/ in FF (Bug 1714767) if (getWebClient().getBrowserVersion().hasFeature(BrowserVersionFeatures.URL_MISSING_SLASHES)) { boolean incorrectnessNotified = false; while (relativeUrl.startsWith("http:") && !relativeUrl.startsWith("http://")) { if (!incorrectnessNotified) { notifyIncorrectness("Incorrect URL \"" + relativeUrl + "\" has been corrected"); incorrectnessNotified = true; } relativeUrl = "http:/" + relativeUrl.substring(5); } } return WebClient.expandUrl(baseUrl, relativeUrl); } /** * Given a target attribute value, resolve the target using a base target for the page. * * @param elementTarget the target specified as an attribute of the element * @return the resolved target to use for the element */ public String getResolvedTarget(final String elementTarget) { final String resolvedTarget; if (base_ == null) { resolvedTarget = elementTarget; } else if (elementTarget != null && elementTarget.length() > 0) { resolvedTarget = elementTarget; } else { resolvedTarget = base_.getTargetAttribute(); } return resolvedTarget; } /** * Returns a list of ids (strings) that correspond to the tabbable elements * in this page. Return them in the same order specified in {@link #getTabbableElements} * * @return the list of id's */ public List getTabbableElementIds() { final List list = new ArrayList(); for (final HtmlElement element : getTabbableElements()) { list.add(element.getAttribute("id")); } return Collections.unmodifiableList(list); } /** * Returns a list of all elements that are tabbable in the order that will * be used for tabbing.

* * The rules for determining tab order are as follows: *

    *
  1. Those elements that support the tabindex attribute and assign a * positive value to it are navigated first. Navigation proceeds from the * element with the lowest tabindex value to the element with the highest * value. Values need not be sequential nor must they begin with any * particular value. Elements that have identical tabindex values should * be navigated in the order they appear in the character stream. *
  2. Those elements that do not support the tabindex attribute or * support it and assign it a value of "0" are navigated next. These * elements are navigated in the order they appear in the character * stream. *
  3. Elements that are disabled do not participate in the tabbing * order. *
* Additionally, the value of tabindex must be within 0 and 32767. Any * values outside this range will be ignored.

* * The following elements support the tabindex attribute: A, AREA, BUTTON, * INPUT, OBJECT, SELECT, and TEXTAREA.

* * @return all the tabbable elements in proper tab order */ public List getTabbableElements() { final List tags = Arrays .asList(new String[] {"a", "area", "button", "input", "object", "select", "textarea"}); final List tabbableElements = new ArrayList(); for (final HtmlElement element : getHtmlElementDescendants()) { final String tagName = element.getTagName(); if (tags.contains(tagName)) { final boolean disabled = element.hasAttribute("disabled"); if (!disabled && element.getTabIndex() != HtmlElement.TAB_INDEX_OUT_OF_BOUNDS) { tabbableElements.add(element); } } } Collections.sort(tabbableElements, createTabOrderComparator()); return Collections.unmodifiableList(tabbableElements); } private Comparator createTabOrderComparator() { return new Comparator() { public int compare(final HtmlElement element1, final HtmlElement element2) { final Short i1 = element1.getTabIndex(); final Short i2 = element2.getTabIndex(); final short index1; if (i1 != null) { index1 = i1.shortValue(); } else { index1 = -1; } final short index2; if (i2 != null) { index2 = i2.shortValue(); } else { index2 = -1; } final int result; if (index1 > 0 && index2 > 0) { result = index1 - index2; } else if (index1 > 0) { result = -1; } else if (index2 > 0) { result = +1; } else if (index1 == index2) { result = 0; } else { result = index2 - index1; } return result; } }; } /** * Returns the HTML element that is assigned to the specified access key. An * access key (aka mnemonic key) is used for keyboard navigation of the * page.

* * Only the following HTML elements may have accesskeys defined: A, AREA, * BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA. * * @param accessKey the key to look for * @return the HTML element that is assigned to the specified key or null * if no elements can be found that match the specified key. */ public HtmlElement getElementByAccessKey(final char accessKey) { final List elements = getElementsByAccessKey(accessKey); if (elements.isEmpty()) { return null; } return elements.get(0); } /** * Returns all the HTML elements that are assigned to the specified access key. An * access key (aka mnemonic key) is used for keyboard navigation of the * page.

* * The HTML specification seems to indicate that one accesskey cannot be used * for multiple elements however Internet Explorer does seem to support this. * It's worth noting that Mozilla does not support multiple elements with one * access key so you are making your HTML browser specific if you rely on this * feature.

* * Only the following HTML elements may have accesskeys defined: A, AREA, * BUTTON, INPUT, LABEL, LEGEND, and TEXTAREA. * * @param accessKey the key to look for * @return the elements that are assigned to the specified accesskey */ public List getElementsByAccessKey(final char accessKey) { final List elements = new ArrayList(); final String searchString = Character.toString(accessKey).toLowerCase(); final List acceptableTagNames = Arrays.asList( new String[]{"a", "area", "button", "input", "label", "legend", "textarea"}); for (final HtmlElement element : getHtmlElementDescendants()) { if (acceptableTagNames.contains(element.getTagName())) { final String accessKeyAttribute = element.getAttribute("accesskey"); if (searchString.equalsIgnoreCase(accessKeyAttribute)) { elements.add(element); } } } return elements; } /** *

Executes the specified JavaScript code within the page. The usage would be similar to what can * be achieved to execute JavaScript in the current page by entering "javascript:...some JS code..." * in the URL field of a native browser.

*

Note: the provided code won't be executed if JavaScript has been disabled on the WebClient * (see {@link WebClient#isJavaScriptEnabled()}.

* @param sourceCode the JavaScript code to execute * @return a ScriptResult which will contain both the current page (which may be different than * the previous page) and a JavaScript result object */ public ScriptResult executeJavaScript(final String sourceCode) { return executeJavaScriptIfPossible(sourceCode, "injected script", 1); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*

* Execute the specified JavaScript if a JavaScript engine was successfully * instantiated. If this JavaScript causes the current page to be reloaded * (through location="" or form.submit()) then return the new page. Otherwise * return the current page. *

*

Please note: Although this method is public, it is not intended for * general execution of JavaScript. Users of HtmlUnit should interact with the pages * as a user would by clicking on buttons or links and having the JavaScript event * handlers execute as needed.. *

* * @param sourceCode the JavaScript code to execute * @param sourceName the name for this chunk of code (will be displayed in error messages) * @param startLine the line at which the script source starts * @return a ScriptResult which will contain both the current page (which may be different than * the previous page and a JavaScript result object. */ public ScriptResult executeJavaScriptIfPossible(String sourceCode, final String sourceName, final int startLine) { if (!getWebClient().isJavaScriptEnabled()) { return new ScriptResult(null, this); } if (StringUtils.startsWithIgnoreCase(sourceCode, JavaScriptURLConnection.JAVASCRIPT_PREFIX)) { sourceCode = sourceCode.substring(JavaScriptURLConnection.JAVASCRIPT_PREFIX.length()); } final Object result = getWebClient().getJavaScriptEngine().execute(this, sourceCode, sourceName, startLine); return new ScriptResult(result, getWebClient().getCurrentWindow().getEnclosedPage()); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Execute a Function in the given context. * * @param function the JavaScript Function to call * @param thisObject the "this" object to be used during invocation * @param args the arguments to pass into the call * @param htmlElementScope the HTML element for which this script is being executed * This element will be the context during the JavaScript execution. If null, * the context will default to the page. * @return a ScriptResult which will contain both the current page (which may be different than * the previous page and a JavaScript result object. */ public ScriptResult executeJavaScriptFunctionIfPossible(final Function function, final Scriptable thisObject, final Object[] args, final DomNode htmlElementScope) { if (!getWebClient().isJavaScriptEnabled()) { return new ScriptResult(null, this); } final JavaScriptEngine engine = getWebClient().getJavaScriptEngine(); final Object result = engine.callFunction(this, function, thisObject, args, htmlElementScope); return new ScriptResult(result, getWebClient().getCurrentWindow().getEnclosedPage()); } /** Various possible external JavaScript file loading results. */ enum JavaScriptLoadResult { /** The load was aborted and nothing was done. */ NOOP, /** The external JavaScript file was downloaded and compiled successfully. */ SUCCESS, /** The external JavaScript file was not downloaded successfully. */ DOWNLOAD_ERROR, /** The external JavaScript file was downloaded but was not compiled successfully. */ COMPILATION_ERROR } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * @param srcAttribute the source attribute from the script tag * @param charset the charset attribute from the script tag * @return the result of loading the specified external JavaScript file * @throws FailingHttpStatusCodeException if the request's status code indicates a request * failure and the {@link WebClient} was configured to throw exceptions on failing * HTTP status codes */ JavaScriptLoadResult loadExternalJavaScriptFile(final String srcAttribute, final String charset) throws FailingHttpStatusCodeException { final WebClient client = getWebClient(); if (StringUtils.isBlank(srcAttribute) || !client.isJavaScriptEnabled()) { return JavaScriptLoadResult.NOOP; } final URL scriptURL; try { scriptURL = getFullyQualifiedUrl(srcAttribute); if ("javascript".equals(scriptURL.getProtocol())) { LOG.info("Ignoring script src [" + srcAttribute + "]"); return JavaScriptLoadResult.NOOP; } } catch (final MalformedURLException e) { LOG.error("Unable to build URL for script src tag [" + srcAttribute + "]"); final JavaScriptErrorListener javaScriptErrorListener = client.getJavaScriptErrorListener(); if (javaScriptErrorListener != null) { javaScriptErrorListener.malformedScriptURL(this, srcAttribute, e); } return JavaScriptLoadResult.NOOP; } final Script script; try { script = loadJavaScriptFromUrl(scriptURL, charset); } catch (final IOException e) { LOG.error("Error loading JavaScript from [" + scriptURL + "].", e); final JavaScriptErrorListener javaScriptErrorListener = client.getJavaScriptErrorListener(); if (javaScriptErrorListener != null) { javaScriptErrorListener.loadScriptError(this, scriptURL, e); } return JavaScriptLoadResult.DOWNLOAD_ERROR; } catch (final FailingHttpStatusCodeException e) { LOG.error("Error loading JavaScript from [" + scriptURL + "].", e); final JavaScriptErrorListener javaScriptErrorListener = client.getJavaScriptErrorListener(); if (javaScriptErrorListener != null) { javaScriptErrorListener.loadScriptError(this, scriptURL, e); } throw e; } if (script == null) { return JavaScriptLoadResult.COMPILATION_ERROR; } client.getJavaScriptEngine().execute(this, script); return JavaScriptLoadResult.SUCCESS; } /** * Loads JavaScript from the specified URL. This method may return null if * there is a problem loading the code from the specified URL. * * @param url the URL of the script * @param charset the charset to use to read the text * @return the content of the file, or null if we ran into a compile error * @throws IOException if there is a problem downloading the JavaScript file * @throws FailingHttpStatusCodeException if the request's status code indicates a request * failure and the {@link WebClient} was configured to throw exceptions on failing * HTTP status codes */ private Script loadJavaScriptFromUrl(final URL url, final String charset) throws IOException, FailingHttpStatusCodeException { String scriptEncoding = charset; final String pageEncoding = getPageEncoding(); final WebRequest referringRequest = getWebResponse().getWebRequest(); final WebClient client = getWebClient(); final Cache cache = client.getCache(); final WebRequest request = new WebRequest(url); request.setAdditionalHeaders(new HashMap(referringRequest.getAdditionalHeaders())); request.setAdditionalHeader("Referer", referringRequest.getUrl().toString()); final Object cachedScript = cache.getCachedObject(request); if (cachedScript instanceof Script) { return (Script) cachedScript; } final WebResponse response = client.loadWebResponse(request); client.printContentIfNecessary(response); client.throwFailingHttpStatusCodeExceptionIfNecessary(response); final int statusCode = response.getStatusCode(); final boolean successful = (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES); final boolean noContent = (statusCode == HttpStatus.SC_NO_CONTENT); if (!successful || noContent) { throw new IOException("Unable to download JavaScript from '" + url + "' (status " + statusCode + ")."); } //http://www.ietf.org/rfc/rfc4329.txt final String contentType = response.getContentType(); if (!"application/javascript".equalsIgnoreCase(contentType) && !"application/ecmascript".equalsIgnoreCase(contentType)) { // warn about obsolete or not supported content types if ("text/javascript".equals(contentType) || "text/ecmascript".equals(contentType) || "application/x-javascript".equalsIgnoreCase(contentType)) { getWebClient().getIncorrectnessListener().notify( "Obsolete content type encountered: '" + contentType + "'.", this); } else { getWebClient().getIncorrectnessListener().notify( "Expected content type of 'application/javascript' or 'application/ecmascript' for " + "remotely loaded JavaScript element at '" + url + "', " + "but got '" + contentType + "'.", this); } } if (StringUtils.isEmpty(scriptEncoding)) { final String contentCharset = response.getContentCharset(); if (!contentCharset.equals(TextUtil.DEFAULT_CHARSET)) { scriptEncoding = contentCharset; } else if (!pageEncoding.equals(TextUtil.DEFAULT_CHARSET)) { scriptEncoding = pageEncoding; } else { scriptEncoding = TextUtil.DEFAULT_CHARSET; } } final String scriptCode = response.getContentAsString(scriptEncoding); final JavaScriptEngine javaScriptEngine = client.getJavaScriptEngine(); final Script script = javaScriptEngine.compile(this, scriptCode, url.toExternalForm(), 1); if (script != null) { cache.cacheIfPossible(request, response, script); } return script; } /** * Returns the title of this page or an empty string if the title wasn't specified. * * @return the title of this page or an empty string if the title wasn't specified */ public String getTitleText() { final HtmlTitle titleElement = getTitleElement(); if (titleElement != null) { return titleElement.asText(); } return ""; } /** * Sets the text for the title of this page. If there is not a title element * on this page, then one has to be generated. * @param message the new text */ public void setTitleText(final String message) { HtmlTitle titleElement = getTitleElement(); if (titleElement == null) { if (LOG.isDebugEnabled()) { LOG.debug("No title element, creating one"); } final HtmlHead head = (HtmlHead) getFirstChildElement(getDocumentElement(), HtmlHead.class); if (head == null) { // perhaps should we create head too? throw new IllegalStateException("Headelement was not defined for this page"); } final Map emptyMap = Collections.emptyMap(); titleElement = new HtmlTitle(null, HtmlTitle.TAG_NAME, this, emptyMap); if (head.getFirstChild() != null) { head.getFirstChild().insertBefore(titleElement); } else { head.appendChild(titleElement); } } titleElement.setNodeValue(message); } /** * Gets the first child of startElement that is an instance of the given class. * @param startElement the parent element * @param clazz the class to search for * @return null if no child found */ private HtmlElement getFirstChildElement(final HtmlElement startElement, final Class< ? > clazz) { for (final HtmlElement element : startElement.getChildElements()) { if (clazz.isInstance(element)) { return element; } } return null; } /** * Gets the title element for this page. Returns null if one is not found. * * @return the title element for this page or null if this is not one */ private HtmlTitle getTitleElement() { final HtmlHead head = (HtmlHead) getFirstChildElement(getDocumentElement(), HtmlHead.class); if (head != null) { return (HtmlTitle) getFirstChildElement(head, HtmlTitle.class); } return null; } /** * Looks for and executes any appropriate event handlers. Looks for body and frame tags. * @param eventType either {@link Event#TYPE_LOAD}, {@link Event#TYPE_UNLOAD}, or {@link Event#TYPE_BEFORE_UNLOAD} * @return true if user accepted onbeforeunload (not relevant to other events) */ private boolean executeEventHandlersIfNeeded(final String eventType) { // If JavaScript isn't enabled, there's nothing for us to do. if (!getWebClient().isJavaScriptEnabled()) { return true; } // Execute the specified event on the document element. final WebWindow window = getEnclosingWindow(); final Window jsWindow = (Window) window.getScriptObject(); if (jsWindow != null) { final HtmlElement element = getDocumentElement(); if (element == null) { // should never occur but see bug 3039471 // try to give more information as we currently don't know when and why it occurs final StringBuilder sb = new StringBuilder("No document element ("); sb.append(getUrl()).append(")\n"); try { sb.append(asXml()); } catch (final Exception e) { // ignore } throw new NullPointerException(sb.toString()); } final Event event = new Event(element, eventType); element.fireEvent(event); if (!isOnbeforeunloadAccepted(this, event)) { return false; } } // If this page was loaded in a frame, execute the version of the event specified on the frame tag. if (window instanceof FrameWindow) { final FrameWindow fw = (FrameWindow) window; final BaseFrame frame = fw.getFrameElement(); if (frame.hasEventHandlers("on" + eventType)) { if (LOG.isDebugEnabled()) { LOG.debug("Executing on" + eventType + " handler for " + frame); } final Event event = new Event(frame, eventType); ((Node) frame.getScriptObject()).executeEvent(event); if (!isOnbeforeunloadAccepted((HtmlPage) frame.getPage(), event)) { return false; } } } return true; } private boolean isOnbeforeunloadAccepted(final HtmlPage page, final Event event) { if (event.jsxGet_type().equals(Event.TYPE_BEFORE_UNLOAD) && event.jsxGet_returnValue() != null) { final OnbeforeunloadHandler handler = getWebClient().getOnbeforeunloadHandler(); if (handler == null) { LOG.warn("document.onbeforeunload() returned a string in event.returnValue," + " but no onbeforeunload handler installed."); } else { final String message = Context.toString(event.jsxGet_returnValue()); return handler.handleEvent(page, message); } } return true; } /** * If a refresh has been specified either through a meta tag or an HTTP * response header, then perform that refresh. * @throws IOException if an IO problem occurs */ private void executeRefreshIfNeeded() throws IOException { // If this page is not in a frame then a refresh has already happened, // most likely through the JavaScript onload handler, so we don't do a // second refresh. final WebWindow window = getEnclosingWindow(); if (window == null) { return; } final String refreshString = getRefreshStringOrNull(); if (refreshString == null || refreshString.length() == 0) { return; } final double time; final URL url; int index = StringUtils.indexOfAnyBut(refreshString, "0123456789"); final boolean timeOnly = (index == -1); if (timeOnly) { // Format: try { time = Double.parseDouble(refreshString); } catch (final NumberFormatException e) { LOG.error("Malformed refresh string (no ';' but not a number): " + refreshString, e); return; } url = getWebResponse().getWebRequest().getUrl(); } else { // Format: try { time = Double.parseDouble(refreshString.substring(0, index).trim()); } catch (final NumberFormatException e) { LOG.error("Malformed refresh string (no valid number before ';') " + refreshString, e); return; } index = refreshString.toLowerCase().indexOf("url=", index); if (index == -1) { LOG.error("Malformed refresh string (found ';' but no 'url='): " + refreshString); return; } final StringBuilder buffer = new StringBuilder(refreshString.substring(index + 4)); if (StringUtils.isBlank(buffer.toString())) { //content='10; URL=' is treated as content='10' url = getWebResponse().getWebRequest().getUrl(); } else { if (buffer.charAt(0) == '"' || buffer.charAt(0) == 0x27) { buffer.deleteCharAt(0); } if (buffer.charAt(buffer.length() - 1) == '"' || buffer.charAt(buffer.length() - 1) == 0x27) { buffer.deleteCharAt(buffer.length() - 1); } final String urlString = buffer.toString(); try { url = getFullyQualifiedUrl(urlString); } catch (final MalformedURLException e) { LOG.error("Malformed URL in refresh string: " + refreshString, e); throw e; } } } final int timeRounded = (int) time; getWebClient().getRefreshHandler().handleRefresh(this, url, timeRounded); } /** * Returns an auto-refresh string if specified. This will look in both the meta * tags and inside the HTTP response headers. * @return the auto-refresh string */ private String getRefreshStringOrNull() { for (final HtmlMeta meta : getMetaTags("refresh")) { return meta.getContentAttribute().trim(); } return getWebResponse().getResponseHeaderValue("Refresh"); } /** * Executes any deferred scripts, if necessary. */ private void executeDeferredScriptsIfNeeded() { if (!getWebClient().isJavaScriptEnabled()) { return; } if (getWebClient().getBrowserVersion().hasFeature(BrowserVersionFeatures.JS_DEFERRED)) { final HtmlElement doc = getDocumentElement(); final List elements = doc.getHtmlElementsByTagName("script"); for (final HtmlElement e : elements) { if (e instanceof HtmlScript) { final HtmlScript script = (HtmlScript) e; if (script.isDeferred()) { script.executeScriptIfNeeded(); } } } } } /** * Sets the ready state on any deferred scripts, if necessary. */ private void setReadyStateOnDeferredScriptsIfNeeded() { if (getWebClient().isJavaScriptEnabled() && getWebClient().getBrowserVersion() .hasFeature(BrowserVersionFeatures.JS_DEFERRED)) { final List elements = getDocumentElement().getHtmlElementsByTagName("script"); for (final HtmlElement e : elements) { if (e instanceof HtmlScript) { final HtmlScript script = (HtmlScript) e; if (script.isDeferred()) { script.setAndExecuteReadyState(READY_STATE_COMPLETE); } } } } } /** * Deregister frames that are no longer in use. */ public void deregisterFramesIfNeeded() { for (final WebWindow window : getFrames()) { getWebClient().deregisterWebWindow(window); if (window.getEnclosedPage() instanceof HtmlPage) { final HtmlPage page = (HtmlPage) window.getEnclosedPage(); if (page != null) { // seems quite silly, but for instance if the src attribute of an iframe is not // set, the error only occurs when leaving the page page.deregisterFramesIfNeeded(); } } } } /** * Returns a list containing all the frames (from frame and iframe tags) in this page. * @return a list of {@link FrameWindow} */ public List getFrames() { final List list = new ArrayList(); final WebWindow enclosingWindow = getEnclosingWindow(); for (final WebWindow window : getWebClient().getWebWindows()) { // quite strange but for a TopLevelWindow parent == self if (enclosingWindow == window.getParentWindow() && enclosingWindow != window) { list.add((FrameWindow) window); } } return list; } /** * Returns the first frame contained in this page with the specified name. * @param name the name to search for * @return the first frame found * @exception ElementNotFoundException If no frame exist in this page with the specified name. */ public FrameWindow getFrameByName(final String name) throws ElementNotFoundException { for (final FrameWindow frame : getFrames()) { if (frame.getName().equals(name)) { return frame; } } throw new ElementNotFoundException("frame or iframe", "name", name); } /** * Simulate pressing an access key. This may change the focus, may click buttons and may invoke * JavaScript. * * @param accessKey the key that will be pressed * @return the element that has the focus after pressing this access key or null if no element * has the focus. * @throws IOException if an IO error occurs during the processing of this access key (this * would only happen if the access key triggered a button which in turn caused a page load) */ public HtmlElement pressAccessKey(final char accessKey) throws IOException { final HtmlElement element = getElementByAccessKey(accessKey); if (element != null) { element.focus(); final Page newPage; if (element instanceof HtmlAnchor) { newPage = ((HtmlAnchor) element).click(); } else if (element instanceof HtmlArea) { newPage = ((HtmlArea) element).click(); } else if (element instanceof HtmlButton) { newPage = ((HtmlButton) element).click(); } else if (element instanceof HtmlInput) { newPage = ((HtmlInput) element).click(); } else if (element instanceof HtmlLabel) { newPage = ((HtmlLabel) element).click(); } else if (element instanceof HtmlLegend) { newPage = ((HtmlLegend) element).click(); } else if (element instanceof HtmlTextArea) { newPage = ((HtmlTextArea) element).click(); } else { newPage = this; } if (newPage != this && getFocusedElement() == element) { // The page was reloaded therefore no element on this page will have the focus. getFocusedElement().blur(); } } return getFocusedElement(); } /** * Move the focus to the next element in the tab order. To determine the specified tab * order, refer to {@link HtmlPage#getTabbableElements()} * * @return the element that has focus after calling this method */ public HtmlElement tabToNextElement() { final List elements = getTabbableElements(); if (elements.isEmpty()) { setFocusedElement(null); return null; } final HtmlElement elementToGiveFocus; final HtmlElement elementWithFocus = getFocusedElement(); if (elementWithFocus == null) { elementToGiveFocus = elements.get(0); } else { final int index = elements.indexOf(elementWithFocus); if (index == -1) { // The element with focus isn't on this page elementToGiveFocus = elements.get(0); } else { if (index == elements.size() - 1) { elementToGiveFocus = elements.get(0); } else { elementToGiveFocus = elements.get(index + 1); } } } setFocusedElement(elementToGiveFocus); return elementToGiveFocus; } /** * Move the focus to the previous element in the tab order. To determine the specified tab * order, refer to {@link HtmlPage#getTabbableElements()} * * @return the element that has focus after calling this method */ public HtmlElement tabToPreviousElement() { final List elements = getTabbableElements(); if (elements.isEmpty()) { setFocusedElement(null); return null; } final HtmlElement elementToGiveFocus; final HtmlElement elementWithFocus = getFocusedElement(); if (elementWithFocus == null) { elementToGiveFocus = elements.get(elements.size() - 1); } else { final int index = elements.indexOf(elementWithFocus); if (index == -1) { // The element with focus isn't on this page elementToGiveFocus = elements.get(elements.size() - 1); } else { if (index == 0) { elementToGiveFocus = elements.get(elements.size() - 1); } else { elementToGiveFocus = elements.get(index - 1); } } } setFocusedElement(elementToGiveFocus); return elementToGiveFocus; } /** * Returns the HTML element with the specified ID. If more than one element * has this ID (not allowed by the HTML spec), then this method returns the * first one. * * @param id the ID value to search for * @param the element type * @return the HTML element with the specified ID * @throws ElementNotFoundException if no element was found matching the specified ID */ @SuppressWarnings("unchecked") public E getHtmlElementById(final String id) throws ElementNotFoundException { return (E) getHtmlElementById(id, true); } /** * Returns the HTML element with the specified ID. If more than one element * has this ID (not allowed by the HTML spec), then this method returns the * first one. * * @param id the ID value to search for * @param caseSensitive whether to consider case sensitivity or not * @param the element type * @return the HTML element with the specified ID * @throws ElementNotFoundException if no element was found matching the specified ID */ @SuppressWarnings("unchecked") public E getHtmlElementById(final String id, final boolean caseSensitive) throws ElementNotFoundException { String usedID = id; if (!caseSensitive) { for (final String key : idMap_.keySet()) { if (key.equalsIgnoreCase(usedID)) { usedID = key; break; } } } final List elements = idMap_.get(usedID); if (elements != null) { return (E) elements.get(0); } throw new ElementNotFoundException("*", "id", id); } /** * Returns the HTML element with the specified name. If more than one element * has this name, then this method returns the first one. * * @param name the name value to search for * @param the element type * @return the HTML element with the specified name * @throws ElementNotFoundException if no element was found matching the specified name */ @SuppressWarnings("unchecked") public E getElementByName(final String name) throws ElementNotFoundException { final List elements = nameMap_.get(name); if (elements != null) { return (E) elements.get(0); } throw new ElementNotFoundException("*", "name", name); } /** * Returns the HTML elements with the specified name attribute. If there are no elements * with the specified name, this method returns an empty list. Please note that * the lists returned by this method are immutable. * * @param name the name value to search for * @return the HTML elements with the specified name attribute */ public List getElementsByName(final String name) { final List list = nameMap_.get(name); if (list != null) { return Collections.unmodifiableList(list); } return Collections.emptyList(); } /** * Returns the HTML elements with the specified string for their name or ID. If there are * no elements with the specified name or ID, this method returns an empty list. * * @param idAndOrName the value to search for * @return the HTML elements with the specified string for their name or ID */ public List getElementsByIdAndOrName(final String idAndOrName) { final List list1 = idMap_.get(idAndOrName); final List list2 = nameMap_.get(idAndOrName); final List list = new ArrayList(); if (list1 != null) { list.addAll(list1); } if (list2 != null) { for (final HtmlElement elt : list2) { if (!list.contains(elt)) { list.add(elt); } } } return list; } /** * Adds an element to the ID and name maps, if necessary. * @param element the element to be added to the ID and name maps */ void addMappedElement(final HtmlElement element) { addMappedElement(element, false); } /** * Adds an element to the ID and name maps, if necessary. * @param element the element to be added to the ID and name maps * @param recurse indicates if children must be added too */ void addMappedElement(final HtmlElement element, final boolean recurse) { if (isDescendant(element)) { addElement(idMap_, element, "id", recurse); addElement(nameMap_, element, "name", recurse); } } /** * Checks whether the specified element is descendant of this HtmlPage or not. */ private boolean isDescendant(final HtmlElement element) { for (DomNode parent = element; parent != null; parent = parent.getParentNode()) { if (parent == this) { return true; } } return false; } private void addElement(final Map> map, final HtmlElement element, final String attribute, final boolean recurse) { final String value = element.getAttribute(attribute); if (DomElement.ATTRIBUTE_NOT_DEFINED != value) { List elements = map.get(value); if (elements == null) { elements = new ArrayList(); elements.add(element); map.put(value, elements); } else if (!elements.contains(element)) { elements.add(element); } } if (recurse) { for (final HtmlElement child : element.getChildElements()) { addElement(map, child, attribute, true); } } } /** * Removes an element from the ID and name maps, if necessary. * @param element the element to be removed from the ID and name maps */ void removeMappedElement(final HtmlElement element) { removeMappedElement(element, false, false); } /** * Removes an element and optionally its children from the ID and name maps, if necessary. * @param element the element to be removed from the ID and name maps * @param recurse indicates if children must be removed too * @param descendant indicates of the element was descendant of this HtmlPage, but now its parent might be null */ void removeMappedElement(final HtmlElement element, final boolean recurse, final boolean descendant) { if (descendant || isDescendant(element)) { removeElement(idMap_, element, "id", recurse); removeElement(nameMap_, element, "name", recurse); } } private void removeElement(final Map> map, final HtmlElement element, final String att, final boolean recurse) { final String value = element.getAttribute(att); if (!StringUtils.isEmpty(value)) { final List elements = map.remove(value); if (elements != null && (elements.size() != 1 || !elements.contains(element))) { elements.remove(element); map.put(value, elements); } } if (recurse) { for (final HtmlElement child : element.getChildElements()) { removeElement(map, child, att, true); } } } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * @param node the node that has just been added to the document */ void notifyNodeAdded(final DomNode node) { if (node instanceof HtmlElement) { addMappedElement((HtmlElement) node, true); if ("base".equals(node.getNodeName())) { calculateBase(); } } node.onAddedToPage(); } private void calculateBase() { final List baseElements = getDocumentElement().getHtmlElementsByTagName("base"); switch (baseElements.size()) { case 0: base_ = null; break; case 1: base_ = baseElements.get(0); break; default: base_ = baseElements.get(0); notifyIncorrectness("Multiple 'base' detected, only the first is used."); } } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * @param node the node that has just been removed from the tree */ void notifyNodeRemoved(final DomNode node) { if (node instanceof HtmlElement) { removeMappedElement((HtmlElement) node, true, true); if ("base".equals(node.getNodeName())) { calculateBase(); } } } /** * Loads the content of the contained frames. This is done after the page is completely loaded, to allow script * contained in the frames to reference elements from the page located after the closing </frame> tag. * @throws FailingHttpStatusCodeException if the server returns a failing status code AND the property * {@link WebClient#setThrowExceptionOnFailingStatusCode(boolean)} is set to true */ void loadFrames() throws FailingHttpStatusCodeException { for (final FrameWindow w : getFrames()) { final BaseFrame frame = w.getFrameElement(); // test if the frame should really be loaded: // if a script has already changed its content, it should be skipped // use == and not equals(...) to identify initial content (versus URL set to "about:blank") if (WebClient.URL_ABOUT_BLANK == frame.getEnclosedPage().getWebResponse().getWebRequest().getUrl() && !frame.isContentLoaded()) { frame.loadInnerPage(); } } } /** * Gives a basic representation for debugging purposes. * @return a basic representation */ @Override public String toString() { final StringBuilder buffer = new StringBuilder(); buffer.append("HtmlPage("); buffer.append(getWebResponse().getWebRequest().getUrl()); buffer.append(")@"); buffer.append(hashCode()); return buffer.toString(); } /** * Moves the focus to the specified element. This will trigger any relevant JavaScript * event handlers. * * @param newElement the element that will receive the focus, use null to remove focus from any element * @return true if the specified element now has the focus * @see #getFocusedElement() * @see #tabToNextElement() * @see #tabToPreviousElement() * @see #pressAccessKey(char) * @see WebAssert#assertAllTabIndexAttributesSet(HtmlPage) */ public boolean setFocusedElement(final HtmlElement newElement) { return setFocusedElement(newElement, false); } /** * Moves the focus to the specified element. This will trigger any relevant JavaScript * event handlers. * * @param newElement the element that will receive the focus, use null to remove focus from any element * @param windowActivated - whether the enclosing window got focus resulting in specified element getting focus * @return true if the specified element now has the focus * @see #getFocusedElement() * @see #tabToNextElement() * @see #tabToPreviousElement() * @see #pressAccessKey(char) * @see WebAssert#assertAllTabIndexAttributesSet(HtmlPage) */ public boolean setFocusedElement(final HtmlElement newElement, final boolean windowActivated) { if (elementWithFocus_ == newElement && (!windowActivated)) { // nothing to do return true; } else if (newElement != null && newElement.getPage() != this) { throw new IllegalArgumentException("Can't move focus to an element from a different page."); } final HtmlElement oldFocusedElement = elementWithFocus_; elementWithFocus_ = null; if (!windowActivated) { if (oldFocusedElement != null) { oldFocusedElement.fireEvent(Event.TYPE_FOCUS_OUT); } if (newElement != null) { newElement.fireEvent(Event.TYPE_FOCUS_IN); } if (oldFocusedElement != null) { if (getWebClient().getBrowserVersion().hasFeature(BrowserVersionFeatures.BLUR_BEFORE_ONCHANGE)) { oldFocusedElement.fireEvent(Event.TYPE_BLUR); oldFocusedElement.removeFocus(); } else { // IE, FF3 oldFocusedElement.removeFocus(); oldFocusedElement.fireEvent(Event.TYPE_BLUR); } } } elementWithFocus_ = newElement; if (elementWithFocus_ instanceof SelectableTextInput && getWebClient().getBrowserVersion() .hasFeature(BrowserVersionFeatures.PAGE_SELECTION_RANGE_FROM_SELECTABLE_TEXT_INPUT)) { final SelectableTextInput sti = (SelectableTextInput) elementWithFocus_; setSelectionRange(new SimpleRange(sti, sti.getSelectionStart(), sti, sti.getSelectionEnd())); } if (elementWithFocus_ != null) { elementWithFocus_.focus(); elementWithFocus_.fireEvent(Event.TYPE_FOCUS); } // If a page reload happened as a result of the focus change then obviously this // element will not have the focus because its page has gone away. return this == getEnclosingWindow().getEnclosedPage(); } /** * Returns the element with the focus or null if no element has the focus. * @return the element with focus or null * @see #setFocusedElement(HtmlElement) */ public HtmlElement getFocusedElement() { return elementWithFocus_; } /** * Gets the meta tag for a given http-equiv value. * @param httpEquiv the http-equiv value * @return a list of {@link HtmlMeta} */ protected List getMetaTags(final String httpEquiv) { final String nameLC = httpEquiv.toLowerCase(); final List tags = getDocumentElement().getHtmlElementsByTagName("meta"); for (final Iterator iter = tags.iterator(); iter.hasNext();) { final HtmlMeta element = iter.next(); if (!nameLC.equals(element.getHttpEquivAttribute().toLowerCase())) { iter.remove(); } } return tags; } /** * Select the specified radio button in the page (outside any <form>). * * @param radioButtonInput the radio Button */ @SuppressWarnings("unchecked") void setCheckedRadioButton(final HtmlRadioButtonInput radioButtonInput) { // May be done in single XPath search? final List pageInputs = (List) getByXPath("//input[lower-case(@type)='radio' " + "and @name='" + radioButtonInput.getNameAttribute() + "']"); final List formInputs = (List) getByXPath("//form//input[lower-case(@type)='radio' " + "and @name='" + radioButtonInput.getNameAttribute() + "']"); pageInputs.removeAll(formInputs); boolean found = false; for (final HtmlRadioButtonInput input : pageInputs) { if (input == radioButtonInput) { input.setAttribute("checked", "checked"); found = true; } else { input.removeAttribute("checked"); } } for (final HtmlRadioButtonInput input : formInputs) { if (input == radioButtonInput) { found = true; } } if (!found) { radioButtonInput.setAttribute("checked", "checked"); } } /** * Creates a clone of this instance, and clears cached state * to be not shared with the original. * * @return a clone of this instance */ @Override protected HtmlPage clone() { final HtmlPage result = (HtmlPage) super.clone(); result.elementWithFocus_ = null; result.idMap_ = new HashMap>(); result.nameMap_ = new HashMap>(); return result; } /** * {@inheritDoc} * Override cloneNode to add cloned elements to the clone, not to the original. */ @Override public HtmlPage cloneNode(final boolean deep) { final HtmlPage result = (HtmlPage) super.cloneNode(deep); result.setScriptObject(getScriptObject()); if (deep) { // fix up idMap_ and result's idMap_s for (final HtmlElement child : result.getHtmlElementDescendants()) { removeMappedElement(child); result.addMappedElement(child); } } return result; } /** * Adds an HtmlAttributeChangeListener to the listener list. * The listener is registered for all attributes of all HtmlElements contained in this page. * * @param listener the attribute change listener to be added * @see #removeHtmlAttributeChangeListener(HtmlAttributeChangeListener) */ public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) { WebAssert.notNull("listener", listener); synchronized (lock_) { if (attributeListeners_ == null) { attributeListeners_ = new ArrayList(); } if (!attributeListeners_.contains(listener)) { attributeListeners_.add(listener); } } } /** * Removes an HtmlAttributeChangeListener from the listener list. * This method should be used to remove HtmlAttributeChangeListener that were registered * for all attributes of all HtmlElements contained in this page. * * @param listener the attribute change listener to be removed * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener) */ public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) { WebAssert.notNull("listener", listener); synchronized (lock_) { if (attributeListeners_ != null) { attributeListeners_.remove(listener); } } } /** * Notifies all registered listeners for the given event to add an attribute. * @param event the event to fire */ void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) { final List listeners = safeGetAttributeListeners(); if (listeners != null) { for (final HtmlAttributeChangeListener listener : listeners) { listener.attributeAdded(event); } } } /** * Notifies all registered listeners for the given event to replace an attribute. * @param event the event to fire */ void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) { final List listeners = safeGetAttributeListeners(); if (listeners != null) { for (final HtmlAttributeChangeListener listener : listeners) { listener.attributeReplaced(event); } } } /** * Notifies all registered listeners for the given event to remove an attribute. * @param event the event to fire */ void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) { final List listeners = safeGetAttributeListeners(); if (listeners != null) { for (final HtmlAttributeChangeListener listener : listeners) { listener.attributeRemoved(event); } } } private List safeGetAttributeListeners() { synchronized (lock_) { if (attributeListeners_ != null) { return new ArrayList(attributeListeners_); } return null; } } /** * {@inheritDoc} */ @Override protected void checkChildHierarchy(final org.w3c.dom.Node newChild) throws DOMException { if (newChild instanceof Element) { if (getDocumentElement() != null) { throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "The Document may only have a single child Element."); } } else if (newChild instanceof DocumentType) { if (getDoctype() != null) { throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "The Document may only have a single child DocumentType."); } } else if (!((newChild instanceof Comment) || (newChild instanceof ProcessingInstruction))) { throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "The Document may not have a child of this type: " + newChild.getNodeType()); } super.checkChildHierarchy(newChild); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * @return true if the OnbeforeunloadHandler has accepted to change the page */ public boolean isOnbeforeunloadAccepted() { return executeEventHandlersIfNeeded(Event.TYPE_BEFORE_UNLOAD); } /** * Returns true if an HTML parser is operating on this page, adding content to it. * @return true if an HTML parser is operating on this page, adding content to it */ public boolean isBeingParsed() { return parserCount_ > 0; } /** * Called by the HTML parser to let the page know that it has started parsing some content for this page. */ void registerParsingStart() { parserCount_++; } /** * Called by the HTML parser to let the page know that it has finished parsing some content for this page. */ void registerParsingEnd() { parserCount_--; } /** * Returns true if an HTML parser is parsing a non-inline HTML snippet to add content * to this page. Non-inline content is content that is parsed for the page, but not in the * same stream as the page itself -- basically anything other than document.write() * or document.writeln(): innerHTML, otherHTML, * document.createElement(), etc. * * @return true if an HTML parser is parsing a non-inline HTML snippet to add content * to this page */ boolean isParsingHtmlSnippet() { return snippetParserCount_ > 0; } /** * Called by the HTML parser to let the page know that it has started parsing a non-inline HTML snippet. */ void registerSnippetParsingStart() { snippetParserCount_++; } /** * Called by the HTML parser to let the page know that it has finished parsing a non-inline HTML snippet. */ void registerSnippetParsingEnd() { snippetParserCount_--; // maybe the stream has added a iframe tag if (0 == snippetParserCount_) { loadFrames(); } } /** * Returns true if an HTML parser is parsing an inline HTML snippet to add content * to this page. Inline content is content inserted into the parser stream dynamically * while the page is being parsed (i.e. document.write() or document.writeln()). * * @return true if an HTML parser is parsing an inline HTML snippet to add content * to this page */ boolean isParsingInlineHtmlSnippet() { return inlineSnippetParserCount_ > 0; } /** * Called by the HTML parser to let the page know that it has started parsing an inline HTML snippet. */ void registerInlineSnippetParsingStart() { inlineSnippetParserCount_++; } /** * Called by the HTML parser to let the page know that it has finished parsing an inline HTML snippet. */ void registerInlineSnippetParsingEnd() { inlineSnippetParserCount_--; } /** * Refreshes the page by sending the same parameters as previously sent to get this page. * @return the newly loaded page. * @throws IOException if an IO problem occurs */ public Page refresh() throws IOException { return getWebClient().getPage(getWebResponse().getWebRequest()); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*

* Parses the given string as would it belong to the content being parsed * at the current parsing position *

* @param string the HTML code to write in place */ public void writeInParsedStream(final String string) { builder_.pushInputString(string); } /** * Sets the builder to allow page to send content from document.write(ln) calls. * @param htmlUnitDOMBuilder the builder */ void setBuilder(final HtmlUnitDOMBuilder htmlUnitDOMBuilder) { builder_ = htmlUnitDOMBuilder; } /** * Returns the current builder. * @return the current builder */ HtmlUnitDOMBuilder getBuilder() { return builder_; } /** *

INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.

* *

Returns the page's current selection ranges. Note that some browsers, like IE, only allow * a single selection at a time.

* * @return the page's current selection ranges */ public List getSelectionRanges() { return selectionRanges_; } /** *

INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.

* *

Makes the specified selection range the *only* selection range on this page.

* * @param selectionRange the selection range */ public void setSelectionRange(final Range selectionRange) { selectionRanges_.clear(); selectionRanges_.add(selectionRange); } /** *

Returns all namespaces defined in the root element of this page.

*

The default namespace has a key of an empty string.

* @return all namespaces defined in the root element of this page */ public Map getNamespaces() { final org.w3c.dom.NamedNodeMap attributes = getDocumentElement().getAttributes(); final Map namespaces = new HashMap(); for (int i = 0; i < attributes.getLength(); i++) { final Attr attr = (Attr) attributes.item(i); String name = attr.getName(); if (name.startsWith("xmlns")) { int startPos = 5; if ((name.length() > 5) && (name.charAt(5) == ':')) { startPos = 6; } name = name.substring(startPos); namespaces.put(name, attr.getValue()); } } return namespaces; } /** * {@inheritDoc} */ @Override protected void setDocumentType(final DomDocumentType type) { super.setDocumentType(type); } /** * Saves the current page, with all images, to the specified location. * The default behavior removes all script elements. * * @param file file to write this page into * @throws IOException If an error occurs */ public void save(final File file) throws IOException { new XmlSerializer().save(this, file); } /** * Indicates if the attribute name indicates that the owning element is mapped. * @param document the owning document * @param attributeName the name of the attribute to consider * @return true if the owning element should be mapped in its owning page */ static boolean isMappedElement(final Document document, final String attributeName) { return (document instanceof HtmlPage) && ("name".equals(attributeName) || "id".equals(attributeName)); } /** * Returns whether the current page mode is in quirks mode or in standards mode. * @return true for quirks mode, false for standards mode */ public boolean isQuirksMode() { boolean quirks = true; final DocumentType docType = getDoctype(); if (docType != null) { final String publicId = docType.getPublicId(); final String systemId = docType.getSystemId(); if (systemId != null) { if ("http://www.w3.org/TR/html4/strict.dtd".equals(systemId)) { quirks = false; } else if ("http://www.w3.org/TR/html4/loose.dtd".equals(systemId)) { if ("-//W3C//DTD HTML 4.01 Transitional//EN".equals(publicId) || ("-//W3C//DTD HTML 4.0 Transitional//EN".equals(publicId) && getWebClient().getBrowserVersion() .hasFeature(BrowserVersionFeatures.DOCTYPE_4_0_TRANSITIONAL_STANDARDS))) { quirks = false; } } else if ("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd".equals(systemId) || "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd".equals(systemId)) { quirks = false; } } } return quirks; } /** * Retrieves all element nodes from descendants of the starting element node that match any selector * within the supplied selector strings. * @param selectors one or more CSS selectors separated by commas * @return list of all found nodes */ public DomNodeList querySelectorAll(final String selectors) { return super.querySelectorAll(selectors); } /** * Returns the first element within the document that matches the specified group of selectors. * @param selectors one or more CSS selectors separated by commas * @return null if no matches are found; otherwise, it returns the first matching element */ public DomNode querySelector(final String selectors) { return super.querySelector(selectors); } @Override protected boolean isDirectlyAttachedToPage() { return true; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy