com.gargoylesoftware.htmlunit.html.HtmlElement Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of htmlunit Show documentation
Show all versions of htmlunit Show documentation
A headless browser intended for use in testing web-based applications.
/*
* Copyright (c) 2002-2016 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 static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT;
import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.KEYBOARD_EVENT_SPECIAL_KEYPRESS;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.InteractivePage;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.WebAssert;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
import com.gargoylesoftware.htmlunit.javascript.host.event.EventHandler;
import com.gargoylesoftware.htmlunit.javascript.host.event.KeyboardEvent;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
/**
* An abstract wrapper for HTML elements.
*
* @author Mike Bowler
* @author Mike J. Bresnahan
* @author David K. Taylor
* @author Christian Sell
* @author David D. Kilzer
* @author Mike Gallaher
* @author Denis N. Antonioli
* @author Marc Guillemot
* @author Ahmed Ashour
* @author Daniel Gredler
* @author Dmitri Zoubkov
* @author Sudhan Moghe
* @author Ronald Brill
* @author Frank Danek
*/
public abstract class HtmlElement extends DomElement {
/**
* Enum for the different display styles.
*/
public enum DisplayStyle {
/** Empty string. */
EMPTY(""),
/** none. */
NONE("none"),
/** block. */
BLOCK("block"),
/** inline. */
INLINE("inline"),
/** inline-block. */
INLINE_BLOCK("inline-block"),
/** list-item. */
LIST_ITEM("list-item"),
/** table. */
TABLE("table"),
/** table-cell. */
TABLE_CELL("table-cell"),
/** table-column. */
TABLE_COLUMN("table-column"),
/** table-column-group. */
TABLE_COLUMN_GROUP("table-column-group"),
/** table-row. */
TABLE_ROW("table-row"),
/** table-row-group. */
TABLE_ROW_GROUP("table-row-group"),
/** table-header-group. */
TABLE_HEADER_GROUP("table-header-group"),
/** table-footer-group. */
TABLE_FOOTER_GROUP("table-footer-group"),
/** table-caption. */
TABLE_CAPTION("table-caption"),
/** ruby. */
RUBY("ruby"),
/** ruby-text. */
RUBY_TEXT("ruby-text");
private final String value_;
DisplayStyle(final String value) {
value_ = value;
}
/**
* The string used from js.
* @return the value as string
*/
public String value() {
return value_;
}
}
private static final Log LOG = LogFactory.getLog(HtmlElement.class);
/**
* Constant indicating that a tab index value is out of bounds (less than 0 or greater
* than 32767).
*
* @see #getTabIndex()
*/
public static final Short TAB_INDEX_OUT_OF_BOUNDS = new Short(Short.MIN_VALUE);
/** The listeners which are to be notified of attribute changes. */
private final Collection attributeListeners_;
/** The owning form for lost form children. */
private HtmlForm owningForm_;
/**
* Creates an instance.
*
* @param qualifiedName the qualified name of the element type to instantiate
* @param page the page that contains this element
* @param attributes a map ready initialized with the attributes for this element, or
* {@code null}. The map will be stored as is, not copied.
*/
protected HtmlElement(final String qualifiedName, final SgmlPage page,
final Map attributes) {
super(HTMLParser.XHTML_NAMESPACE, qualifiedName, page, attributes);
attributeListeners_ = new LinkedHashSet<>();
}
/**
* Sets the value of the specified attribute. This method may be overridden by subclasses
* which are interested in specific attribute value changes, but such methods must
* invoke super.setAttributeValue(), and should consider the value of the
* cloning parameter when deciding whether or not to execute custom logic.
*
* @param namespaceURI the URI that identifies an XML namespace
* @param qualifiedName the qualified name of the attribute
* @param attributeValue the value of the attribute
*/
@Override
public void setAttributeNS(final String namespaceURI, final String qualifiedName,
final String attributeValue) {
// TODO: Clean up; this is a hack for HtmlElement living within an XmlPage.
if (null == getHtmlPageOrNull()) {
super.setAttributeNS(namespaceURI, qualifiedName, attributeValue);
return;
}
final String oldAttributeValue = getAttribute(qualifiedName);
final HtmlPage htmlPage = (HtmlPage) getPage();
final boolean mappedElement = isAttachedToPage()
&& HtmlPage.isMappedElement(htmlPage, qualifiedName);
if (mappedElement) {
// cast is save here because isMappedElement checks for HtmlPage
htmlPage.removeMappedElement(this);
}
super.setAttributeNS(namespaceURI, qualifiedName, attributeValue);
if (mappedElement) {
htmlPage.addMappedElement(this);
}
final HtmlAttributeChangeEvent htmlEvent;
if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
htmlEvent = new HtmlAttributeChangeEvent(this, qualifiedName, attributeValue);
fireHtmlAttributeAdded(htmlEvent);
htmlPage.fireHtmlAttributeAdded(htmlEvent);
}
else {
htmlEvent = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
fireHtmlAttributeReplaced(htmlEvent);
htmlPage.fireHtmlAttributeReplaced(htmlEvent);
}
}
/**
* Sets the specified attribute. This method may be overridden by subclasses
* which are interested in specific attribute value changes, but such methods must
* invoke super.setAttributeNode(), and should consider the value of the
* cloning parameter when deciding whether or not to execute custom logic.
*
* @param attribute the attribute to set
* @return {@inheritDoc}
*/
@Override
public Attr setAttributeNode(final Attr attribute) {
final String qualifiedName = attribute.getName();
final String oldAttributeValue = getAttribute(qualifiedName);
final HtmlPage htmlPage = (HtmlPage) getPage();
final boolean mappedElement = isAttachedToPage()
&& HtmlPage.isMappedElement(htmlPage, qualifiedName);
if (mappedElement) {
// cast is save here because isMappedElement checks for HtmlPage
htmlPage.removeMappedElement(this);
}
final Attr result = super.setAttributeNode(attribute);
if (mappedElement) {
htmlPage.addMappedElement(this);
}
final HtmlAttributeChangeEvent htmlEvent;
if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
htmlEvent = new HtmlAttributeChangeEvent(this, qualifiedName, attribute.getValue());
fireHtmlAttributeAdded(htmlEvent);
htmlPage.fireHtmlAttributeAdded(htmlEvent);
}
else {
htmlEvent = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
fireHtmlAttributeReplaced(htmlEvent);
htmlPage.fireHtmlAttributeReplaced(htmlEvent);
}
return result;
}
/**
* Returns the HTML elements that are descendants of this element and that have one of the specified tag names.
* @param tagNames the tag names to match (case-insensitive)
* @return the HTML elements that are descendants of this element and that have one of the specified tag name
* @deprecated as of 2.21, please use {@link #getElementsByTagName(String)}
*/
@Deprecated
public final List getHtmlElementsByTagNames(final List tagNames) {
final List list = new ArrayList<>();
for (final String tagName : tagNames) {
list.addAll(getElementsByTagName(tagName));
}
return list;
}
/**
* Returns the HTML elements that are descendants of this element and that have the specified tag name.
* @param tagName the tag name to match (case-insensitive)
* @param the sub-element type
* @return the HTML elements that are descendants of this element and that have the specified tag name
* @deprecated as of 2.21, please use {@link #getElementsByTagName(String)}, which returns read-only list
*/
@Deprecated
public final List getHtmlElementsByTagName(final String tagName) {
return new ArrayList<>(this.getElementsByTagNameImpl(tagName));
}
/**
* Removes an attribute specified by name from this element.
* @param attributeName the attribute attributeName
*/
@Override
public final void removeAttribute(final String attributeName) {
final String value = getAttribute(attributeName);
if (value == ATTRIBUTE_NOT_DEFINED) {
return;
}
final HtmlPage htmlPage = getHtmlPageOrNull();
if (htmlPage != null) {
htmlPage.removeMappedElement(this);
}
// TODO is this toLowerCase call needed?
super.removeAttribute(attributeName.toLowerCase(Locale.ROOT));
if (htmlPage != null) {
htmlPage.addMappedElement(this);
final HtmlAttributeChangeEvent event = new HtmlAttributeChangeEvent(this, attributeName, value);
fireHtmlAttributeRemoved(event);
htmlPage.fireHtmlAttributeRemoved(event);
}
}
/**
* Support for reporting HTML attribute changes. This method can be called when an attribute
* has been added and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
* registered {@link HtmlAttributeChangeListener}s.
*
* Note that this method recursively calls this element's parent's
* {@link #fireHtmlAttributeAdded(HtmlAttributeChangeEvent)} method.
*
* @param event the event
* @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
*/
protected void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
synchronized (attributeListeners_) {
for (final HtmlAttributeChangeListener listener : attributeListeners_) {
listener.attributeAdded(event);
}
}
final DomNode parentNode = getParentNode();
if (parentNode instanceof HtmlElement) {
((HtmlElement) parentNode).fireHtmlAttributeAdded(event);
}
}
/**
* Support for reporting HTML attribute changes. This method can be called when an attribute
* has been replaced and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
* registered {@link HtmlAttributeChangeListener}s.
*
* Note that this method recursively calls this element's parent's
* {@link #fireHtmlAttributeReplaced(HtmlAttributeChangeEvent)} method.
*
* @param event the event
* @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
*/
protected void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
synchronized (attributeListeners_) {
for (final HtmlAttributeChangeListener listener : attributeListeners_) {
listener.attributeReplaced(event);
}
}
final DomNode parentNode = getParentNode();
if (parentNode instanceof HtmlElement) {
((HtmlElement) parentNode).fireHtmlAttributeReplaced(event);
}
}
/**
* Support for reporting HTML attribute changes. This method can be called when an attribute
* has been removed and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
* registered {@link HtmlAttributeChangeListener}s.
*
* Note that this method recursively calls this element's parent's
* {@link #fireHtmlAttributeRemoved(HtmlAttributeChangeEvent)} method.
*
* @param event the event
* @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
*/
protected void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
synchronized (attributeListeners_) {
for (final HtmlAttributeChangeListener listener : attributeListeners_) {
listener.attributeRemoved(event);
}
}
final DomNode parentNode = getParentNode();
if (parentNode instanceof HtmlElement) {
((HtmlElement) parentNode).fireHtmlAttributeRemoved(event);
}
}
/**
* @return the same value as returned by {@link #getTagName()}
*/
@Override
public String getNodeName() {
final String prefix = getPrefix();
if (prefix != null) {
// create string builder only if needed (performance)
final StringBuilder name = new StringBuilder(prefix.toLowerCase(Locale.ROOT));
name.append(':');
name.append(getLocalName().toLowerCase(Locale.ROOT));
return name.toString();
}
return getLocalName().toLowerCase(Locale.ROOT);
}
/**
* Sets the identifier this element.
*
* @param newId the new identifier of this element
*/
public final void setId(final String newId) {
setAttribute("id", newId);
}
/**
* Returns this element's tab index, if it has one. If the tab index is outside of the
* valid range (less than 0 or greater than 32767), this method
* returns {@link #TAB_INDEX_OUT_OF_BOUNDS}. If this element does not have
* a tab index, or its tab index is otherwise invalid, this method returns {@code null}.
*
* @return this element's tab index
*/
public Short getTabIndex() {
final String index = getAttribute("tabindex");
if (index == null || index.isEmpty()) {
return null;
}
try {
final long l = Long.parseLong(index);
if (l >= 0 && l <= Short.MAX_VALUE) {
return Short.valueOf((short) l);
}
return TAB_INDEX_OUT_OF_BOUNDS;
}
catch (final NumberFormatException e) {
return null;
}
}
/**
* Returns the first element with the specified tag name that is an ancestor to this element, or
* {@code null} if no such element is found.
* @param tagName the name of the tag searched (case insensitive)
* @return the first element with the specified tag name that is an ancestor to this element
*/
public HtmlElement getEnclosingElement(final String tagName) {
final String tagNameLC = tagName.toLowerCase(Locale.ROOT);
for (DomNode currentNode = getParentNode(); currentNode != null; currentNode = currentNode.getParentNode()) {
if (currentNode instanceof HtmlElement && currentNode.getNodeName().equals(tagNameLC)) {
return (HtmlElement) currentNode;
}
}
return null;
}
/**
* Returns the form which contains this element, or {@code null} if this element is not inside
* of a form.
* @return the form which contains this element
*/
public HtmlForm getEnclosingForm() {
if (owningForm_ != null) {
return owningForm_;
}
return (HtmlForm) getEnclosingElement("form");
}
/**
* Returns the form which contains this element. If this element is not inside a form, this method
* throws an {@link IllegalStateException}.
* @return the form which contains this element
* @throws IllegalStateException if the element is not inside a form
*/
public HtmlForm getEnclosingFormOrDie() throws IllegalStateException {
final HtmlForm form = getEnclosingForm();
if (form == null) {
throw new IllegalStateException("Element is not contained within a form: " + this);
}
return form;
}
/**
* Simulates typing the specified text while this element has focus.
* Note that for some elements, typing '\n' submits the enclosed form.
* @param text the text you with to simulate typing
* @exception IOException If an IO error occurs
*/
public void type(final String text) throws IOException {
for (final char ch : text.toCharArray()) {
type(ch);
}
}
/**
* Simulates typing the specified text while this element has focus.
* Note that for some elements, typing '\n' submits the enclosed form.
* @param text the text you with to simulate typing
* @param shiftKey true if SHIFT is pressed
* @param ctrlKey true if CTRL is pressed
* @param altKey true if ALT is pressed
* @exception IOException If an IO error occurs
* @deprecated as of 2.18, please use {@link #type(Keyboard)} instead
*/
@Deprecated
public void type(final String text, final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
throws IOException {
for (final char ch : text.toCharArray()) {
type(ch, shiftKey, ctrlKey, altKey);
}
}
/**
* Simulates typing the specified character while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc. Note also that for some elements, typing '\n'
* submits the enclosed form.
*
* @param c the character you wish to simulate typing
* @return the page that occupies this window after typing
* @exception IOException if an IO error occurs
*/
public Page type(final char c) throws IOException {
return type(c, false, false, false);
}
/**
* Simulates typing the specified character while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc. Note also that for some elements, typing '\n'
* submits the enclosed form.
*
* @param c the character you wish to simulate typing
* @param shiftKey {@code true} if SHIFT is pressed during the typing
* @param ctrlKey {@code true} if CTRL is pressed during the typing
* @param altKey {@code true} if ALT is pressed during the typing
* @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
* @exception IOException if an IO error occurs
* @deprecated as of 2.18, please use {@link #type(Keyboard)} instead
*/
@Deprecated
// would be private
public Page type(final char c, final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
throws IOException {
if (this instanceof DisabledElement && ((DisabledElement) this).isDisabled()) {
return getPage();
}
// make enclosing window the current one
getPage().getWebClient().setCurrentWindow(getPage().getEnclosingWindow());
final HtmlPage page = (HtmlPage) getPage();
if (page.getFocusedElement() != this) {
focus();
}
final boolean isShiftNeeded = KeyboardEvent.isShiftNeeded(c, shiftKey);
final Event shiftDown;
final ScriptResult shiftDownResult;
if (isShiftNeeded) {
shiftDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, KeyboardEvent.DOM_VK_SHIFT,
true, ctrlKey, altKey);
shiftDownResult = fireEvent(shiftDown);
}
else {
shiftDown = null;
shiftDownResult = null;
}
final Event keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, c, shiftKey, ctrlKey, altKey);
final ScriptResult keyDownResult = fireEvent(keyDown);
final Event keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, c, shiftKey, ctrlKey, altKey);
final ScriptResult keyPressResult = fireEvent(keyPress);
if ((shiftDownResult == null || !shiftDown.isAborted(shiftDownResult))
&& !keyDown.isAborted(keyDownResult) && !keyPress.isAborted(keyPressResult)) {
doType(c, shiftKey, ctrlKey, altKey);
}
final WebClient webClient = page.getWebClient();
if (this instanceof HtmlTextInput
|| this instanceof HtmlTextArea
|| this instanceof HtmlPasswordInput) {
final Event input = new KeyboardEvent(this, Event.TYPE_INPUT, c, shiftKey, ctrlKey, altKey);
fireEvent(input);
}
final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, c, shiftKey, ctrlKey, altKey);
fireEvent(keyUp);
if (isShiftNeeded) {
final Event shiftUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, KeyboardEvent.DOM_VK_SHIFT,
false, ctrlKey, altKey);
fireEvent(shiftUp);
}
final HtmlForm form = getEnclosingForm();
if (form != null && c == '\n' && isSubmittableByEnter()) {
final HtmlSubmitInput submit = form.getFirstByXPath(".//input[@type='submit']");
if (submit != null) {
return submit.click();
}
form.submit((SubmittableElement) this);
webClient.getJavaScriptEngine().processPostponedActions();
}
return webClient.getCurrentWindow().getEnclosedPage();
}
/**
* Simulates typing the specified key code while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc. Note also that for some elements, typing XXXXXXXXXXX
* submits the enclosed form.
*
* An example of predefined values is {@link KeyboardEvent#DOM_VK_PAGE_DOWN}.
*
* @param keyCode the key code to simulate typing
* @return the page that occupies this window after typing
* @exception IOException if an IO error occurs
*/
public Page type(final int keyCode) {
return type(keyCode, false, false, false, true, true, true);
}
/**
* Simulates typing the specified {@link Keyboard} while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc. Note also that for some elements, typing XXXXXXXXXXX
* submits the enclosed form.
*
* @param keyboard the keyboard
* @return the page that occupies this window after typing
* @exception IOException if an IO error occurs
*/
public Page type(final Keyboard keyboard) throws IOException {
Page page = null;
boolean shiftPressed = false;
boolean ctrlPressed = false;
boolean altPressed = false;
List specialKeys = null;
final List