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 xlt Show documentation
Show all versions of xlt Show documentation
XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.
/*
* Copyright (c) 2002-2021 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
* https://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.FORM_SUBMISSION_FORM_ATTRIBUTE;
import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLELEMENT_DETACH_ACTIVE_TRIGGERS_NO_KEYUP_EVENT;
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.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
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.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.html.impl.SelectableTextInput;
import com.gargoylesoftware.htmlunit.javascript.host.dom.Document;
import com.gargoylesoftware.htmlunit.javascript.host.dom.MutationObserver;
import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
import com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget;
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
* @author Ronny Shapiro
*/
public abstract class HtmlElement extends DomElement {
/**
* Enum for the different display styles.
*/
public enum DisplayStyle {
/** Empty string. */
EMPTY(""),
/** none. */
NONE("none"),
/** block. */
BLOCK("block"),
/** contents. */
CONTENTS("contents"),
/** 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_;
}
}
/**
* 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);
private static final String ATTRIBUTE_REQUIRED = "required";
/** The listeners which are to be notified of attribute changes. */
private Collection attributeListeners_;
/** The owning form for lost form children. */
private HtmlForm owningForm_;
private boolean shiftPressed_;
private boolean ctrlPressed_;
private boolean altPressed_;
/**
* 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) {
this(Html.XHTML_NAMESPACE, qualifiedName, page, attributes);
}
/**
* Creates an instance of a DOM element that can have a namespace.
*
* @param namespaceURI the URI that identifies an XML namespace
* @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 namespaceURI, final String qualifiedName, final SgmlPage page,
final Map attributes) {
super(namespaceURI, qualifiedName, page, attributes);
attributeListeners_ = new LinkedHashSet<>();
}
/**
* {@inheritDoc}
*/
@Override
protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
final String attributeValue, final boolean notifyAttributeChangeListeners,
final boolean notifyMutationObservers) {
// TODO: Clean up; this is a hack for HtmlElement living within an XmlPage.
if (null == getHtmlPageOrNull()) {
super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
notifyMutationObservers);
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);
}
final HtmlAttributeChangeEvent event;
if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
event = new HtmlAttributeChangeEvent(this, qualifiedName, attributeValue);
}
else {
event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
}
super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
notifyMutationObservers);
if (notifyAttributeChangeListeners) {
notifyAttributeChangeListeners(event, this, oldAttributeValue, notifyMutationObservers);
}
fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
}
/**
* Recursively notifies all {@link HtmlAttributeChangeListener}s.
* @param event the event
* @param element the element
* @param oldAttributeValue the old attribute value
* @param notifyMutationObservers whether to notify {@link MutationObserver}s or not
*/
protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event,
final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) {
final Collection listeners = element.attributeListeners_;
if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
synchronized (listeners) {
for (final HtmlAttributeChangeListener listener : listeners) {
if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
listener.attributeAdded(event);
}
}
}
}
else {
synchronized (listeners) {
for (final HtmlAttributeChangeListener listener : listeners) {
if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
listener.attributeReplaced(event);
}
}
}
}
final DomNode parentNode = element.getParentNode();
if (parentNode instanceof HtmlElement) {
notifyAttributeChangeListeners(event, (HtmlElement) parentNode, oldAttributeValue, notifyMutationObservers);
}
}
private void fireAttributeChangeImpl(final HtmlAttributeChangeEvent event,
final HtmlPage htmlPage, final boolean mappedElement, final String oldAttributeValue) {
if (mappedElement) {
htmlPage.addMappedElement(this);
}
if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
fireHtmlAttributeAdded(event);
htmlPage.fireHtmlAttributeAdded(event);
}
else {
fireHtmlAttributeReplaced(event);
htmlPage.fireHtmlAttributeReplaced(event);
}
}
/**
* 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 HtmlAttributeChangeEvent event;
if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
event = new HtmlAttributeChangeEvent(this, qualifiedName, attribute.getValue());
}
else {
event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
}
notifyAttributeChangeListeners(event, this, oldAttributeValue, true);
final Attr result = super.setAttributeNode(attribute);
fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
return result;
}
/**
* Removes an attribute specified by name from this element.
* @param attributeName the attribute attributeName
*/
@Override
public 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);
}
super.removeAttribute(attributeName);
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) {
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) {
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))
.append(':')
.append(getLocalName().toLowerCase(Locale.ROOT));
return name.toString();
}
return getLocalName().toLowerCase(Locale.ROOT);
}
/**
* 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 = getAttributeDirect("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() {
final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion();
if (browserVersion.hasFeature(FORM_SUBMISSION_FORM_ATTRIBUTE)) {
final String formId = getAttribute("form");
if (formId != ATTRIBUTE_NOT_DEFINED) {
final Element formById = getPage().getElementById(formId);
if (formById instanceof HtmlForm) {
return (HtmlForm) formById;
}
return null;
}
}
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
*/
public HtmlForm getEnclosingFormOrDie() {
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 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, true);
}
/**
* 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 startAtEnd whether typing should start at the text end or not
* @param lastType is this the last character to type
* @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
* @exception IOException if an IO error occurs
*/
private Page type(final char c, final boolean lastType)
throws IOException {
if (isDisabledElementAndDisabled()) {
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, shiftPressed_);
final Event shiftDown;
final ScriptResult shiftDownResult;
if (isShiftNeeded) {
shiftDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, KeyboardEvent.DOM_VK_SHIFT,
true, ctrlPressed_, altPressed_);
shiftDownResult = fireEvent(shiftDown);
}
else {
shiftDown = null;
shiftDownResult = null;
}
final Event keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
final ScriptResult keyDownResult = fireEvent(keyDown);
if (!keyDown.isAborted(keyDownResult)) {
final Event keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
final ScriptResult keyPressResult = fireEvent(keyPress);
if ((shiftDown == null || !shiftDown.isAborted(shiftDownResult))
&& !keyPress.isAborted(keyPressResult)) {
doType(c, lastType);
}
}
final WebClient webClient = page.getWebClient();
if (this instanceof HtmlTextInput
|| this instanceof HtmlTextArea
|| this instanceof HtmlTelInput
|| this instanceof HtmlNumberInput
|| this instanceof HtmlSearchInput
|| this instanceof HtmlPasswordInput) {
fireEvent(new KeyboardEvent(this, Event.TYPE_INPUT, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_));
}
HtmlElement eventSource = this;
if (!isAttachedToPage()) {
final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
if (browserVersion.hasFeature(HTMLELEMENT_DETACH_ACTIVE_TRIGGERS_NO_KEYUP_EVENT)) {
eventSource = null;
}
else {
eventSource = page.getBody();
}
}
if (eventSource != null) {
final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
eventSource.fireEvent(keyUp);
if (isShiftNeeded) {
final Event shiftUp = new KeyboardEvent(this, Event.TYPE_KEY_UP,
KeyboardEvent.DOM_VK_SHIFT,
false, ctrlPressed_, altPressed_);
eventSource.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
*/
public Page type(final int keyCode) {
return type(keyCode, true, 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;
final List