001: /*
002: * Copyright (c) 2002-2008 Gargoyle Software Inc. All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * 1. Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: * 2. Redistributions in binary form must reproduce the above copyright notice,
010: * this list of conditions and the following disclaimer in the documentation
011: * and/or other materials provided with the distribution.
012: * 3. The end-user documentation included with the redistribution, if any, must
013: * include the following acknowledgment:
014: *
015: * "This product includes software developed by Gargoyle Software Inc.
016: * (http://www.GargoyleSoftware.com/)."
017: *
018: * Alternately, this acknowledgment may appear in the software itself, if
019: * and wherever such third-party acknowledgments normally appear.
020: * 4. The name "Gargoyle Software" must not be used to endorse or promote
021: * products derived from this software without prior written permission.
022: * For written permission, please contact info@GargoyleSoftware.com.
023: * 5. Products derived from this software may not be called "HtmlUnit", nor may
024: * "HtmlUnit" appear in their name, without prior written permission of
025: * Gargoyle Software Inc.
026: *
027: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
028: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
029: * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
030: * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
031: * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
032: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
033: * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
034: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
035: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
036: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037: */
038: package com.gargoylesoftware.htmlunit.javascript;
039:
040: import java.util.ArrayList;
041: import java.util.Iterator;
042: import java.util.List;
043: import java.util.Map;
044: import java.util.Set;
045:
046: import org.apache.commons.collections.CollectionUtils;
047: import org.apache.commons.collections.Transformer;
048: import org.apache.commons.collections.functors.NOPTransformer;
049: import org.jaxen.JaxenException;
050: import org.jaxen.XPath;
051: import org.jaxen.saxpath.SAXPathException;
052: import org.mozilla.javascript.Context;
053: import org.mozilla.javascript.Function;
054: import org.mozilla.javascript.JavaScriptException;
055: import org.mozilla.javascript.Scriptable;
056:
057: import com.gargoylesoftware.htmlunit.BrowserVersion;
058: import com.gargoylesoftware.htmlunit.WebWindow;
059: import com.gargoylesoftware.htmlunit.html.DomChangeEvent;
060: import com.gargoylesoftware.htmlunit.html.DomChangeListener;
061: import com.gargoylesoftware.htmlunit.html.DomNode;
062: import com.gargoylesoftware.htmlunit.html.DomText;
063: import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeEvent;
064: import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeListener;
065: import com.gargoylesoftware.htmlunit.html.HtmlElement;
066: import com.gargoylesoftware.htmlunit.html.HtmlNoScript;
067: import com.gargoylesoftware.htmlunit.javascript.configuration.JavaScriptConfiguration;
068: import com.gargoylesoftware.htmlunit.xml.XmlAttr;
069: import com.gargoylesoftware.htmlunit.xml.XmlElement;
070: import com.gargoylesoftware.htmlunit.xml.XmlPage;
071:
072: /**
073: * An array of elements. Used for the element arrays returned by <tt>document.all</tt>,
074: * <tt>document.all.tags('x')</tt>, <tt>document.forms</tt>, <tt>window.frames</tt>, etc.
075: * Note that this class must not be used for collections that can be modified, for example
076: * <tt>map.areas</tt> and <tt>select.options</tt>.
077: * <br>
078: * This class (like all classes in this package) is specific for the javascript engine.
079: * Users of HtmlUnit shouldn't use it directly.
080: *
081: * @version $Revision: 2158 $
082: * @author Daniel Gredler
083: * @author Marc Guillemot
084: * @author Chris Erskine
085: * @author Ahmed Ashour
086: */
087: public class HTMLCollection extends SimpleScriptable implements
088: Function {
089: private static final long serialVersionUID = 4049916048017011764L;
090:
091: private XPath xpath_;
092: private DomNode node_;
093:
094: /**
095: * The transformer used to get the element to return from the html element.
096: * It returns the html element itself except for frames where it returns the nested window.
097: */
098: private Transformer transformer_;
099:
100: private List cachedElements_;
101:
102: /**
103: * Create an instance. Javascript objects must have a default constructor.
104: */
105: public HTMLCollection() {
106: // nothing
107: }
108:
109: /**
110: * Create an instance
111: * @param parentScope parent scope
112: */
113: public HTMLCollection(final SimpleScriptable parentScope) {
114: setParentScope(parentScope);
115: setPrototype(getPrototype(getClass()));
116: }
117:
118: /**
119: * Init the content of this collection. The elements will be "calculated" at each
120: * access using the xpath applied on the node.
121: * @param node the node to serve as root for the xpath expression
122: * @param xpath the xpath giving the elements of the collection
123: */
124: public void init(final DomNode node, final XPath xpath) {
125: init(node, xpath, NOPTransformer.INSTANCE);
126: }
127:
128: /**
129: * Init the content of this collection. The elements will be "calculated" at each
130: * access using the xpath applied on the node and transformed using the transformer.
131: * @param node the node to serve as root for the xpath expression
132: * @param xpath the xpath giving the elements of the collection
133: * @param transformer the transformer allowing to get the expected objects from the xpath
134: * evaluation
135: */
136: public void init(final DomNode node, final XPath xpath,
137: final Transformer transformer) {
138: if (node != null) {
139: node_ = node;
140: xpath_ = xpath;
141: try {
142: if (node instanceof XmlPage) {
143: final XmlElement documentElement = ((XmlPage) node)
144: .getDocumentXmlElement();
145: if (documentElement != null) {
146: addNamespace(documentElement);
147: }
148: } else if (node instanceof XmlElement) {
149: addNamespace((XmlElement) node);
150: }
151: } catch (final JaxenException e) {
152: throw Context
153: .reportRuntimeError("Exception adding namespaces: "
154: + e);
155: }
156: transformer_ = transformer;
157: final DomHtmlAttributeChangeListenerImpl listener = new DomHtmlAttributeChangeListenerImpl();
158: node_.addDomChangeListener(listener);
159: if (node_ instanceof HtmlElement) {
160: ((HtmlElement) node_)
161: .addHtmlAttributeChangeListener(listener);
162: cachedElements_ = null;
163: }
164: }
165: }
166:
167: /**
168: * Adds all namespaces defined in the given element and its all descendants to the XPath.
169: * @param element root element
170: * @throws JaxenException if a <code>NamespaceContext</code>
171: * used by the XPath has been explicitly installed
172: */
173: private void addNamespace(final XmlElement element)
174: throws JaxenException {
175: final Map attributes = element.getAttributes();
176: for (final Iterator keys = attributes.keySet().iterator(); keys
177: .hasNext();) {
178: final String name = (String) keys.next();
179: final String value = (String) ((XmlAttr) attributes
180: .get(name)).getValue();
181: if (name.startsWith("xmlns:")) {
182: final String prefix = name.substring("xmlns:".length());
183: xpath_.addNamespace(prefix, value);
184: }
185: }
186: for (final Iterator children = element.getChildIterator(); children
187: .hasNext();) {
188: final DomNode child = (DomNode) children.next();
189: if (child instanceof XmlElement) {
190: addNamespace((XmlElement) child);
191: }
192: }
193: }
194:
195: /**
196: * {@inheritDoc}
197: */
198: public final Object call(final Context context,
199: final Scriptable scope, final Scriptable this Obj,
200: final Object[] args) throws JavaScriptException {
201: if (args.length == 0) {
202: throw Context
203: .reportRuntimeError("Zero arguments; need an index or a key.");
204: }
205: final Object response = get(args[0]);
206: if (response == NOT_FOUND) {
207: return null;
208: }
209: return response;
210: }
211:
212: /**
213: * {@inheritDoc}
214: */
215: public final Scriptable construct(final Context arg0,
216: final Scriptable arg1, final Object[] arg2)
217: throws JavaScriptException {
218: return null;
219: }
220:
221: /**
222: * Private helper that retrieves the item or items corresponding to the specified
223: * index or key.
224: * @param o The index or key corresponding to the element or elements to return.
225: * @return The element or elements corresponding to the specified index or key.
226: */
227: private Object get(final Object o) {
228: if (o instanceof Number) {
229: final Number n = (Number) o;
230: final int i = n.intValue();
231: return get(i, this );
232: } else {
233: final String key = String.valueOf(o);
234: return get(key, this );
235: }
236: }
237:
238: /**
239: * Returns the element at the specified index, or <tt>NOT_FOUND</tt> if the
240: * index is invalid.
241: * {@inheritDoc}
242: */
243: public final Object get(final int index, final Scriptable start) {
244: final HTMLCollection array = (HTMLCollection) start;
245: final List elements = array.getElements();
246:
247: if (index >= 0 && index < elements.size()) {
248: return getScriptableFor(transformer_.transform(elements
249: .get(index)));
250: } else {
251: return NOT_FOUND;
252: }
253: }
254:
255: /**
256: * Gets the html elements. Avoid calling it multiple times within a method because the evaluation
257: * needs to be performed each time again
258: * @return the list of {@link HtmlElement} contained in this collection
259: */
260: private List getElements() {
261: if (cachedElements_ == null) {
262: try {
263: if (node_ != null) {
264: cachedElements_ = xpath_.selectNodes(node_);
265: } else {
266: cachedElements_ = new ArrayList();
267: }
268: boolean isXmlPage = false;
269:
270: //TODO: should be replaced by "getPage() instanceof XmlPage"
271: for (DomNode parent = node_; parent != null; parent = parent
272: .getParentDomNode()) {
273: if (parent instanceof XmlPage) {
274: isXmlPage = true;
275: }
276: }
277:
278: final boolean isIE = getWindow().getWebWindow()
279: .getWebClient().getBrowserVersion().isIE();
280:
281: for (int i = 0; i < cachedElements_.size(); i++) {
282: final DomNode element = (DomNode) cachedElements_
283: .get(i);
284:
285: //IE: XmlPage ignores all empty text nodes
286: if (isIE
287: && isXmlPage
288: && element instanceof DomText
289: && ((DomText) element).getNodeValue()
290: .trim().length() == 0) {
291:
292: //and 'xml:space' is 'default'
293: final Boolean xmlSpaceDefault = isXMLSpaceDefault(element
294: .getParentDomNode());
295: if (xmlSpaceDefault != Boolean.FALSE) {
296: cachedElements_.remove(i--);
297: continue;
298: }
299: }
300: for (DomNode parent = element.getParentDomNode(); parent != null; parent = parent
301: .getParentDomNode()) {
302: if (parent instanceof HtmlNoScript) {
303: cachedElements_.remove(i--);
304: break;
305: }
306: }
307: }
308: } catch (final JaxenException e) {
309: throw Context
310: .reportRuntimeError("Exeption getting elements: "
311: + e.getMessage());
312: }
313: }
314: return cachedElements_;
315: }
316:
317: /**
318: * Recursively checks whether "xml:space" attribute is set to "default".
319: * @param node node to start checking from.
320: * @return {@link Boolean#TRUE} if "default" is set, {@link Boolean#FALSE} for other value,
321: * or null if nothing is set.
322: */
323: private static Boolean isXMLSpaceDefault(DomNode node) {
324: for (; node instanceof XmlElement; node = node
325: .getParentDomNode()) {
326: final String value = ((XmlElement) node)
327: .getAttributeValue("xml:space");
328: if (value.length() != 0) {
329: if (value.equals("default")) {
330: return Boolean.TRUE;
331: } else {
332: return Boolean.FALSE;
333: }
334: }
335: }
336: return null;
337: }
338:
339: /**
340: * Returns the element or elements that match the specified key. If it is the name
341: * of a property, the property value is returned. If it is the id of an element in
342: * the array, that element is returned. Finally, if it is the name of an element or
343: * elements in the array, then all those elements are returned. Otherwise,
344: * {@link #NOT_FOUND} is returned.
345: * {@inheritDoc}
346: */
347: protected Object getWithPreemption(final String name) {
348: // Test to see if we are trying to get the length of this collection?
349: // If so return NOT_FOUND here to let the property be retrieved using the prototype
350: if ("length".equals(name)) {
351: return NOT_FOUND;
352: }
353:
354: final List elements = getElements();
355: CollectionUtils.transform(elements, transformer_);
356:
357: // See if there is an element in the element array with the specified id.
358: for (final Iterator iter = elements.iterator(); iter.hasNext();) {
359: final Object next = iter.next();
360: if (next instanceof HtmlElement) {
361: final HtmlElement element = (HtmlElement) next;
362: final String id = element.getId();
363: if (id != null && id.equals(name)) {
364: getLog().debug(
365: "Property \"" + name
366: + "\" evaluated (by id) to "
367: + element);
368: return getScriptableFor(element);
369: }
370: } else if (next instanceof WebWindow) {
371: final WebWindow window = (WebWindow) next;
372: final String windowName = window.getName();
373: if (windowName != null && windowName.equals(name)) {
374: getLog().debug(
375: "Property \"" + name
376: + "\" evaluated (by name) to "
377: + window);
378: return getScriptableFor(window);
379: }
380: } else {
381: getLog().debug(
382: "Unrecognized type in array: \""
383: + next.getClass().getName() + "\"");
384: }
385: }
386:
387: // See if there are any elements in the element array with the specified name.
388: final HTMLCollection array = new HTMLCollection(this );
389: try {
390: final String newCondition = "@name = '" + name + "'";
391: final String currentXPathExpr = xpath_.toString();
392: final String xpathExpr;
393: if (currentXPathExpr.endsWith("]")) {
394: xpathExpr = currentXPathExpr.substring(0,
395: currentXPathExpr.length() - 1)
396: + " and " + newCondition + "]";
397: } else {
398: xpathExpr = currentXPathExpr + "[" + newCondition + "]";
399: }
400: final XPath xpathName = xpath_.getNavigator().parseXPath(
401: xpathExpr);
402: array.init(node_, xpathName);
403: } catch (final SAXPathException e) {
404: throw Context
405: .reportRuntimeError("Failed getting sub elements by name"
406: + e.getMessage());
407: }
408:
409: final List subElements = array.getElements();
410: if (subElements.size() > 1) {
411: getLog().debug(
412: "Property \"" + name + "\" evaluated (by name) to "
413: + array + " with " + subElements.size()
414: + " elements");
415: return array;
416: } else if (subElements.size() == 1) {
417: final SimpleScriptable singleResult = getScriptableFor(subElements
418: .get(0));
419: getLog().debug(
420: "Property \"" + name + "\" evaluated (by name) to "
421: + singleResult);
422: return singleResult;
423: }
424:
425: // Nothing was found.
426: return NOT_FOUND;
427: }
428:
429: /**
430: * Returns the length of this element array.
431: * @return The length of this element array.
432: * @see <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/properties/length.asp">MSDN doc</a>
433: */
434: public final int jsxGet_length() {
435: return getElements().size();
436: }
437:
438: /**
439: * Retrieves the item or items corresponding to the specified index or key.
440: * @param index The index or key corresponding to the element or elements to return.
441: * @return The element or elements corresponding to the specified index or key.
442: * @see <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/item.asp">MSDN doc</a>
443: */
444: public final Object jsxFunction_item(final Object index) {
445: return get(index);
446: }
447:
448: /**
449: * Retrieves the item or items corresponding to the specified name (checks ids, and if
450: * that does not work, then names).
451: * @param name The name or id the element or elements to return.
452: * @return The element or elements corresponding to the specified name or id.
453: * @see <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/nameditem.asp">MSDN doc</a>
454: */
455: public final Object jsxFunction_namedItem(final String name) {
456: return get(name);
457: }
458:
459: /**
460: * Returns all the elements in this element array that have the specified tag name.
461: * This method returns an empty element array if there are no elements with the
462: * specified tag name.
463: * @param tagName The name of the tag of the elements to return.
464: * @return All the elements in this element array that have the specified tag name.
465: * @see <a href="http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/tags.asp">MSDN doc</a>
466: */
467: public final Object jsxFunction_tags(final String tagName) {
468: final HTMLCollection array = new HTMLCollection(this );
469: try {
470: final String newXPathExpr = xpath_ + "[name() = '"
471: + tagName.toLowerCase() + "']";
472: array.init(node_, xpath_.getNavigator().parseXPath(
473: newXPathExpr));
474: } catch (final SAXPathException e) {
475: // should never occur
476: throw Context.reportRuntimeError("Failed call tags: "
477: + e.getMessage());
478: }
479:
480: return array;
481: }
482:
483: /**
484: * Just for debug purpose.
485: * {@inheritDoc}
486: */
487: public String toString() {
488: if (xpath_ != null) {
489: return super .toString() + '<' + xpath_ + '>';
490: }
491: return super .toString();
492: }
493:
494: /**
495: * Called for the js "==".
496: * {@inheritDoc}
497: */
498: protected Object equivalentValues(final Object other) {
499: if (other == this ) {
500: return Boolean.TRUE;
501: } else if (other instanceof HTMLCollection) {
502: final HTMLCollection otherArray = (HTMLCollection) other;
503: if (node_ == otherArray.node_
504: && xpath_.toString().equals(
505: otherArray.xpath_.toString())
506: && transformer_.equals(otherArray.transformer_)) {
507: return Boolean.TRUE;
508: } else {
509: return NOT_FOUND;
510: }
511: }
512:
513: return super .equivalentValues(other);
514: }
515:
516: /**
517: * {@inheritDoc}
518: */
519: public boolean has(final String name, final Scriptable start) {
520: if (!getWindow().getWebWindow().getWebClient()
521: .getBrowserVersion().isIE()) {
522: if (name.equals("0") || name.equals("length")) {
523: return true;
524: }
525: final JavaScriptConfiguration jsConfig = JavaScriptConfiguration
526: .getInstance(BrowserVersion.FIREFOX_2);
527: final Set functionKeys = jsConfig.getClassConfiguration(
528: getClassName()).functionKeys();
529: for (final Iterator functionIt = functionKeys.iterator(); functionIt
530: .hasNext();) {
531: if (name.equals(functionIt.next())) {
532: return true;
533: }
534: }
535: return false;
536: } else {
537: return name.equals("length")
538: || getWithPreemption(name) != NOT_FOUND;
539: }
540: }
541:
542: /**
543: * Returns the element or elements that match the specified key. If it is the name
544: * of a property, the property value is returned. If it is the id of an element in
545: * the array, that element is returned. Finally, if it is the name of an element or
546: * elements in the array, then all those elements are returned. Otherwise,
547: * {@inheritDoc}.
548: */
549: public Object[] getIds() {
550: final List idList = new ArrayList();
551:
552: if (!getWindow().getWebWindow().getWebClient()
553: .getBrowserVersion().isIE()) {
554: idList.add("0");
555: idList.add("length");
556: final JavaScriptConfiguration jsConfig = JavaScriptConfiguration
557: .getInstance(BrowserVersion.FIREFOX_2);
558: final Set functionKeys = jsConfig.getClassConfiguration(
559: getClassName()).functionKeys();
560: for (final Iterator functionIt = functionKeys.iterator(); functionIt
561: .hasNext();) {
562: idList.add(functionIt.next());
563: }
564: //'document.all.tags' is different from 'document.forms.tags'
565: //See HTMLCollectionTest.testTags()
566: idList.remove("tags");
567: } else {
568: idList.add("length");
569:
570: final List elements = getElements();
571: CollectionUtils.transform(elements, transformer_);
572:
573: // See if there is an element in the element array with the specified id.
574: for (final Iterator iter = elements.iterator(); iter
575: .hasNext();) {
576: final Object next = iter.next();
577: if (next instanceof HtmlElement) {
578: final HtmlElement element = (HtmlElement) next;
579: final String id = element.getId();
580: if (id != HtmlElement.ATTRIBUTE_NOT_DEFINED) {
581: idList.add(id);
582: }
583: } else if (next instanceof WebWindow) {
584: final WebWindow window = (WebWindow) next;
585: final String windowName = window.getName();
586: if (windowName != null) {
587: idList.add(windowName);
588: }
589: } else {
590: getLog().debug(
591: "Unrecognized type in array: \""
592: + next.getClass().getName() + "\"");
593: }
594: }
595:
596: if (xpath_ != null) {
597: // See if there are any elements in the element array with the specified name.
598: final HTMLCollection array = new HTMLCollection(this );
599: try {
600: final String newCondition = "@name";
601: final String currentXPathExpr = xpath_.toString();
602: final String xpathExpr;
603: if (currentXPathExpr.endsWith("]")) {
604: xpathExpr = currentXPathExpr.substring(0,
605: currentXPathExpr.length() - 1)
606: + " and " + newCondition + "]";
607: } else {
608: xpathExpr = currentXPathExpr + "["
609: + newCondition + "]";
610: }
611: final XPath xpathName = xpath_.getNavigator()
612: .parseXPath(xpathExpr);
613: array.init(node_, xpathName);
614: } catch (final SAXPathException e) {
615: throw Context
616: .reportRuntimeError("Failed getting sub elements by name"
617: + e.getMessage());
618: }
619:
620: final List subElements = array.getElements();
621: for (final Iterator it = subElements.iterator(); it
622: .hasNext();) {
623: final DomNode next = (DomNode) it.next();
624: if (next instanceof HtmlElement) {
625: final HtmlElement element = (HtmlElement) next;
626: final String id = element.getAttribute("name");
627: if (id != null) {
628: idList.add(id);
629: }
630: }
631: }
632: }
633: }
634: return idList.toArray();
635: }
636:
637: private class DomHtmlAttributeChangeListenerImpl implements
638: DomChangeListener, HtmlAttributeChangeListener {
639: /**
640: * {@inheritDoc}
641: */
642: public void nodeAdded(final DomChangeEvent event) {
643: cachedElements_ = null;
644: }
645:
646: /**
647: * {@inheritDoc}
648: */
649: public void nodeDeleted(final DomChangeEvent event) {
650: cachedElements_ = null;
651: }
652:
653: /**
654: * {@inheritDoc}
655: */
656: public void attributeAdded(final HtmlAttributeChangeEvent event) {
657: cachedElements_ = null;
658: }
659:
660: /**
661: * {@inheritDoc}
662: */
663: public void attributeRemoved(
664: final HtmlAttributeChangeEvent event) {
665: cachedElements_ = null;
666: }
667:
668: /**
669: * {@inheritDoc}
670: */
671: public void attributeReplaced(
672: final HtmlAttributeChangeEvent event) {
673: cachedElements_ = null;
674: }
675: }
676: }
|