com.gargoylesoftware.htmlunit.html.HtmlPage Maven / Gradle / Ivy
Show all versions of vaadin-client-compiler-deps Show documentation
/*
* 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:
*
* - 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.
*
- 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.
*
- 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;
}
}