/projects/htmlunit-2.8/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/html/HTMLCollection.java
Java | 708 lines | 433 code | 65 blank | 210 comment | 115 complexity | 32d46c93b9ad66dcd80e95f15d41ca4c MD5 | raw file
- /*
- * Copyright (c) 2002-2010 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.javascript.host.html;
- import java.util.ArrayList;
- import java.util.List;
- import net.sourceforge.htmlunit.corejs.javascript.Context;
- import net.sourceforge.htmlunit.corejs.javascript.Function;
- import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
- import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
- import org.apache.commons.collections.CollectionUtils;
- import org.apache.commons.collections.Transformer;
- import org.apache.commons.collections.functors.NOPTransformer;
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.w3c.dom.Node;
- import org.w3c.dom.NodeList;
- import com.gargoylesoftware.htmlunit.BrowserVersion;
- import com.gargoylesoftware.htmlunit.BrowserVersionFeatures;
- import com.gargoylesoftware.htmlunit.WebWindow;
- import com.gargoylesoftware.htmlunit.html.DomChangeEvent;
- import com.gargoylesoftware.htmlunit.html.DomChangeListener;
- import com.gargoylesoftware.htmlunit.html.DomElement;
- import com.gargoylesoftware.htmlunit.html.DomNode;
- import com.gargoylesoftware.htmlunit.html.DomText;
- import com.gargoylesoftware.htmlunit.html.FrameWindow;
- import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeEvent;
- import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeListener;
- import com.gargoylesoftware.htmlunit.html.HtmlElement;
- import com.gargoylesoftware.htmlunit.html.HtmlNoScript;
- import com.gargoylesoftware.htmlunit.html.xpath.XPathUtils;
- import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
- import com.gargoylesoftware.htmlunit.javascript.configuration.JavaScriptConfiguration;
- import com.gargoylesoftware.htmlunit.javascript.host.Window;
- import com.gargoylesoftware.htmlunit.xml.XmlPage;
- /**
- * An array of elements. Used for the element arrays returned by <tt>document.all</tt>,
- * <tt>document.all.tags('x')</tt>, <tt>document.forms</tt>, <tt>window.frames</tt>, etc.
- * Note that this class must not be used for collections that can be modified, for example
- * <tt>map.areas</tt> and <tt>select.options</tt>.
- * <br>
- * This class (like all classes in this package) is specific for the JavaScript engine.
- * Users of HtmlUnit shouldn't use it directly.
- *
- * @version $Revision: 5864 $
- * @author Daniel Gredler
- * @author Marc Guillemot
- * @author Chris Erskine
- * @author Ahmed Ashour
- */
- public class HTMLCollection extends SimpleScriptable implements Function, NodeList {
- private static final long serialVersionUID = 4049916048017011764L;
- private static final Log LOG = LogFactory.getLog(HTMLCollection.class);
- private String xpath_;
- private DomNode node_;
- private boolean avoidObjectDetection_ = false;
- /**
- * The transformer used to get the element to return from the HTML element.
- * It returns the HTML element itself except for frames where it returns the nested window.
- */
- private Transformer transformer_;
- /**
- * Cache collection elements when possible, so as to avoid expensive XPath expression evaluations.
- */
- private List<Object> cachedElements_;
- /**
- * IE provides a way of enumerating through some element collections; this counter supports that functionality.
- */
- private int currentIndex_ = 0;
- /**
- * Creates an instance. JavaScript objects must have a default constructor.
- * Don't call.
- */
- @Deprecated
- public HTMLCollection() {
- // Empty.
- }
- /**
- * Creates an instance.
- * @param parentScope parent scope
- */
- public HTMLCollection(final DomNode parentScope) {
- this(parentScope.getScriptObject());
- }
- /**
- * Creates an instance.
- * @param parentScope parent scope
- */
- public HTMLCollection(final ScriptableObject parentScope) {
- setParentScope(parentScope);
- setPrototype(getPrototype(getClass()));
- }
- /**
- * Constructs an instance with an initial cache value.
- * @param parentScope the parent scope, on which we listen for changes
- * @param initialElements the initial content for the cache
- */
- HTMLCollection(final DomNode parentScope, final List<?> initialElements) {
- this(parentScope);
- init(parentScope, null);
- cachedElements_ = new ArrayList<Object>(initialElements);
- }
- /**
- * Only needed to make collections like <tt>document.all</tt> available but "invisible" when simulating Firefox.
- * {@inheritDoc}
- */
- @Override
- public boolean avoidObjectDetection() {
- return avoidObjectDetection_;
- }
- /**
- * @param newValue the new value
- */
- public void setAvoidObjectDetection(final boolean newValue) {
- avoidObjectDetection_ = newValue;
- }
- /**
- * Initializes the content of this collection. The elements will be "calculated" at each
- * access using the specified XPath expression, applied to the specified node.
- * @param node the node to serve as root for the XPath expression
- * @param xpath the XPath expression which determines the elements of the collection
- */
- public void init(final DomNode node, final String xpath) {
- init(node, xpath, NOPTransformer.INSTANCE);
- }
- /**
- * Initializes the content of this collection. The elements will be "calculated" at each
- * access using the specified XPath expression, applied to the specified node, and
- * transformed using the specified transformer.
- * @param node the node to serve as root for the XPath expression
- * @param xpath the XPath expression which determines the elements of the collection
- * @param transformer the transformer enabling the retrieval of the expected objects from
- * the results of the XPath evaluation
- */
- public void init(final DomNode node, final String xpath, final Transformer transformer) {
- if (node != null) {
- node_ = node;
- xpath_ = xpath;
- transformer_ = transformer;
- final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl();
- node_.addDomChangeListener(listener);
- if (node_ instanceof HtmlElement) {
- ((HtmlElement) node_).addHtmlAttributeChangeListener(listener);
- cachedElements_ = null;
- }
- }
- }
- /**
- * Initializes the collection. The elements will be "calculated" as the children of the node.
- * @param node the node to grab children from
- */
- public void initFromChildren(final DomNode node) {
- if (node != null) {
- node_ = node;
- final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl();
- node_.addDomChangeListener(listener);
- if (node_ instanceof HtmlElement) {
- ((HtmlElement) node_).addHtmlAttributeChangeListener(listener);
- cachedElements_ = null;
- }
- }
- transformer_ = NOPTransformer.INSTANCE;
- }
- /**
- * {@inheritDoc}
- */
- public final Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
- if (args.length == 0) {
- throw Context.reportRuntimeError("Zero arguments; need an index or a key.");
- }
- return nullIfNotFound(getIt(args[0]));
- }
- /**
- * {@inheritDoc}
- */
- public final Scriptable construct(final Context cx, final Scriptable scope, final Object[] args) {
- return null;
- }
- /**
- * Private helper that retrieves the item or items corresponding to the specified
- * index or key.
- * @param o the index or key corresponding to the element or elements to return
- * @return the element or elements corresponding to the specified index or key
- */
- private Object getIt(final Object o) {
- if (o instanceof Number) {
- final Number n = (Number) o;
- final int i = n.intValue();
- return get(i, this);
- }
- final String key = String.valueOf(o);
- return get(key, this);
- }
- /**
- * Returns the element at the specified index, or <tt>NOT_FOUND</tt> if the index is invalid.
- * {@inheritDoc}
- */
- @Override
- public final Object get(final int index, final Scriptable start) {
- final HTMLCollection array = (HTMLCollection) start;
- final List<Object> elements = array.getElements();
- if (index >= 0 && index < elements.size()) {
- return getScriptableForElement(transformer_.transform(elements.get(index)));
- }
- return NOT_FOUND;
- }
- /**
- * Gets the HTML elements from cache or retrieve them at first call.
- * @return the list of {@link HtmlElement} contained in this collection
- */
- protected List<Object> getElements() {
- if (cachedElements_ == null) {
- cachedElements_ = computeElements();
- }
- return cachedElements_;
- }
- /**
- * Returns the elements whose associated host objects are available through this collection.
- * @return the elements whose associated host objects are available through this collection
- */
- protected List<Object> computeElements() {
- final List<Object> response;
- if (node_ != null) {
- if (xpath_ != null) {
- response = XPathUtils.getByXPath(node_, xpath_);
- }
- else {
- response = new ArrayList<Object>();
- Node node = node_.getFirstChild();
- while (node != null) {
- response.add(node);
- node = node.getNextSibling();
- }
- }
- }
- else {
- response = new ArrayList<Object>();
- }
- final boolean isXmlPage = node_ != null && node_.getOwnerDocument() instanceof XmlPage;
- final boolean isIE = getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_45);
- for (int i = 0; i < response.size(); i++) {
- final DomNode element = (DomNode) response.get(i);
- //IE: XmlPage ignores all empty text nodes
- if (isIE && isXmlPage && element instanceof DomText
- && ((DomText) element).getNodeValue().trim().length() == 0) { //and 'xml:space' is 'default'
- final Boolean xmlSpaceDefault = isXMLSpaceDefault(element.getParentNode());
- if (xmlSpaceDefault != Boolean.FALSE) {
- response.remove(i--);
- continue;
- }
- }
- for (DomNode parent = element.getParentNode(); parent != null;
- parent = parent.getParentNode()) {
- if (parent instanceof HtmlNoScript) {
- response.remove(i--);
- break;
- }
- }
- }
- return response;
- }
- /**
- * Recursively checks whether "xml:space" attribute is set to "default".
- * @param node node to start checking from
- * @return {@link Boolean#TRUE} if "default" is set, {@link Boolean#FALSE} for other value,
- * or null if nothing is set.
- */
- private static Boolean isXMLSpaceDefault(DomNode node) {
- for ( ; node instanceof DomElement; node = node.getParentNode()) {
- final String value = ((DomElement) node).getAttribute("xml:space");
- if (value.length() != 0) {
- if (value.equals("default")) {
- return Boolean.TRUE;
- }
- return Boolean.FALSE;
- }
- }
- return null;
- }
- /**
- * Returns the element or elements that match the specified key. If it is the name
- * of a property, the property value is returned. If it is the id of an element in
- * the array, that element is returned. Finally, if it is the name of an element or
- * elements in the array, then all those elements are returned. Otherwise,
- * {@link #NOT_FOUND} is returned.
- * {@inheritDoc}
- */
- @Override
- protected Object getWithPreemption(final String name) {
- // Test to see if we are trying to get the length of this collection?
- // If so return NOT_FOUND here to let the property be retrieved using the prototype
- if (xpath_ == null || "length".equals(name)) {
- return NOT_FOUND;
- }
- final List<Object> elements = getElements();
- CollectionUtils.transform(elements, transformer_);
- // See if there is an element in the element array with the specified id.
- for (final Object next : elements) {
- if (next instanceof DomElement) {
- final String id = ((DomElement) next).getAttribute("id");
- if (id != null && id.equals(name)) {
- if (getBrowserVersion().hasFeature(BrowserVersionFeatures.HTMLCOLLECTION_IDENTICAL_IDS)) {
- int totalIDs = 0;
- for (final Object o : elements) {
- if (o instanceof DomElement && name.equals(((DomElement) o).getAttribute("id"))) {
- totalIDs++;
- }
- }
- if (totalIDs > 1) {
- final HTMLCollectionTags collection =
- new HTMLCollectionTags((SimpleScriptable) getParentScope());
- collection.setAvoidObjectDetection(
- !getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_46));
- collection.init(node_, ".//*[@id='" + id + "']");
- return collection;
- }
- }
- if (LOG.isDebugEnabled()) {
- LOG.debug("Property \"" + name + "\" evaluated (by id) to " + next);
- }
- return getScriptableForElement(next);
- }
- }
- else if (next instanceof WebWindow) {
- final WebWindow window = (WebWindow) next;
- final String windowName = window.getName();
- if (windowName != null && windowName.equals(name)) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("Property \"" + name + "\" evaluated (by name) to " + window);
- }
- return getScriptableForElement(window);
- }
- if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_47) && window instanceof FrameWindow
- && ((FrameWindow) window).getFrameElement().getAttribute("id").equals(name)) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("Property \"" + name + "\" evaluated (by id) to " + window);
- }
- return getScriptableForElement(window);
- }
- }
- else {
- LOG.warn("Unrecognized type in collection: " + next + " (" + next.getClass().getName() + ")");
- }
- }
- // See if there are any elements in the element array with the specified name.
- final HTMLCollection array = new HTMLCollection(this);
- final String newCondition = "@name = '" + name.replaceAll("\\$", "\\\\\\$") + "'";
- final String xpathExpr;
- if (xpath_.endsWith("]")) {
- xpathExpr = xpath_.replaceAll("\\[([^\\]]*)\\]$", "[($1) and " + newCondition + "]");
- }
- else {
- xpathExpr = xpath_ + "[" + newCondition + "]";
- }
- array.init(node_, xpathExpr);
- final List<Object> subElements = array.getElements();
- if (subElements.size() > 1) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("Property \"" + name + "\" evaluated (by name) to " + array + " with "
- + subElements.size() + " elements");
- }
- return array;
- }
- else if (subElements.size() == 1) {
- final Scriptable singleResult = getScriptableForElement(subElements.get(0));
- if (LOG.isDebugEnabled()) {
- LOG.debug("Property \"" + name + "\" evaluated (by name) to " + singleResult);
- }
- return singleResult;
- }
- // Nothing was found.
- return NOT_FOUND;
- }
- /**
- * Returns the length of this element array.
- * @return the length of this element array
- * @see <a href="http://msdn.microsoft.com/en-us/library/ms534101.aspx">MSDN doc</a>
- */
- public final int jsxGet_length() {
- return getElements().size();
- }
- /**
- * Returns the item or items corresponding to the specified index or key.
- * @param index the index or key corresponding to the element or elements to return
- * @return the element or elements corresponding to the specified index or key
- * @see <a href="http://msdn.microsoft.com/en-us/library/ms536460.aspx">MSDN doc</a>
- */
- public final Object jsxFunction_item(final Object index) {
- return nullIfNotFound(getIt(index));
- }
- /**
- * Returns the specified object, unless it is the <tt>NOT_FOUND</tt> constant, in which case <tt>null</tt>
- * is returned for IE.
- * @param object the object to return
- * @return the specified object, unless it is the <tt>NOT_FOUND</tt> constant, in which case <tt>null</tt>
- * is returned for IE.
- */
- private Object nullIfNotFound(final Object object) {
- if (object == NOT_FOUND) {
- if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_48)) {
- return null;
- }
- return Context.getUndefinedValue();
- }
- return object;
- }
- /**
- * Retrieves the item or items corresponding to the specified name (checks ids, and if
- * that does not work, then names).
- * @param name the name or id the element or elements to return
- * @return the element or elements corresponding to the specified name or id
- * @see <a href="http://msdn.microsoft.com/en-us/library/ms536634.aspx">MSDN doc</a>
- */
- public final Object jsxFunction_namedItem(final String name) {
- return nullIfNotFound(getIt(name));
- }
- /**
- * Returns the next node in the collection (supporting iteration in IE only).
- * @return the next node in the collection
- */
- public Object jsxFunction_nextNode() {
- Object nextNode;
- final List<Object> elements = getElements();
- if (currentIndex_ >= 0 && currentIndex_ < elements.size()) {
- nextNode = elements.get(currentIndex_);
- }
- else {
- nextNode = null;
- }
- currentIndex_++;
- return nextNode;
- }
- /**
- * Resets the node iterator accessed via {@link #jsxFunction_nextNode()}.
- */
- public void jsxFunction_reset() {
- currentIndex_ = 0;
- }
- /**
- * Returns all the elements in this element array that have the specified tag name.
- * This method returns an empty element array if there are no elements with the
- * specified tag name.
- * @param tagName the name of the tag of the elements to return
- * @return all the elements in this element array that have the specified tag name
- * @see <a href="http://msdn.microsoft.com/en-us/library/ms536776.aspx">MSDN doc</a>
- */
- public Object jsxFunction_tags(final String tagName) {
- final HTMLCollection array = new HTMLCollection(this);
- array.init(node_, xpath_ + "[name() = '" + tagName.toLowerCase() + "']");
- return array;
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public String toString() {
- if (xpath_ != null) {
- return super.toString() + '<' + xpath_ + '>';
- }
- return super.toString();
- }
- /**
- * Called for the js "==".
- * {@inheritDoc}
- */
- @Override
- protected Object equivalentValues(final Object other) {
- if (other == this) {
- return Boolean.TRUE;
- }
- else if (other instanceof HTMLCollection) {
- final HTMLCollection otherArray = (HTMLCollection) other;
- if (node_ == otherArray.node_
- && xpath_.toString().equals(otherArray.xpath_.toString())
- && transformer_.equals(otherArray.transformer_)) {
- return Boolean.TRUE;
- }
- return NOT_FOUND;
- }
- return super.equivalentValues(other);
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean has(final String name, final Scriptable start) {
- try {
- final int index = Integer.parseInt(name);
- final List<Object> elements = getElements();
- CollectionUtils.transform(elements, transformer_);
- if (index >= 0 && index < elements.size()) {
- return true;
- }
- }
- catch (final NumberFormatException e) {
- // Ignore.
- }
- if (name.equals("length")) {
- return true;
- }
- if (!getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_49)) {
- final JavaScriptConfiguration jsConfig = JavaScriptConfiguration.getInstance(BrowserVersion.FIREFOX_3);
- for (final String functionName : jsConfig.getClassConfiguration(getClassName()).functionKeys()) {
- if (name.equals(functionName)) {
- return true;
- }
- }
- return false;
- }
- return getWithPreemption(name) != NOT_FOUND;
- }
- /**
- * {@inheritDoc}.
- */
- @Override
- public Object[] getIds() {
- final List<String> idList = new ArrayList<String>();
- final List<Object> elements = getElements();
- CollectionUtils.transform(elements, transformer_);
- if (!getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_50)) {
- final int length = getElements().size();
- for (int i = 0; i < length; i++) {
- idList.add(Integer.toString(i));
- }
- idList.add("length");
- final JavaScriptConfiguration jsConfig = JavaScriptConfiguration.getInstance(BrowserVersion.FIREFOX_3);
- for (final String name : jsConfig.getClassConfiguration(getClassName()).functionKeys()) {
- idList.add(name);
- }
- }
- else {
- idList.add("length");
- int index = 0;
- for (final Object next : elements) {
- if (next instanceof HtmlElement) {
- final HtmlElement element = (HtmlElement) next;
- final String name = element.getAttribute("name");
- if (name != HtmlElement.ATTRIBUTE_NOT_DEFINED) {
- idList.add(name);
- }
- else {
- final String id = element.getId();
- if (id != HtmlElement.ATTRIBUTE_NOT_DEFINED) {
- idList.add(id);
- }
- else {
- idList.add(Integer.toString(index));
- }
- }
- index++;
- }
- else if (next instanceof WebWindow) {
- final WebWindow window = (WebWindow) next;
- final String windowName = window.getName();
- if (windowName != null) {
- idList.add(windowName);
- }
- }
- else if (LOG.isDebugEnabled()) {
- LOG.debug("Unrecognized type in array: \"" + next.getClass().getName() + "\"");
- }
- }
- }
- return idList.toArray();
- }
- private class DomHtmlAttributeChangeListenerImpl implements DomChangeListener, HtmlAttributeChangeListener {
- private static final long serialVersionUID = -6690451155079053212L;
- /**
- * {@inheritDoc}
- */
- public void nodeAdded(final DomChangeEvent event) {
- cachedElements_ = null;
- }
- /**
- * {@inheritDoc}
- */
- public void nodeDeleted(final DomChangeEvent event) {
- cachedElements_ = null;
- }
- /**
- * {@inheritDoc}
- */
- public void attributeAdded(final HtmlAttributeChangeEvent event) {
- cachedElements_ = null;
- }
- /**
- * {@inheritDoc}
- */
- public void attributeRemoved(final HtmlAttributeChangeEvent event) {
- cachedElements_ = null;
- }
- /**
- * {@inheritDoc}
- */
- public void attributeReplaced(final HtmlAttributeChangeEvent event) {
- cachedElements_ = null;
- }
- }
- /**
- * {@inheritDoc}
- */
- public int getLength() {
- return jsxGet_length();
- }
- /**
- * {@inheritDoc}
- */
- public Node item(final int index) {
- return (DomNode) transformer_.transform(getElements().get(index));
- }
- /**
- * Gets the scriptable for the provided element that may already be the right scriptable.
- * @param object the object for which to get the scriptable
- * @return the scriptable
- */
- protected Scriptable getScriptableForElement(final Object object) {
- if (object instanceof Scriptable) {
- return (Scriptable) object;
- }
- else if (object instanceof WebWindow) {
- return Window.getProxy((WebWindow) object);
- }
- return getScriptableFor(object);
- }
- /**
- * {@inheritDoc}
- */
- @Override
- public String getClassName() {
- return "HTMLCollection";
- }
- }