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

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

Go to download

XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.

There is a newer version: 8.1.0
Show newest version
/*
 * Copyright (c) 2002-2021 Gargoyle Software Inc.
 * Copyright (c) 2005-2021 Xceptance Software Technologies GmbH
 *
 * 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.EVENT_ONCLICK_USES_POINTEREVENT;
import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_AREA_WITHOUT_HREF_FOCUSABLE;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.TypeInfo;

import com.gargoylesoftware.css.parser.CSSException;
import com.gargoylesoftware.css.parser.selector.Selector;
import com.gargoylesoftware.css.parser.selector.SelectorList;
import com.gargoylesoftware.css.parser.selector.SelectorSpecificity;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.Page;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.SgmlPage;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.css.StyleElement;
import com.gargoylesoftware.htmlunit.javascript.AbstractJavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.HtmlUnitContextFactory;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
import com.gargoylesoftware.htmlunit.javascript.host.css.CSSStyleSheet;
import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
import com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget;
import com.gargoylesoftware.htmlunit.javascript.host.event.MouseEvent;
import com.gargoylesoftware.htmlunit.javascript.host.event.PointerEvent;
import com.gargoylesoftware.htmlunit.util.StringUtils;

/**
 * @author Ahmed Ashour
 * @author Marc Guillemot
 * @author Tom Anderson
 * @author Ronald Brill
 * @author Frank Danek
 */
public class DomElement extends DomNamespaceNode implements Element {

    private static final Log LOG = LogFactory.getLog(DomElement.class);

    /** src. */
    public static final String SRC_ATTRIBUTE = "src";

    /** Constant meaning that the specified attribute was not defined. */
    public static final String ATTRIBUTE_NOT_DEFINED = new String("");

    /** Constant meaning that the specified attribute was found but its value was empty. */
    public static final String ATTRIBUTE_VALUE_EMPTY = new String();

