View Javadoc

1   /*
2    * Copyright (c) 2002-2011 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.javascript.host;
16  
17  import java.io.IOException;
18  
19  import net.sourceforge.htmlunit.corejs.javascript.Context;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  
24  import com.gargoylesoftware.htmlunit.BrowserVersion;
25  import com.gargoylesoftware.htmlunit.BrowserVersionFeatures;
26  import com.gargoylesoftware.htmlunit.ElementNotFoundException;
27  import com.gargoylesoftware.htmlunit.SgmlPage;
28  import com.gargoylesoftware.htmlunit.html.DomComment;
29  import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
30  import com.gargoylesoftware.htmlunit.html.DomElement;
31  import com.gargoylesoftware.htmlunit.html.DomNode;
32  import com.gargoylesoftware.htmlunit.html.DomText;
33  import com.gargoylesoftware.htmlunit.html.FrameWindow;
34  import com.gargoylesoftware.htmlunit.html.HTMLParser;
35  import com.gargoylesoftware.htmlunit.html.HtmlDivision;
36  import com.gargoylesoftware.htmlunit.html.HtmlPage;
37  import com.gargoylesoftware.htmlunit.html.impl.SimpleRange;
38  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
39  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLCollection;
40  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
41  import com.gargoylesoftware.htmlunit.xml.XmlUtil;
42  
43  /**
44   * A JavaScript object for a Document.
45   *
46   * @version $Revision: 6472 $
47   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
48   * @author David K. Taylor
49   * @author <a href="mailto:chen_jun@users.sourceforge.net">Chen Jun</a>
50   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
51   * @author Chris Erskine
52   * @author Marc Guillemot
53   * @author Daniel Gredler
54   * @author Michael Ottati
55   * @author <a href="mailto:george@murnock.com">George Murnock</a>
56   * @author Ahmed Ashour
57   * @author Rob Di Marco
58   * @see <a href="http://msdn.microsoft.com/en-us/library/ms531073.aspx">MSDN documentation</a>
59   * @see <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-7068919">W3C Dom Level 1</a>
60   */
61  public class Document extends EventNode {
62  
63      private static final Log LOG = LogFactory.getLog(Document.class);
64  
65      private Window window_;
66      private DOMImplementation implementation_;
67      private String designMode_;
68  
69      /**
70       * Sets the Window JavaScript object that encloses this document.
71       * @param window the Window JavaScript object that encloses this document
72       */
73      public void setWindow(final Window window) {
74          window_ = window;
75      }
76  
77      /**
78       * Returns the value of the "location" property.
79       * @return the value of the "location" property
80       */
81      public Location jsxGet_location() {
82          return window_.jsxGet_location();
83      }
84  
85      /**
86       * Sets the value of the "location" property. The location's default property is "href",
87       * so setting "document.location='http://www.sf.net'" is equivalent to setting
88       * "document.location.href='http://www.sf.net'".
89       * @see <a href="http://msdn.microsoft.com/en-us/library/ms535866.aspx">MSDN documentation</a>
90       * @param location the location to navigate to
91       * @throws IOException when location loading fails
92       */
93      public void jsxSet_location(final String location) throws IOException {
94          window_.jsxSet_location(location);
95      }
96  
97      /**
98       * Returns the value of the "referrer" property.
99       * @return the value of the "referrer" property
100      */
101     public String jsxGet_referrer() {
102         final String referrer = getPage().getWebResponse().getWebRequest().getAdditionalHeaders().get("Referer");
103         if (referrer == null) {
104             return "";
105         }
106         return referrer;
107     }
108 
109     /**
110      * Gets the JavaScript property "documentElement" for the document.
111      * @return the root node for the document
112      */
113     public Element jsxGet_documentElement() {
114         final Object documentElement = getPage().getDocumentElement();
115         if (documentElement == null) {
116             // for instance with an XML document with parsing error
117             return null;
118         }
119         return (Element) getScriptableFor(documentElement);
120     }
121 
122     /**
123      * Gets the JavaScript property "doctype" for the document.
124      * @return the DocumentType of the document
125      */
126     public SimpleScriptable jsxGet_doctype() {
127         final Object documentType = getPage().getDoctype();
128         if (documentType == null) {
129             return null;
130         }
131         return getScriptableFor(documentType);
132     }
133 
134     /**
135      * Returns a value which indicates whether or not the document can be edited.
136      * @return a value which indicates whether or not the document can be edited
137      */
138     public String jsxGet_designMode() {
139         if (designMode_ == null) {
140             if (getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_30)) {
141                 if (getWindow().getWebWindow() instanceof FrameWindow) {
142                     designMode_ = "Inherit";
143                 }
144                 else {
145                     designMode_ = "Off";
146                 }
147             }
148             else {
149                 designMode_ = "off";
150             }
151         }
152         return designMode_;
153     }
154 
155     /**
156      * Sets a value which indicates whether or not the document can be edited.
157      * @param mode a value which indicates whether or not the document can be edited
158      */
159     public void jsxSet_designMode(final String mode) {
160         final boolean ie = getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_31);
161         if (ie) {
162             if (!"on".equalsIgnoreCase(mode) && !"off".equalsIgnoreCase(mode) && !"inherit".equalsIgnoreCase(mode)) {
163                 throw Context.reportRuntimeError("Invalid document.designMode value '" + mode + "'.");
164             }
165             else if (!(getWindow().getWebWindow() instanceof FrameWindow)) {
166                 // IE ignores designMode changes for documents that aren't in frames.
167                 return;
168             }
169             else if ("on".equalsIgnoreCase(mode)) {
170                 designMode_ = "On";
171             }
172             else if ("off".equalsIgnoreCase(mode)) {
173                 designMode_ = "Off";
174             }
175             else if ("inherit".equalsIgnoreCase(mode)) {
176                 designMode_ = "Inherit";
177             }
178         }
179         else {
180             if ("on".equalsIgnoreCase(mode)) {
181                 designMode_ = "on";
182                 final SgmlPage page = getPage();
183                 if (page instanceof HtmlPage) {
184                     final HtmlPage htmlPage = (HtmlPage) page;
185                     final DomNode child = htmlPage.getBody().getFirstChild();
186                     final DomNode rangeNode = child != null ? child : htmlPage.getBody();
187                     htmlPage.setSelectionRange(new SimpleRange(rangeNode, 0));
188                 }
189             }
190             else if ("off".equalsIgnoreCase(mode)) {
191                 designMode_ = "off";
192             }
193         }
194     }
195 
196     /**
197      * Returns the page that this document is modeling.
198      * @return the page that this document is modeling
199      */
200     protected SgmlPage getPage() {
201         return (SgmlPage) getDomNodeOrDie();
202     }
203 
204     /**
205      * Gets the window in which this document is contained.
206      * @return the window
207      */
208     public Object jsxGet_defaultView() {
209         return getWindow();
210     }
211 
212     /**
213      * Creates a new document fragment.
214      * @return a newly created document fragment
215      */
216     public Object jsxFunction_createDocumentFragment() {
217         final DomDocumentFragment fragment = this.<DomNode>getDomNodeOrDie().getPage().createDomDocumentFragment();
218         final DocumentFragment node = new DocumentFragment();
219         node.setParentScope(getParentScope());
220         node.setPrototype(getPrototype(node.getClass()));
221         node.setDomNode(fragment);
222         return getScriptableFor(fragment);
223     }
224 
225     /**
226      * Creates a new HTML attribute with the specified name.
227      *
228      * @param attributeName the name of the attribute to create
229      * @return an attribute with the specified name
230      */
231     public Attr jsxFunction_createAttribute(final String attributeName) {
232         return (Attr) getPage().createAttribute(attributeName).getScriptObject();
233     }
234 
235     /**
236      * Returns the {@link BoxObject} for the specific element.
237      *
238      * @param element target for BoxObject
239      * @return the BoxObject
240      */
241     public BoxObject jsxFunction_getBoxObjectFor(final HTMLElement element) {
242         return element.getBoxObject();
243     }
244 
245     /**
246      * Imports a node from another document to this document.
247      * The source node is not altered or removed from the original document;
248      * this method creates a new copy of the source node.
249      *
250      * @param importedNode the node to import
251      * @param deep Whether to recursively import the subtree under the specified node; or not
252      * @return the imported node that belongs to this Document
253      */
254     public Object jsxFunction_importNode(final Node importedNode, final boolean deep) {
255         return importedNode.<DomNode>getDomNodeOrDie().cloneNode(deep).getScriptObject();
256     }
257 
258     /**
259      * Returns the implementation object of the current document.
260      * @return implementation-specific object
261      */
262     public DOMImplementation jsxGet_implementation() {
263         if (implementation_ == null) {
264             implementation_ = new DOMImplementation();
265             implementation_.setParentScope(getWindow());
266             implementation_.setPrototype(getPrototype(implementation_.getClass()));
267         }
268         return implementation_;
269     }
270 
271     /**
272      * Does nothing special anymore... just like FF.
273      * @param type the type of events to capture
274      * @see Window#jsxFunction_captureEvents(String)
275      */
276     public void jsxFunction_captureEvents(final String type) {
277         // Empty.
278     }
279 
280     /**
281      * Adapts any DOM node to resolve namespaces so that an XPath expression can be easily
282      * evaluated relative to the context of the node where it appeared within the document.
283      * @param nodeResolver the node to be used as a context for namespace resolution
284      * @return an XPathNSResolver which resolves namespaces with respect to the definitions
285      *         in scope for a specified node
286      */
287     public XPathNSResolver jsxFunction_createNSResolver(final Node nodeResolver) {
288         final XPathNSResolver resolver = new XPathNSResolver();
289         resolver.setElement(nodeResolver);
290         resolver.setParentScope(getWindow());
291         resolver.setPrototype(getPrototype(resolver.getClass()));
292         return resolver;
293     }
294 
295     /**
296      * Create a new DOM text node with the given data.
297      *
298      * @param newData the string value for the text node
299      * @return the new text node or NOT_FOUND if there is an error
300      */
301     public Object jsxFunction_createTextNode(final String newData) {
302         Object result = NOT_FOUND;
303         try {
304             final DomNode domNode = new DomText(this.<DomNode>getDomNodeOrDie().getPage(), newData);
305             final Object jsElement = getScriptableFor(domNode);
306 
307             if (jsElement == NOT_FOUND) {
308                 if (LOG.isDebugEnabled()) {
309                     LOG.debug("createTextNode(" + newData
310                             + ") cannot return a result as there isn't a JavaScript object for the DOM node "
311                             + domNode.getClass().getName());
312                 }
313             }
314             else {
315                 result = jsElement;
316             }
317         }
318         catch (final ElementNotFoundException e) {
319             // Just fall through - result is already set to NOT_FOUND
320         }
321         return result;
322     }
323 
324     /**
325      * Creates a new Comment.
326      * @param comment the comment text
327      * @return the new Comment
328      */
329     public Object jsxFunction_createComment(final String comment) {
330         final DomNode domNode = new DomComment(this.<DomNode>getDomNodeOrDie().getPage(), comment);
331         return getScriptableFor(domNode);
332     }
333 
334     /**
335      * Evaluates an XPath expression string and returns a result of the specified type if possible.
336      * @param expression the XPath expression string to be parsed and evaluated
337      * @param contextNode the context node for the evaluation of this XPath expression
338      * @param resolver the resolver permits translation of all prefixes, including the XML namespace prefix,
339      *        within the XPath expression into appropriate namespace URIs.
340      * @param type If a specific type is specified, then the result will be returned as the corresponding type
341      * @param result the result object which may be reused and returned by this method
342      * @return the result of the evaluation of the XPath expression
343      */
344     public XPathResult jsxFunction_evaluate(final String expression, final Node contextNode,
345             final Object resolver, final int type, final Object result) {
346         XPathResult xPathResult = (XPathResult) result;
347         if (xPathResult == null) {
348             xPathResult = new XPathResult();
349             xPathResult.setParentScope(getParentScope());
350             xPathResult.setPrototype(getPrototype(xPathResult.getClass()));
351         }
352         xPathResult.init(contextNode.<DomNode>getDomNodeOrDie().getByXPath(expression), type);
353         return xPathResult;
354     }
355 
356     /**
357      * Create a new HTML element with the given tag name.
358      *
359      * @param tagName the tag name
360      * @return the new HTML element, or NOT_FOUND if the tag is not supported
361      */
362     public Object jsxFunction_createElement(String tagName) {
363         Object result = NOT_FOUND;
364         try {
365             final BrowserVersion browserVersion = getBrowserVersion();
366 
367             if (tagName.startsWith("<") && tagName.endsWith(">")
368                     && browserVersion.hasFeature(BrowserVersionFeatures.GENERATED_153)) {
369                 tagName = tagName.substring(1, tagName.length() - 1);
370                 if (!tagName.matches("\\w+")) {
371                     LOG.error("Unexpected exception occurred while parsing HTML snippet");
372                     throw Context.reportRuntimeError("Unexpected exception occurred while parsing HTML snippet: "
373                             + tagName);
374                 }
375             }
376 
377             final SgmlPage page = getPage();
378             final org.w3c.dom.Element element = page.createElement(tagName);
379             final Object jsElement = getScriptableFor(element);
380 
381             if (jsElement == NOT_FOUND) {
382                 if (LOG.isDebugEnabled()) {
383                     LOG.debug("createElement(" + tagName
384                         + ") cannot return a result as there isn't a JavaScript object for the element "
385                         + element.getClass().getName());
386                 }
387             }
388             else {
389                 result = jsElement;
390             }
391         }
392         catch (final ElementNotFoundException e) {
393             // Just fall through - result is already set to NOT_FOUND
394         }
395         return result;
396     }
397 
398     /**
399      * Creates a new HTML element with the given tag name, and name.
400      *
401      * @param namespaceURI the URI that identifies an XML namespace
402      * @param qualifiedName the qualified name of the element type to instantiate
403      * @return the new HTML element, or NOT_FOUND if the tag is not supported
404      */
405     public Object jsxFunction_createElementNS(final String namespaceURI, final String qualifiedName) {
406         final org.w3c.dom.Element element;
407         final BrowserVersion browserVersion = getBrowserVersion();
408         if (browserVersion.hasFeature(BrowserVersionFeatures.XUL_SUPPORT)
409                 && "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul".equals(namespaceURI)) {
410             // simple hack, no need to implement the XUL objects (at least in a first time)
411             element = new HtmlDivision(namespaceURI, qualifiedName, getPage(), null);
412         }
413         else if (HTMLParser.XHTML_NAMESPACE.equals(namespaceURI)) {
414             element = getPage().createElementNS(namespaceURI, qualifiedName);
415         }
416         else {
417             element = new DomElement(namespaceURI, qualifiedName, getPage(), null);
418         }
419         return getScriptableFor(element);
420     }
421 
422     /**
423      * Returns all the descendant elements with the specified tag name.
424      * @param tagName the name to search for
425      * @return all the descendant elements with the specified tag name
426      */
427     public HTMLCollection jsxFunction_getElementsByTagName(final String tagName) {
428         final String description = "Document.getElementsByTagName('" + tagName + "')";
429 
430         final HTMLCollection collection;
431         if ("*".equals(tagName)) {
432             collection = new HTMLCollection(getDomNodeOrDie(), false, description) {
433                 @Override
434                 protected boolean isMatching(final DomNode node) {
435                     return true;
436                 }
437             };
438         }
439         else {
440             final boolean useLocalName = getBrowserVersion().hasFeature(BrowserVersionFeatures.GENERATED_32);
441             final String tagNameLC = tagName.toLowerCase();
442 
443             collection = new HTMLCollection(getDomNodeOrDie(), false, description) {
444                 @Override
445                 protected boolean isMatching(final DomNode node) {
446                     if (useLocalName) {
447                         return tagNameLC.equalsIgnoreCase(node.getLocalName());
448                     }
449                     return tagNameLC.equalsIgnoreCase(node.getNodeName());
450                 }
451             };
452         }
453 
454         return collection;
455     }
456 
457     /**
458      * Returns a list of elements with the given tag name belonging to the given namespace.
459      * @param namespaceURI the namespace URI of elements to look for
460      * @param localName is either the local name of elements to look for or the special value "*",
461      *                  which matches all elements.
462      * @return a live NodeList of found elements in the order they appear in the tree
463      */
464     public Object jsxFunction_getElementsByTagNameNS(final Object namespaceURI, final String localName) {
465         final String description = "Document.getElementsByTagNameNS('" + namespaceURI + "', '" + localName + "')";
466         final DomElement domNode = getPage().getDocumentElement();
467 
468         final String prefix;
469         if (namespaceURI != null && !"*".equals("*")) {
470             prefix = XmlUtil.lookupPrefix(domNode, Context.toString(namespaceURI));
471         }
472         else {
473             prefix = null;
474         }
475 
476         final HTMLCollection collection = new HTMLCollection(domNode, false, description) {
477             @Override
478             protected boolean isMatching(final DomNode node) {
479                 if (!localName.equals(node.getLocalName())) {
480                     return false;
481                 }
482                 if (prefix == null) {
483                     return true;
484                 }
485                 return true;
486             }
487         };
488 
489         return collection;
490     }
491 }