    /** The map holding the attributes, keyed by name. */
    private NamedAttrNodeMapImpl attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive());

    /** The map holding the namespaces, keyed by URI. */
    private final Map namespaces_ = new HashMap<>();

    /** Cache for the styles. */
    private String styleString_;
    private Map styleMap_;

    /**
     * Whether the Mouse is currently over this element or not.
     */
    private boolean mouseOver_;

    /**
     * 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.
     */
    public DomElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
            final Map attributes) {
        super(namespaceURI, qualifiedName, page);
        if (attributes != null && !attributes.isEmpty()) {
            attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive(), attributes);
            for (final DomAttr entry : attributes_.values()) {
                entry.setParentNode(this);
                final String attrNamespaceURI = entry.getNamespaceURI();
                final String prefix = entry.getPrefix();
                if (attrNamespaceURI != null && prefix != null) {
                    namespaces_.put(attrNamespaceURI, prefix);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getNodeName() {
        return getQualifiedName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final short getNodeType() {
        return ELEMENT_NODE;
    }

    /**
     * Returns the tag name of this element.
     * @return the tag name of this element
     */
    @Override
    public final String getTagName() {
        return getNodeName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final boolean hasAttributes() {
        return !attributes_.isEmpty();
    }

    /**
     * Returns whether the attribute specified by name has a value.
     *
     * @param attributeName the name of the attribute
     * @return true if an attribute with the given name is specified on this element or has a
     * default value, false otherwise.
     */
    @Override
    public boolean hasAttribute(final String attributeName) {
        return attributes_.containsKey(attributeName);
    }

    /**
     * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Replaces the value of the named style attribute. If there is no style attribute with the * specified name, a new one is added. If the specified value is an empty (or all whitespace) * string, this method actually removes the named style attribute. * @param name the attribute name (delimiter-separated, not camel-cased) * @param value the attribute value * @param priority the new priority of the property; "important"or the empty string if none. */ public void replaceStyleAttribute(final String name, final String value, final String priority) { if (org.apache.commons.lang3.StringUtils.isBlank(value)) { removeStyleAttribute(name); return; } final Map styleMap = getStyleMap(); final StyleElement old = styleMap.get(name); final StyleElement element; if (old == null) { element = new StyleElement(name, value, priority, SelectorSpecificity.FROM_STYLE_ATTRIBUTE); } else { element = new StyleElement(name, value, priority, SelectorSpecificity.FROM_STYLE_ATTRIBUTE, old.getIndex()); } styleMap.put(name, element); writeStyleToElement(styleMap); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Removes the specified style attribute, returning the value of the removed attribute. * @param name the attribute name (delimiter-separated, not camel-cased) * @return the removed value */ public String removeStyleAttribute(final String name) { final Map styleMap = getStyleMap(); final StyleElement value = styleMap.get(name); if (value == null) { return ""; } styleMap.remove(name); writeStyleToElement(styleMap); return value.getValue(); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Determines the StyleElement for the given name. * * @param name the name of the requested StyleElement * @return the StyleElement or null if not found */ public StyleElement getStyleElement(final String name) { final Map map = getStyleMap(); if (map != null) { return map.get(name); } return null; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Determines the StyleElement for the given name. * This ignores the case of the name. * * @param name the name of the requested StyleElement * @return the StyleElement or null if not found */ public StyleElement getStyleElementCaseInSensitive(final String name) { final Map map = getStyleMap(); for (final Map.Entry entry : map.entrySet()) { if (entry.getKey().equalsIgnoreCase(name)) { return entry.getValue(); } } return null; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Returns a sorted map containing style elements, keyed on style element name. We use a * {@link LinkedHashMap} map so that results are deterministic and are thus testable. * * @return a sorted map containing style elements, keyed on style element name */ public Map getStyleMap() { final String styleAttribute = getAttributeDirect("style"); if (styleString_ == styleAttribute) { return styleMap_; } final Map styleMap = new LinkedHashMap<>(); if (DomElement.ATTRIBUTE_NOT_DEFINED == styleAttribute || DomElement.ATTRIBUTE_VALUE_EMPTY == styleAttribute) { styleMap_ = styleMap; styleString_ = styleAttribute; return styleMap_; } // TODO this should be done by using cssparser also for (final String token : org.apache.commons.lang3.StringUtils.split(styleAttribute, ';')) { final int index = token.indexOf(':'); if (index != -1) { final String key = token.substring(0, index).trim().toLowerCase(Locale.ROOT); String value = token.substring(index + 1).trim(); String priority = ""; if (org.apache.commons.lang3.StringUtils.endsWithIgnoreCase(value, "!important")) { priority = StyleElement.PRIORITY_IMPORTANT; value = value.substring(0, value.length() - 10); value = value.trim(); } final StyleElement element = new StyleElement(key, value, priority, SelectorSpecificity.FROM_STYLE_ATTRIBUTE); styleMap.put(key, element); } } styleMap_ = styleMap; styleString_ = styleAttribute; return styleMap_; } /** * Prints the content between "<" and ">" (or "/>") in the output of the tag name * and its attributes in XML format. * @param printWriter the writer to print in */ protected void printOpeningTagContentAsXml(final PrintWriter printWriter) { printWriter.print(getTagName()); for (final Map.Entry entry : attributes_.entrySet()) { printWriter.print(" "); printWriter.print(entry.getKey()); printWriter.print("=\""); printWriter.print(StringUtils.escapeXmlAttributeValue(entry.getValue().getNodeValue())); printWriter.print("\""); } } /** * Recursively write the XML data for the node tree starting at node. * * @param indent white space to indent child nodes * @param printWriter writer where child nodes are written */ @Override protected void printXml(final String indent, final PrintWriter printWriter) { final boolean hasChildren = getFirstChild() != null; printWriter.print(indent + "<"); printOpeningTagContentAsXml(printWriter); if (hasChildren || isEmptyXmlTagExpanded()) { printWriter.print(">\r\n"); printChildrenAsXml(indent, printWriter); printWriter.print(indent); printWriter.print("\r\n"); } else { printWriter.print("/>\r\n"); } } /** * Indicates if a node without children should be written in expanded form as XML * (i.e. with closing tag rather than with "/>") * @return {@code false} by default */ protected boolean isEmptyXmlTagExpanded() { return false; } /** * Returns the qualified name (prefix:local) for the specified namespace and local name, * or {@code null} if the specified namespace URI does not exist. * * @param namespaceURI the URI that identifies an XML namespace * @param localName the name within the namespace * @return the qualified name for the specified namespace and local name */ String getQualifiedName(final String namespaceURI, final String localName) { final String qualifiedName; if (namespaceURI == null) { qualifiedName = localName; } else { final String prefix = namespaces_.get(namespaceURI); if (prefix == null) { qualifiedName = null; } else { qualifiedName = prefix + ':' + localName; } } return qualifiedName; } /** * Returns the value of the attribute specified by name or an empty string. If the * result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED} * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the * attribute was specified but it was empty. * * @param attributeName the name of the attribute * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY} */ @Override public String getAttribute(final String attributeName) { final DomAttr attr = attributes_.get(attributeName); if (attr != null) { return attr.getNodeValue(); } return ATTRIBUTE_NOT_DEFINED; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * @param attributeName the name of the attribute * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY} */ public String getAttributeDirect(final String attributeName) { final DomAttr attr = attributes_.getDirect(attributeName); if (attr != null) { return attr.getNodeValue(); } return ATTRIBUTE_NOT_DEFINED; } /** * Removes an attribute specified by name from this element. * @param attributeName the attribute attributeName */ @Override public void removeAttribute(final String attributeName) { attributes_.remove(attributeName); } /** * Removes an attribute specified by namespace and local name from this element. * @param namespaceURI the URI that identifies an XML namespace * @param localName the name within the namespace */ @Override public final void removeAttributeNS(final String namespaceURI, final String localName) { final String qualifiedName = getQualifiedName(namespaceURI, localName); if (qualifiedName != null) { removeAttribute(qualifiedName); } } /** * {@inheritDoc} * Not yet implemented. */ @Override public final Attr removeAttributeNode(final Attr attribute) { throw new UnsupportedOperationException("DomElement.removeAttributeNode is not yet implemented."); } /** * Returns whether the attribute specified by namespace and local name has a value. * * @param namespaceURI the URI that identifies an XML namespace * @param localName the name within the namespace * @return true if an attribute with the given name is specified on this element or has a * default value, false otherwise. */ @Override public final boolean hasAttributeNS(final String namespaceURI, final String localName) { final String qualifiedName = getQualifiedName(namespaceURI, localName); if (qualifiedName != null) { return attributes_.get(qualifiedName) != null; } return false; } /** * Returns the map holding the attributes, keyed by name. * @return the attributes map */ public final Map getAttributesMap() { return attributes_; } /** * {@inheritDoc} */ @Override public NamedNodeMap getAttributes() { return attributes_; } /** * Sets the value of the attribute specified by name. * * @param attributeName the name of the attribute * @param attributeValue the value of the attribute */ @Override public void setAttribute(final String attributeName, final String attributeValue) { setAttributeNS(null, attributeName, attributeValue); } /** * Sets the value of the attribute specified by namespace and qualified name. * * @param namespaceURI the URI that identifies an XML namespace * @param qualifiedName the qualified name (prefix:local) of the attribute * @param attributeValue the value of the attribute */ @Override public void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue) { setAttributeNS(namespaceURI, qualifiedName, attributeValue, true, true); } /** * Sets the value of the attribute specified by namespace and qualified name. * * @param namespaceURI the URI that identifies an XML namespace * @param qualifiedName the qualified name (prefix:local) of the attribute * @param attributeValue the value of the attribute * @param notifyAttributeChangeListeners to notify the associated {@link HtmlAttributeChangeListener}s * @param notifyMutationObservers to notify {@code MutationObserver}s or not */ protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue, final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) { final String value = attributeValue; final DomAttr newAttr = new DomAttr(getPage(), namespaceURI, qualifiedName, value, true); newAttr.setParentNode(this); attributes_.put(qualifiedName, newAttr); if (namespaceURI != null) { namespaces_.put(namespaceURI, newAttr.getPrefix()); } } /** * Indicates if the attribute names are case sensitive. * @return {@code true} */ protected boolean isAttributeCaseSensitive() { return true; } /** * Returns the value of the attribute specified by namespace and local name or an empty * string. If the result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED} * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the * attribute was specified but it was empty. * * @param namespaceURI the URI that identifies an XML namespace * @param localName the name within the namespace * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY} */ @Override public final String getAttributeNS(final String namespaceURI, final String localName) { final String qualifiedName = getQualifiedName(namespaceURI, localName); if (qualifiedName != null) { return getAttribute(qualifiedName); } return ATTRIBUTE_NOT_DEFINED; } /** * {@inheritDoc} */ @Override public DomAttr getAttributeNode(final String name) { return attributes_.get(name); } /** * {@inheritDoc} */ @Override public DomAttr getAttributeNodeNS(final String namespaceURI, final String localName) { final String qualifiedName = getQualifiedName(namespaceURI, localName); if (qualifiedName != null) { return attributes_.get(qualifiedName); } return null; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * @param styleMap the styles */ public void writeStyleToElement(final Map styleMap) { final StringBuilder builder = new StringBuilder(); final SortedSet sortedValues = new TreeSet<>(styleMap.values()); for (final StyleElement e : sortedValues) { if (builder.length() != 0) { builder.append(' '); } builder.append(e.getName()); builder.append(": "); builder.append(e.getValue()); final String prio = e.getPriority(); if (org.apache.commons.lang3.StringUtils.isNotBlank(prio)) { builder.append(" !"); builder.append(prio); } builder.append(';'); } final String value = builder.toString(); setAttribute("style", value); } /** * {@inheritDoc} */ @Override public DomNodeList getElementsByTagName(final String tagName) { return getElementsByTagNameImpl(tagName); } /** * This should be {@link #getElementsByTagName(String)}, but is separate because of the type erasure in Java. * @param tagName The name of the tag to match on * @return A list of matching elements. */ DomNodeList getElementsByTagNameImpl(final String tagName) { return new AbstractDomNodeList(this) { @Override @SuppressWarnings("unchecked") protected List provideElements() { final List res = new LinkedList<>(); for (final HtmlElement elem : getDomNode().getHtmlElementDescendants()) { if (elem.getLocalName().equalsIgnoreCase(tagName)) { res.add((E) elem); } } return res; } }; } /** * {@inheritDoc} * Not yet implemented. */ @Override public DomNodeList getElementsByTagNameNS(final String namespace, final String localName) { throw new UnsupportedOperationException("DomElement.getElementsByTagNameNS is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public TypeInfo getSchemaTypeInfo() { throw new UnsupportedOperationException("DomElement.getSchemaTypeInfo is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public void setIdAttribute(final String name, final boolean isId) { throw new UnsupportedOperationException("DomElement.setIdAttribute is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) { throw new UnsupportedOperationException("DomElement.setIdAttributeNS is not yet implemented."); } /** * {@inheritDoc} */ @Override public Attr setAttributeNode(final Attr attribute) { attributes_.setNamedItem(attribute); return null; } /** * {@inheritDoc} * Not yet implemented. */ @Override public Attr setAttributeNodeNS(final Attr attribute) { throw new UnsupportedOperationException("DomElement.setAttributeNodeNS is not yet implemented."); } /** * {@inheritDoc} * Not yet implemented. */ @Override public final void setIdAttributeNode(final Attr idAttr, final boolean isId) { throw new UnsupportedOperationException("DomElement.setIdAttributeNode is not yet implemented."); } /** * {@inheritDoc} */ @Override public DomNode cloneNode(final boolean deep) { final DomElement clone = (DomElement) super.cloneNode(deep); clone.attributes_ = new NamedAttrNodeMapImpl(clone, isAttributeCaseSensitive()); clone.attributes_.putAll(attributes_); return clone; } /** * @return the identifier of this element */ public final String getId() { return getAttributeDirect("id"); } /** * Sets the identifier this element. * * @param newId the new identifier of this element */ public final void setId(final String newId) { setAttribute("id", newId); } /** * Returns the first child element node of this element. null if this element has no child elements. * @return the first child element node of this element. null if this element has no child elements */ public DomElement getFirstElementChild() { final Iterator i = getChildElements().iterator(); if (i.hasNext()) { return i.next(); } return null; } /** * Returns the last child element node of this element. null if this element has no child elements. * @return the last child element node of this element. null if this element has no child elements */ public DomElement getLastElementChild() { DomElement lastChild = null; final Iterator i = getChildElements().iterator(); while (i.hasNext()) { lastChild = i.next(); } return lastChild; } /** * Returns the current number of element nodes that are children of this element. * @return the current number of element nodes that are children of this element. */ public int getChildElementCount() { int counter = 0; for (final Iterator i = getChildElements().iterator(); i.hasNext(); i.next()) { counter++; } return counter; } /** * @return an Iterable over the DomElement children of this object, i.e. excluding the non-element nodes */ public final Iterable getChildElements() { return new ChildElementsIterable(this); } /** * An Iterable over the DomElement children. */ private static class ChildElementsIterable implements Iterable { private final Iterator iterator_; /** Constructor. * @param domNode the parent */ protected ChildElementsIterable(final DomNode domNode) { iterator_ = new ChildElementsIterator(domNode); } @Override public Iterator iterator() { return iterator_; } } /** * An iterator over the DomElement children. */ protected static class ChildElementsIterator implements Iterator { private DomElement nextElement_; /** Constructor. * @param domNode the parent */ protected ChildElementsIterator(final DomNode domNode) { final DomNode child = domNode.getFirstChild(); if (child != null) { if (child instanceof DomElement) { nextElement_ = (DomElement) child; } else { setNextElement(child); } } } /** @return is there a next one ? */ @Override public boolean hasNext() { return nextElement_ != null; } /** @return the next one */ @Override public DomElement next() { if (nextElement_ != null) { final DomElement result = nextElement_; setNextElement(nextElement_); return result; } throw new NoSuchElementException(); } /** Removes the current one. */ @Override public void remove() { if (nextElement_ == null) { throw new IllegalStateException(); } final DomNode sibling = nextElement_.getPreviousSibling(); if (sibling != null) { sibling.remove(); } } private void setNextElement(final DomNode node) { DomNode next = node.getNextSibling(); while (next != null && !(next instanceof DomElement)) { next = next.getNextSibling(); } nextElement_ = (DomElement) next; } } /** * Returns a string representation of this element. * @return a string representation of this element */ @Override public String toString() { final StringWriter writer = new StringWriter(); final PrintWriter printWriter = new PrintWriter(writer); printWriter.print(getClass().getSimpleName()); printWriter.print("[<"); printOpeningTagContentAsXml(printWriter); printWriter.print(">]"); printWriter.flush(); return writer.toString(); } /** * Simulates clicking on this element, returning the page in the window that has the focus * after the element has been clicked. Note that the returned page may or may not be the same * as the original page, depending on the type of element being clicked, the presence of JavaScript * action listeners, etc.
* This only clicks the element if it is visible and enabled (isDisplayed() & !isDisabled()). * In case the element is not visible and/or disabled, only a log output is generated. *
* If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false) * * @param

the page type * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()} * @exception IOException if an IO error occurs */ public

P click() throws IOException { return click(false, false, false); } /** * Simulates clicking on this element, returning the page in the window that has the focus * after the element has been clicked. Note that the returned page may or may not be the same * as the original page, depending on the type of element being clicked, the presence of JavaScript * action listeners, etc.
* This only clicks the element if it is visible and enabled (isDisplayed() & !isDisabled()). * In case the element is not visible and/or disabled, only a log output is generated. *
* If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false) * * @param shiftKey {@code true} if SHIFT is pressed during the click * @param ctrlKey {@code true} if CTRL is pressed during the click * @param altKey {@code true} if ALT is pressed during the click * @param

the page type * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()} * @exception IOException if an IO error occurs */ public

P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey) throws IOException { return click(shiftKey, ctrlKey, altKey, true); } /** * Simulates clicking on this element, returning the page in the window that has the focus * after the element has been clicked. Note that the returned page may or may not be the same * as the original page, depending on the type of element being clicked, the presence of JavaScript * action listeners, etc.
* This only clicks the element if it is visible and enabled (isDisplayed() & !isDisabled()). * In case the element is not visible and/or disabled, only a log output is generated. *
* If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false) * * @param shiftKey {@code true} if SHIFT is pressed during the click * @param ctrlKey {@code true} if CTRL is pressed during the click * @param altKey {@code true} if ALT is pressed during the click * @param triggerMouseEvents if true trigger the mouse events also * @param

the page type * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()} * @exception IOException if an IO error occurs */ public

P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final boolean triggerMouseEvents) throws IOException { return click(shiftKey, ctrlKey, altKey, triggerMouseEvents, false, false); } /** * @return true if this is an {@link DisabledElement} and disabled */ protected boolean isDisabledElementAndDisabled() { return this instanceof DisabledElement && ((DisabledElement) this).isDisabled(); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Simulates clicking on this element, returning the page in the window that has the focus * after the element has been clicked. Note that the returned page may or may not be the same * as the original page, depending on the type of element being clicked, the presence of JavaScript * action listeners, etc. * * @param shiftKey {@code true} if SHIFT is pressed during the click * @param ctrlKey {@code true} if CTRL is pressed during the click * @param altKey {@code true} if ALT is pressed during the click * @param triggerMouseEvents if true trigger the mouse events also * @param ignoreVisibility whether to ignore visibility or not * @param disableProcessLabelAfterBubbling ignore label processing * @param

the page type * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()} * @exception IOException if an IO error occurs */ @SuppressWarnings("unchecked") public

P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final boolean triggerMouseEvents, final boolean ignoreVisibility, final boolean disableProcessLabelAfterBubbling) throws IOException { // make enclosing window the current one final SgmlPage page = getPage(); page.getWebClient().setCurrentWindow(page.getEnclosingWindow()); if (!ignoreVisibility) { if (!(page instanceof HtmlPage)) { return (P) page; } // #2896 start /* if (!isDisplayed()) { if (LOG.isWarnEnabled()) { LOG.warn("Calling click() ignored because the target element '" + toString() + "' is not displayed."); } return (P) page; } */ // #2896 end if (isDisabledElementAndDisabled()) { if (LOG.isWarnEnabled()) { LOG.warn("Calling click() ignored because the target element '" + toString() + "' is disabled."); } return (P) page; } } synchronized (page) { if (triggerMouseEvents) { mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT); } // give focus to current element (if possible) or only remove it from previous one DomElement elementToFocus = null; if (this instanceof SubmittableElement || this instanceof HtmlAnchor && ((HtmlAnchor) this).getHrefAttribute() != DomElement.ATTRIBUTE_NOT_DEFINED || this instanceof HtmlArea && (((HtmlArea) this).getHrefAttribute() != DomElement.ATTRIBUTE_NOT_DEFINED || getPage().getWebClient().getBrowserVersion().hasFeature(JS_AREA_WITHOUT_HREF_FOCUSABLE)) || this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null) { elementToFocus = this; } else if (this instanceof HtmlOption) { elementToFocus = ((HtmlOption) this).getEnclosingSelect(); } if (elementToFocus == null) { ((HtmlPage) page).setFocusedElement(null); } else { elementToFocus.focus(); } if (triggerMouseEvents) { mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT); } MouseEvent event = null; if (page.getWebClient().isJavaScriptEnabled()) { if (page.getWebClient().getBrowserVersion().hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) { event = new PointerEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT); } else { event = new MouseEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT); } if (disableProcessLabelAfterBubbling) { event.disableProcessLabelAfterBubbling(); } } return click(event, shiftKey, ctrlKey, altKey, ignoreVisibility); } } /** * Returns the event target element. This could be overridden by subclasses to have other targets. * The default implementation returns 'this'. * @return the event target element. */ protected DomNode getEventTargetElement() { return this; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Simulates clicking on this element, returning the page in the window that has the focus * after the element has been clicked. Note that the returned page may or may not be the same * as the original page, depending on the type of element being clicked, the presence of JavaScript * action listeners, etc. * * @param event the click event used * @param shiftKey {@code true} if SHIFT is pressed during the click * @param ctrlKey {@code true} if CTRL is pressed during the click * @param altKey {@code true} if ALT is pressed during the click * @param ignoreVisibility whether to ignore visibility or not * @param

the page type * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()} * @exception IOException if an IO error occurs */ @SuppressWarnings("unchecked") public

P click(final Event event, final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final boolean ignoreVisibility) throws IOException { final SgmlPage page = getPage(); // #2896 start /* if ((!ignoreVisibility && !isDisplayed()) || isDisabledElementAndDisabled()) { */ if (isDisabledElementAndDisabled()) { // #2896 end return (P) page; } if (!page.getWebClient().isJavaScriptEnabled()) { doClickStateUpdate(shiftKey, ctrlKey); page.getWebClient().loadDownloadedResponses(); return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage(); } // may be different from page when working with "orphaned pages" // (ex: clicking a link in a page that is not active anymore) final Page contentPage = page.getEnclosingWindow().getEnclosedPage(); boolean stateUpdated = false; boolean changed = false; if (isStateUpdateFirst()) { changed = doClickStateUpdate(shiftKey, ctrlKey); stateUpdated = true; } final AbstractJavaScriptEngine jsEngine = page.getWebClient().getJavaScriptEngine(); jsEngine.holdPosponedActions(); try { final ScriptResult scriptResult = doClickFireClickEvent(event); final boolean eventIsAborted = event.isAborted(scriptResult); final boolean pageAlreadyChanged = contentPage != page.getEnclosingWindow().getEnclosedPage(); if (!pageAlreadyChanged && !stateUpdated && !eventIsAborted) { changed = doClickStateUpdate(shiftKey, ctrlKey); } } finally { jsEngine.processPostponedActions(); } if (changed) { doClickFireChangeEvent(); } return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage(); } /** * This method implements the control state update part of the click action. * *

The default implementation only calls doClickStateUpdate on parent's DomElement (if any). * Subclasses requiring different behavior (like {@link HtmlSubmitInput}) will override this method.

* @param shiftKey {@code true} if SHIFT is pressed * @param ctrlKey {@code true} if CTRL is pressed * * @return true if doClickFireEvent method has to be called later on (to signal, * that the value was changed) * @throws IOException if an IO error occurs */ protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException { if (propagateClickStateUpdateToParent()) { // needed for instance to perform link doClickAction when a nested element is clicked // it should probably be changed to do this at the event level but currently // this wouldn't work with JS disabled as events are propagated in the host object tree. final DomNode parent = getParentNode(); if (parent instanceof DomElement) { return ((DomElement) parent).doClickStateUpdate(false, false); } } return false; } /** * @see #doClickStateUpdate(boolean, boolean) * Usually the click is propagated to the parent. Overwrite if you * like to disable this. * * @return true or false */ protected boolean propagateClickStateUpdateToParent() { return true; } /** * This method implements the control onchange handler call during the click action. */ protected void doClickFireChangeEvent() { // nothing to do, in the default case } /** * This method implements the control onclick handler call during the click action. * @param event the click event used * @return the script result */ protected ScriptResult doClickFireClickEvent(final Event event) { return fireEvent(event); } /** * Simulates double-clicking on this element, returning the page in the window that has the focus * after the element has been clicked. Note that the returned page may or may not be the same * as the original page, depending on the type of element being clicked, the presence of JavaScript * action listeners, etc. Note also that {@link #click()} is automatically called first. * * @param

the page type * @return the page that occupies this element's window after the element has been double-clicked * @exception IOException if an IO error occurs */ public

P dblClick() throws IOException { return dblClick(false, false, false); } /** * Simulates double-clicking on this element, returning the page in the window that has the focus * after the element has been clicked. Note that the returned page may or may not be the same * as the original page, depending on the type of element being clicked, the presence of JavaScript * action listeners, etc. Note also that {@link #click(boolean, boolean, boolean)} is automatically * called first. * * @param shiftKey {@code true} if SHIFT is pressed during the double-click * @param ctrlKey {@code true} if CTRL is pressed during the double-click * @param altKey {@code true} if ALT is pressed during the double-click * @param

the page type * @return the page that occupies this element's window after the element has been double-clicked * @exception IOException if an IO error occurs */ @SuppressWarnings("unchecked") public

P dblClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey) throws IOException { if (isDisabledElementAndDisabled()) { return (P) getPage(); } // call click event first P clickPage = click(shiftKey, ctrlKey, altKey); if (clickPage != getPage()) { if (LOG.isDebugEnabled()) { LOG.debug("dblClick() is ignored, as click() loaded a different page."); } return clickPage; } // call click event a second time clickPage = click(shiftKey, ctrlKey, altKey); if (clickPage != getPage()) { if (LOG.isDebugEnabled()) { LOG.debug("dblClick() is ignored, as click() loaded a different page."); } return clickPage; } final Event event; if (getPage().getWebClient().getBrowserVersion().hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) { event = new PointerEvent(this, MouseEvent.TYPE_DBL_CLICK, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT); } else { event = new MouseEvent(this, MouseEvent.TYPE_DBL_CLICK, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT); } final ScriptResult scriptResult = fireEvent(event); if (scriptResult == null) { return clickPage; } return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage(); } /** * Simulates moving the mouse over this element, returning the page which this element's window contains * after the mouse move. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @return the page which this element's window contains after the mouse move */ public Page mouseOver() { return mouseOver(false, false, false, MouseEvent.BUTTON_LEFT); } /** * Simulates moving the mouse over this element, returning the page which this element's window contains * after the mouse move. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @param shiftKey {@code true} if SHIFT is pressed during the mouse move * @param ctrlKey {@code true} if CTRL is pressed during the mouse move * @param altKey {@code true} if ALT is pressed during the mouse move * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE} * or {@link MouseEvent#BUTTON_RIGHT} * @return the page which this element's window contains after the mouse move */ public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) { return doMouseEvent(MouseEvent.TYPE_MOUSE_OVER, shiftKey, ctrlKey, altKey, button); } /** * Simulates moving the mouse over this element, returning the page which this element's window contains * after the mouse move. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @return the page which this element's window contains after the mouse move */ public Page mouseMove() { return mouseMove(false, false, false, MouseEvent.BUTTON_LEFT); } /** * Simulates moving the mouse over this element, returning the page which this element's window contains * after the mouse move. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @param shiftKey {@code true} if SHIFT is pressed during the mouse move * @param ctrlKey {@code true} if CTRL is pressed during the mouse move * @param altKey {@code true} if ALT is pressed during the mouse move * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE} * or {@link MouseEvent#BUTTON_RIGHT} * @return the page which this element's window contains after the mouse move */ public Page mouseMove(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) { return doMouseEvent(MouseEvent.TYPE_MOUSE_MOVE, shiftKey, ctrlKey, altKey, button); } /** * Simulates moving the mouse out of this element, returning the page which this element's window contains * after the mouse move. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @return the page which this element's window contains after the mouse move */ public Page mouseOut() { return mouseOut(false, false, false, MouseEvent.BUTTON_LEFT); } /** * Simulates moving the mouse out of this element, returning the page which this element's window contains * after the mouse move. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @param shiftKey {@code true} if SHIFT is pressed during the mouse move * @param ctrlKey {@code true} if CTRL is pressed during the mouse move * @param altKey {@code true} if ALT is pressed during the mouse move * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE} * or {@link MouseEvent#BUTTON_RIGHT} * @return the page which this element's window contains after the mouse move */ public Page mouseOut(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) { return doMouseEvent(MouseEvent.TYPE_MOUSE_OUT, shiftKey, ctrlKey, altKey, button); } /** * Simulates clicking the mouse on this element, returning the page which this element's window contains * after the mouse click. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @return the page which this element's window contains after the mouse click */ public Page mouseDown() { return mouseDown(false, false, false, MouseEvent.BUTTON_LEFT); } /** * Simulates clicking the mouse on this element, returning the page which this element's window contains * after the mouse click. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @param shiftKey {@code true} if SHIFT is pressed during the mouse click * @param ctrlKey {@code true} if CTRL is pressed during the mouse click * @param altKey {@code true} if ALT is pressed during the mouse click * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE} * or {@link MouseEvent#BUTTON_RIGHT} * @return the page which this element's window contains after the mouse click */ public Page mouseDown(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) { return doMouseEvent(MouseEvent.TYPE_MOUSE_DOWN, shiftKey, ctrlKey, altKey, button); } /** * Simulates releasing the mouse click on this element, returning the page which this element's window contains * after the mouse click release. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @return the page which this element's window contains after the mouse click release */ public Page mouseUp() { return mouseUp(false, false, false, MouseEvent.BUTTON_LEFT); } /** * Simulates releasing the mouse click on this element, returning the page which this element's window contains * after the mouse click release. The returned page may or may not be the same as the original page, depending * on JavaScript event handlers, etc. * * @param shiftKey {@code true} if SHIFT is pressed during the mouse click release * @param ctrlKey {@code true} if CTRL is pressed during the mouse click release * @param altKey {@code true} if ALT is pressed during the mouse click release * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE} * or {@link MouseEvent#BUTTON_RIGHT} * @return the page which this element's window contains after the mouse click release */ public Page mouseUp(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) { return doMouseEvent(MouseEvent.TYPE_MOUSE_UP, shiftKey, ctrlKey, altKey, button); } /** * Simulates right clicking the mouse on this element, returning the page which this element's window * contains after the mouse click. The returned page may or may not be the same as the original page, * depending on JavaScript event handlers, etc. * * @return the page which this element's window contains after the mouse click */ public Page rightClick() { return rightClick(false, false, false); } /** * Simulates right clicking the mouse on this element, returning the page which this element's window * contains after the mouse click. The returned page may or may not be the same as the original page, * depending on JavaScript event handlers, etc. * * @param shiftKey {@code true} if SHIFT is pressed during the mouse click * @param ctrlKey {@code true} if CTRL is pressed during the mouse click * @param altKey {@code true} if ALT is pressed during the mouse click * @return the page which this element's window contains after the mouse click */ public Page rightClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey) { final Page mouseDownPage = mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT); if (mouseDownPage != getPage()) { if (LOG.isDebugEnabled()) { LOG.debug("rightClick() is incomplete, as mouseDown() loaded a different page."); } return mouseDownPage; } final Page mouseUpPage = mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT); if (mouseUpPage != getPage()) { if (LOG.isDebugEnabled()) { LOG.debug("rightClick() is incomplete, as mouseUp() loaded a different page."); } return mouseUpPage; } return doMouseEvent(MouseEvent.TYPE_CONTEXT_MENU, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT); } /** * Simulates the specified mouse event, returning the page which this element's window contains after the event. * The returned page may or may not be the same as the original page, depending on JavaScript event handlers, etc. * * @param eventType the mouse event type to simulate * @param shiftKey {@code true} if SHIFT is pressed during the mouse event * @param ctrlKey {@code true} if CTRL is pressed during the mouse event * @param altKey {@code true} if ALT is pressed during the mouse event * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE} * or {@link MouseEvent#BUTTON_RIGHT} * @return the page which this element's window contains after the event */ private Page doMouseEvent(final String eventType, final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) { final SgmlPage page = getPage(); if (!page.getWebClient().isJavaScriptEnabled()) { return page; } final ScriptResult scriptResult; final Event event; if (MouseEvent.TYPE_CONTEXT_MENU.equals(eventType) && getPage().getWebClient().getBrowserVersion().hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) { event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button); } else { event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button); } scriptResult = fireEvent(event); final Page currentPage; if (scriptResult == null) { currentPage = page; } else { currentPage = page.getWebClient().getCurrentWindow().getEnclosedPage(); } final boolean mouseOver = !MouseEvent.TYPE_MOUSE_OUT.equals(eventType); if (mouseOver_ != mouseOver) { mouseOver_ = mouseOver; final SimpleScriptable scriptable = getScriptableObject(); scriptable.getWindow().clearComputedStyles(); } return currentPage; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Shortcut for {@link #fireEvent(Event)}. * @param eventType the event type (like "load", "click") * @return the execution result, or {@code null} if nothing is executed */ public ScriptResult fireEvent(final String eventType) { if (getPage().getWebClient().isJavaScriptEnabled()) { return fireEvent(new Event(this, eventType)); } return null; } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Fires the event on the element. Nothing is done if JavaScript is disabled. * @param event the event to fire * @return the execution result, or {@code null} if nothing is executed */ public ScriptResult fireEvent(final Event event) { final WebClient client = getPage().getWebClient(); if (!client.isJavaScriptEnabled()) { return null; } if (!handles(event)) { return null; } if (LOG.isDebugEnabled()) { LOG.debug("Firing " + event); } final EventTarget jsElt = getScriptableObject(); final HtmlUnitContextFactory cf = ((JavaScriptEngine) client.getJavaScriptEngine()).getContextFactory(); final ScriptResult result = cf.callSecured(cx -> jsElt.fireEvent(event), getHtmlPageOrNull()); if (event.isAborted(result)) { preventDefault(); } return result; } /** * This method is called if the current fired event is canceled by preventDefault() in FireFox, * or by returning {@code false} in Internet Explorer. * * The default implementation does nothing. */ protected void preventDefault() { // Empty by default; override as needed. } /** * Sets the focus on this element. */ public void focus() { if (!(this instanceof SubmittableElement || this instanceof HtmlAnchor && ((HtmlAnchor) this).getHrefAttribute() != DomElement.ATTRIBUTE_NOT_DEFINED || this instanceof HtmlArea && (((HtmlArea) this).getHrefAttribute() != DomElement.ATTRIBUTE_NOT_DEFINED || getPage().getWebClient().getBrowserVersion().hasFeature(JS_AREA_WITHOUT_HREF_FOCUSABLE)) || this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null)) { return; } if (!isDisplayed() || isDisabledElementAndDisabled()) { return; } final HtmlPage page = (HtmlPage) getPage(); page.setFocusedElement(this); } /** * Removes focus from this element. */ public void blur() { final HtmlPage page = (HtmlPage) getPage(); if (page.getFocusedElement() != this) { return; } page.setFocusedElement(null); } /** * INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
* * Gets notified that it has lost the focus. */ public void removeFocus() { // nothing } /** * Returns {@code true} if state updates should be done before onclick event handling. This method * returns {@code false} by default, and is expected to be overridden to return {@code true} by * derived classes like {@link HtmlCheckBoxInput}. * @return {@code true} if state updates should be done before onclick event handling */ protected boolean isStateUpdateFirst() { return false; } /** * Returns whether the Mouse is currently over this element or not. * @return whether the Mouse is currently over this element or not */ public boolean isMouseOver() { if (mouseOver_) { return true; } for (final DomElement child : getChildElements()) { if (child.isMouseOver()) { return true; } } return false; } /** * Returns true if the element would be selected by the specified selector string; otherwise, returns false. * @param selectorString the selector to test * @return true if the element would be selected by the specified selector string; otherwise, returns false. */ public boolean matches(final String selectorString) { try { final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion(); final SelectorList selectorList = getSelectorList(selectorString, browserVersion); if (selectorList != null) { for (final Selector selector : selectorList) { if (CSSStyleSheet.selects(browserVersion, selector, this, null, true)) { return true; } } } return false; } catch (final IOException e) { throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage()); } } /** * {@inheritDoc} */ @Override public void setNodeValue(final String value) { // Default behavior is to do nothing, overridden in some subclasses } } /** * The {@link NamedNodeMap} to store the node attributes. */ class NamedAttrNodeMapImpl implements Map, NamedNodeMap, Serializable { protected static final NamedAttrNodeMapImpl EMPTY_MAP = new NamedAttrNodeMapImpl(); private static final DomAttr[] EMPTY_ARRAY = new DomAttr[0]; private final Map map_ = new LinkedHashMap<>(); private boolean dirty_ = false; private DomAttr[] attrPositions_ = EMPTY_ARRAY; private final DomElement domNode_; private final boolean caseSensitive_; // cache for avoid the usual identical lowercasing. At the same time, we unify the strings // and reduce the footprint // private static final ConcurrentLRUCache lowerCaseCache = new ConcurrentLRUCache(1001); private NamedAttrNodeMapImpl() { super(); domNode_ = null; caseSensitive_ = true; } NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive) { super(); if (domNode == null) { throw new IllegalArgumentException("Provided domNode can't be null."); } domNode_ = domNode; caseSensitive_ = caseSensitive; } NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive, final Map attributes) { this(domNode, caseSensitive); putAll(attributes); } /** * {@inheritDoc} */ @Override public int getLength() { return size(); } /** * {@inheritDoc} */ @Override public DomAttr getNamedItem(final String name) { return get(name); } private String fixName(final String name) { if (caseSensitive_) { return name; } // #584: temporarily commented out // // String lowerCasedString = lowerCaseCache.get(name); // if (lowerCasedString == null) // { // lowerCasedString = name.toLowerCase(); // lowerCaseCache.put(name, lowerCasedString); // } // return lowerCasedString; return name.toLowerCase(Locale.ROOT); } /** * {@inheritDoc} */ @Override public Node getNamedItemNS(final String namespaceURI, final String localName) { if (domNode_ == null) { return null; } return get(domNode_.getQualifiedName(namespaceURI, fixName(localName))); } /** * {@inheritDoc} */ @Override public Node item(final int index) { if (index < 0 || index >= map_.size()) { return null; } if (dirty_) { attrPositions_ = map_.values().toArray(attrPositions_); dirty_ = false; } return attrPositions_[index]; } /** * {@inheritDoc} */ @Override public Node removeNamedItem(final String name) throws DOMException { return remove(name); } /** * {@inheritDoc} */ @Override public Node removeNamedItemNS(final String namespaceURI, final String localName) { if (domNode_ == null) { return null; } return remove(domNode_.getQualifiedName(namespaceURI, fixName(localName))); } /** * {@inheritDoc} */ @Override public DomAttr setNamedItem(final Node node) { return put(node.getLocalName(), (DomAttr) node); } /** * {@inheritDoc} */ @Override public Node setNamedItemNS(final Node node) throws DOMException { return put(node.getNodeName(), (DomAttr) node); } /** * {@inheritDoc} */ @Override public DomAttr put(final String key, final DomAttr value) { final String name = fixName(key); dirty_ = true; return map_.put(name, value); } /** * {@inheritDoc} */ @Override public DomAttr remove(final Object key) { if (key instanceof String) { final String name = fixName((String) key); dirty_ = true; return map_.remove(name); } return null; } /** * {@inheritDoc} */ @Override public void clear() { dirty_ = true; map_.clear(); } /** * {@inheritDoc} */ @Override public void putAll(final Map t) { // add one after the other to save the positions for (final Map.Entry entry : t.entrySet()) { put(entry.getKey(), entry.getValue()); } } /** * {@inheritDoc} */ @Override public boolean containsKey(final Object key) { if (key instanceof String) { final String name = fixName((String) key); return map_.containsKey(name); } return false; } /** * {@inheritDoc} */ @Override public DomAttr get(final Object key) { if (key instanceof String) { final String name = fixName((String) key); return map_.get(name); } return null; } /** * Fast access. * @param key the key */ protected DomAttr getDirect(final String key) { return map_.get(key); } /** * {@inheritDoc} */ @Override public boolean containsValue(final Object value) { return map_.containsValue(value); } /** * {@inheritDoc} */ @Override public Set> entrySet() { return map_.entrySet(); } /** * {@inheritDoc} */ @Override public boolean isEmpty() { return map_.isEmpty(); } /** * {@inheritDoc} */ @Override public Set keySet() { return map_.keySet(); } /** * {@inheritDoc} */ @Override public int size() { return map_.size(); } /** * {@inheritDoc} */ @Override public Collection values() { return map_.values(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy