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.css;
16  
17  import java.io.ByteArrayInputStream;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.Reader;
21  import java.io.StringReader;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import net.sourceforge.htmlunit.corejs.javascript.Context;
30  
31  import org.apache.commons.io.IOUtils;
32  import org.apache.commons.lang.StringUtils;
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.w3c.css.sac.AttributeCondition;
36  import org.w3c.css.sac.CombinatorCondition;
37  import org.w3c.css.sac.Condition;
38  import org.w3c.css.sac.ConditionalSelector;
39  import org.w3c.css.sac.ContentCondition;
40  import org.w3c.css.sac.DescendantSelector;
41  import org.w3c.css.sac.ElementSelector;
42  import org.w3c.css.sac.ErrorHandler;
43  import org.w3c.css.sac.InputSource;
44  import org.w3c.css.sac.LangCondition;
45  import org.w3c.css.sac.NegativeCondition;
46  import org.w3c.css.sac.NegativeSelector;
47  import org.w3c.css.sac.Selector;
48  import org.w3c.css.sac.SelectorList;
49  import org.w3c.css.sac.SiblingSelector;
50  import org.w3c.dom.css.CSSImportRule;
51  import org.w3c.dom.css.CSSRule;
52  import org.w3c.dom.css.CSSRuleList;
53  
54  import com.gargoylesoftware.htmlunit.BrowserVersion;
55  import com.gargoylesoftware.htmlunit.BrowserVersionFeatures;
56  import com.gargoylesoftware.htmlunit.Cache;
57  import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
58  import com.gargoylesoftware.htmlunit.WebClient;
59  import com.gargoylesoftware.htmlunit.WebRequest;
60  import com.gargoylesoftware.htmlunit.WebResponse;
61  import com.gargoylesoftware.htmlunit.html.DomNode;
62  import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput;
63  import com.gargoylesoftware.htmlunit.html.HtmlElement;
64  import com.gargoylesoftware.htmlunit.html.HtmlHtml;
65  import com.gargoylesoftware.htmlunit.html.HtmlInput;
66  import com.gargoylesoftware.htmlunit.html.HtmlLink;
67  import com.gargoylesoftware.htmlunit.html.HtmlPage;
68  import com.gargoylesoftware.htmlunit.html.HtmlRadioButtonInput;
69  import com.gargoylesoftware.htmlunit.html.HtmlSelect;
70  import com.gargoylesoftware.htmlunit.html.HtmlStyle;
71  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
72  import com.gargoylesoftware.htmlunit.javascript.host.Window;
73  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
74  import com.gargoylesoftware.htmlunit.util.UrlUtils;
75  import com.steadystate.css.dom.CSSImportRuleImpl;
76  import com.steadystate.css.dom.CSSMediaRuleImpl;
77  import com.steadystate.css.dom.CSSStyleRuleImpl;
78  import com.steadystate.css.dom.CSSStyleSheetImpl;
79  import com.steadystate.css.parser.CSSOMParser;
80  import com.steadystate.css.parser.SACParserCSS21;
81  import com.steadystate.css.parser.SelectorListImpl;
82  
83  /**
84   * A JavaScript object for a Stylesheet.
85   *
86   * @see <a href="http://msdn2.microsoft.com/en-us/library/ms535871.aspx">MSDN doc</a>
87   * @version $Revision: 6491 $
88   * @author Marc Guillemot
89   * @author Daniel Gredler
90   * @author Ahmed Ashour
91   */
92  public class CSSStyleSheet extends SimpleScriptable {
93  
94      private static final Log LOG = LogFactory.getLog(CSSStyleSheet.class);
95  
96      /** The parsed stylesheet which this host object wraps. */
97      private final org.w3c.dom.css.CSSStyleSheet wrapped_;
98  
99      /** The HTML element which owns this stylesheet. */
100     private final HTMLElement ownerNode_;
101 
102     /** The collection of rules defined in this style sheet. */
103     private com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList cssRules_;
104 
105     /** The CSS import rules and their corresponding stylesheets. */
106     private Map<CSSImportRule, CSSStyleSheet> imports_ = new HashMap<CSSImportRule, CSSStyleSheet>();
107 
108     /** This stylesheet's URI (used to resolved contained @import rules). */
109     private String uri_;
110 
111     /**
112      * Creates a new empty stylesheet.
113      */
114     public CSSStyleSheet() {
115         wrapped_ = new CSSStyleSheetImpl();
116         ownerNode_ = null;
117     }
118 
119     /**
120      * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
121      * @param element the owning node
122      * @param source the input source which contains the CSS stylesheet which this stylesheet host object represents
123      * @param uri this stylesheet's URI (used to resolved contained @import rules)
124      */
125     public CSSStyleSheet(final HTMLElement element, final InputSource source, final String uri) {
126         setParentScope(element.getWindow());
127         setPrototype(getPrototype(CSSStyleSheet.class));
128         wrapped_ = parseCSS(source);
129         uri_ = uri;
130         ownerNode_ = element;
131     }
132 
133     /**
134      * Creates a new stylesheet representing the specified CSS stylesheet.
135      * @param element the owning node
136      * @param wrapped the CSS stylesheet which this stylesheet host object represents
137      * @param uri this stylesheet's URI (used to resolved contained @import rules)
138      */
139     public CSSStyleSheet(final HTMLElement element, final org.w3c.dom.css.CSSStyleSheet wrapped, final String uri) {
140         setParentScope(element.getWindow());
141         setPrototype(getPrototype(CSSStyleSheet.class));
142         wrapped_ = wrapped;
143         uri_ = uri;
144         ownerNode_ = element;
145     }
146 
147     /**
148      * Returns the wrapped stylesheet.
149      * @return the wrapped stylesheet
150      */
151     public org.w3c.dom.css.CSSStyleSheet getWrappedSheet() {
152         return wrapped_;
153     }
154 
155     /**
156      * Modifies the specified style object by adding any style rules which apply to the specified
157      * element.
158      *
159      * @param style the style to modify
160      * @param element the element to which style rules must apply in order for them to be added to
161      *        the specified style
162      */
163     public void modifyIfNecessary(final ComputedCSSStyleDeclaration style, final HTMLElement element) {
164         final CSSRuleList rules = getWrappedSheet().getCssRules();
165         modifyIfNecessary(style, element, rules, new HashSet<String>());
166     }
167 
168     private void modifyIfNecessary(final ComputedCSSStyleDeclaration style, final HTMLElement element,
169         final CSSRuleList rules, final Set<String> alreadyProcessing) {
170         if (rules == null) {
171             return;
172         }
173         final HtmlElement e = element.getDomNodeOrDie();
174         for (int i = 0; i < rules.getLength(); i++) {
175             final CSSRule rule = rules.item(i);
176             if (rule.getType() == CSSRule.STYLE_RULE) {
177                 final CSSStyleRuleImpl styleRule = (CSSStyleRuleImpl) rule;
178                 final SelectorList selectors = styleRule.getSelectors();
179                 for (int j = 0; j < selectors.getLength(); j++) {
180                     final Selector selector = selectors.item(j);
181                     final boolean selected = selects(selector, e);
182                     if (selected) {
183                         final org.w3c.dom.css.CSSStyleDeclaration dec = styleRule.getStyle();
184                         style.applyStyleFromSelector(dec, selector);
185                     }
186                 }
187             }
188             else if (rule.getType() == CSSRule.IMPORT_RULE) {
189                 final CSSImportRuleImpl importRule = (CSSImportRuleImpl) rule;
190                 CSSStyleSheet sheet = imports_.get(importRule);
191                 if (sheet == null) {
192                     // TODO: surely wrong: in which case is it null and why?
193                     final String uri = (uri_ != null) ? uri_
194                         : e.getPage().getWebResponse().getWebRequest().getUrl().toExternalForm();
195                     final String href = importRule.getHref();
196                     final String url = UrlUtils.resolveUrl(uri, href);
197                     sheet = loadStylesheet(getWindow(), ownerNode_, null, url);
198                     imports_.put(importRule, sheet);
199                 }
200 
201                 if (!alreadyProcessing.contains(sheet.getUri())) {
202                     final CSSRuleList sheetRules = sheet.getWrappedSheet().getCssRules();
203                     alreadyProcessing.add(getUri());
204                     sheet.modifyIfNecessary(style, element, sheetRules, alreadyProcessing);
205                 }
206             }
207             else if (rule.getType() == CSSRule.MEDIA_RULE) {
208                 final CSSMediaRuleImpl mediaRule = (CSSMediaRuleImpl) rule;
209                 final String media = mediaRule.getMedia().getMediaText();
210                 if (isActive(media)) {
211                     final CSSRuleList internalRules = mediaRule.getCssRules();
212                     modifyIfNecessary(style, element, internalRules, alreadyProcessing);
213                 }
214             }
215         }
216     }
217 
218     /**
219      * Loads the stylesheet at the specified link or href.
220      * @param window the current window
221      * @param element the parent DOM element
222      * @param link the stylesheet's link (may be <tt>null</tt> if an <tt>href</tt> is specified)
223      * @param url the stylesheet's url (may be <tt>null</tt> if a <tt>link</tt> is specified)
224      * @return the loaded stylesheet
225      */
226     public static CSSStyleSheet loadStylesheet(final Window window, final HTMLElement element,
227         final HtmlLink link, final String url) {
228         CSSStyleSheet sheet;
229         final HtmlPage page = (HtmlPage) element.getDomNodeOrDie().getPage(); // fallback uri for exceptions
230         String uri = page.getWebResponse().getWebRequest().getUrl().toExternalForm();
231         try {
232             // Retrieve the associated content and respect client settings regarding failing HTTP status codes.
233             final WebRequest request;
234             final WebClient client = page.getWebClient();
235             if (link != null) {
236                 // Use link.
237                 request = link.getWebRequest();
238             }
239             else {
240                 // Use href.
241                 request = new WebRequest(new URL(url));
242                 final String referer = page.getWebResponse().getWebRequest().getUrl().toExternalForm();
243                 request.setAdditionalHeader("Referer", referer);
244             }
245 
246             uri = request.getUrl().toExternalForm();
247             final Cache cache = client.getCache();
248             final Object fromCache = cache.getCachedObject(request);
249             if (fromCache != null && fromCache instanceof org.w3c.dom.css.CSSStyleSheet) {
250                 sheet = new CSSStyleSheet(element, (org.w3c.dom.css.CSSStyleSheet) fromCache, uri);
251             }
252             else {
253                 final WebResponse response = client.loadWebResponse(request);
254                 uri = response.getWebRequest().getUrl().toExternalForm();
255                 client.printContentIfNecessary(response);
256                 client.throwFailingHttpStatusCodeExceptionIfNecessary(response);
257                 // CSS content must have downloaded OK; go ahead and build the corresponding stylesheet.
258                 final InputSource source = new InputSource();
259                 source.setByteStream(response.getContentAsStream());
260                 source.setEncoding(response.getContentCharset());
261                 sheet = new CSSStyleSheet(element, source, uri);
262                 cache.cacheIfPossible(request, response, sheet.getWrappedSheet());
263             }
264         }
265         catch (final FailingHttpStatusCodeException e) {
266             // Got a 404 response or something like that; behave nicely.
267             LOG.error(e.getMessage());
268             final InputSource source = new InputSource(new StringReader(""));
269             sheet = new CSSStyleSheet(element, source, uri);
270         }
271         catch (final IOException e) {
272             // Got a basic IO error; behave nicely.
273             LOG.error(e.getMessage());
274             final InputSource source = new InputSource(new StringReader(""));
275             sheet = new CSSStyleSheet(element, source, uri);
276         }
277         catch (final Exception e) {
278             // Got something unexpected; we can throw an exception in this case.
279             throw Context.reportRuntimeError("Exception: " + e);
280         }
281         return sheet;
282     }
283 
284     /**
285      * Returns <tt>true</tt> if the specified selector selects the specified element.
286      *
287      * @param selector the selector to test
288      * @param element the element to test
289      * @return <tt>true</tt> if it does apply, <tt>false</tt> if it doesn't apply
290      */
291     boolean selects(final Selector selector, final HtmlElement element) {
292         return selects(getBrowserVersion(), selector, element);
293     }
294 
295     /**
296      * Returns <tt>true</tt> if the specified selector selects the specified element.
297      *
298      * @param browserVersion the browser version
299      * @param selector the selector to test
300      * @param element the element to test
301      * @return <tt>true</tt> if it does apply, <tt>false</tt> if it doesn't apply
302      */
303     public static boolean selects(final BrowserVersion browserVersion, final Selector selector,
304             final HtmlElement element) {
305         switch (selector.getSelectorType()) {
306             case Selector.SAC_ANY_NODE_SELECTOR:
307                 return true;
308             case Selector.SAC_CHILD_SELECTOR:
309                 if (element.getParentNode() == element.getPage()) {
310                     return false;
311                 }
312                 final DescendantSelector cs = (DescendantSelector) selector;
313                 if (!(element.getParentNode() instanceof HtmlElement)) {
314                     return false; // for instance parent is a DocumentFragment
315                 }
316                 final HtmlElement parent = (HtmlElement) element.getParentNode();
317                 return selects(browserVersion, cs.getSimpleSelector(), element) && parent != null
318                     && selects(browserVersion, cs.getAncestorSelector(), parent);
319             case Selector.SAC_DESCENDANT_SELECTOR:
320                 final DescendantSelector ds = (DescendantSelector) selector;
321                 if (selects(browserVersion, ds.getSimpleSelector(), element)) {
322                     DomNode ancestor = element.getParentNode();
323                     while (ancestor instanceof HtmlElement) {
324                         if (selects(browserVersion, ds.getAncestorSelector(), (HtmlElement) ancestor)) {
325                             return true;
326                         }
327                         ancestor = ancestor.getParentNode();
328                     }
329                 }
330                 return false;
331             case Selector.SAC_CONDITIONAL_SELECTOR:
332                 final ConditionalSelector conditional = (ConditionalSelector) selector;
333                 final Condition condition = conditional.getCondition();
334                 return selects(browserVersion, conditional.getSimpleSelector(), element)
335                     && selects(browserVersion, condition, element);
336             case Selector.SAC_ELEMENT_NODE_SELECTOR:
337                 final ElementSelector es = (ElementSelector) selector;
338                 final String name = es.getLocalName();
339                 return name == null || name.equalsIgnoreCase(element.getTagName());
340             case Selector.SAC_ROOT_NODE_SELECTOR:
341                 return HtmlHtml.TAG_NAME.equalsIgnoreCase(element.getTagName());
342             case Selector.SAC_DIRECT_ADJACENT_SELECTOR:
343                 final SiblingSelector ss = (SiblingSelector) selector;
344                 final DomNode prev = element.getPreviousSibling();
345                 return prev instanceof HtmlElement
346                     && selects(browserVersion, ss.getSelector(), (HtmlElement) prev)
347                     && selects(browserVersion, ss.getSiblingSelector(), element);
348             case Selector.SAC_NEGATIVE_SELECTOR:
349                 final NegativeSelector ns = (NegativeSelector) selector;
350                 return !selects(browserVersion, ns.getSimpleSelector(), element);
351             case Selector.SAC_PSEUDO_ELEMENT_SELECTOR:
352             case Selector.SAC_COMMENT_NODE_SELECTOR:
353             case Selector.SAC_CDATA_SECTION_NODE_SELECTOR:
354             case Selector.SAC_PROCESSING_INSTRUCTION_NODE_SELECTOR:
355             case Selector.SAC_TEXT_NODE_SELECTOR:
356                 return false;
357             default:
358                 LOG.error("Unknown CSS selector type '" + selector.getSelectorType() + "'.");
359                 return false;
360         }
361     }
362 
363     /**
364      * Returns <tt>true</tt> if the specified condition selects the specified element.
365      *
366      * @param browserVersion the browser version
367      * @param condition the condition to test
368      * @param element the element to test
369      * @return <tt>true</tt> if it does apply, <tt>false</tt> if it doesn't apply
370      */
371     static boolean selects(final BrowserVersion browserVersion, final Condition condition, final HtmlElement element) {
372         switch (condition.getConditionType()) {
373             case Condition.SAC_ID_CONDITION:
374                 final AttributeCondition ac4 = (AttributeCondition) condition;
375                 return ac4.getValue().equals(element.getId());
376             case Condition.SAC_CLASS_CONDITION:
377                 final AttributeCondition ac3 = (AttributeCondition) condition;
378                 final String v3 = ac3.getValue();
379                 final String a3 = element.getAttribute("class");
380                 return a3.equals(v3) || a3.startsWith(v3 + " ") || a3.endsWith(" " + v3) || a3.contains(" " + v3 + " ");
381             case Condition.SAC_AND_CONDITION:
382                 final CombinatorCondition cc1 = (CombinatorCondition) condition;
383                 return selects(browserVersion, cc1.getFirstCondition(), element)
384                     && selects(browserVersion, cc1.getSecondCondition(), element);
385             case Condition.SAC_ATTRIBUTE_CONDITION:
386                 final AttributeCondition ac1 = (AttributeCondition) condition;
387                 if (ac1.getSpecified()) {
388                     return element.getAttribute(ac1.getLocalName()).equals(ac1.getValue());
389                 }
390                 return element.hasAttribute(ac1.getLocalName());
391             case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
392                 final AttributeCondition ac2 = (AttributeCondition) condition;
393                 final String v = ac2.getValue();
394                 final String a = element.getAttribute(ac2.getLocalName());
395                 return a.equals(v) || a.startsWith(v + "-") || a.endsWith("-" + v) || a.contains("-" + v + "-");
396             case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION:
397                 final AttributeCondition ac5 = (AttributeCondition) condition;
398                 final String v2 = ac5.getValue();
399                 final String a2 = element.getAttribute(ac5.getLocalName());
400                 return a2.equals(v2) || a2.startsWith(v2 + " ") || a2.endsWith(" " + v2) || a2.contains(" " + v2 + " ");
401             case Condition.SAC_OR_CONDITION:
402                 final CombinatorCondition cc2 = (CombinatorCondition) condition;
403                 return selects(browserVersion, cc2.getFirstCondition(), element)
404                     || selects(browserVersion, cc2.getSecondCondition(), element);
405             case Condition.SAC_NEGATIVE_CONDITION:
406                 final NegativeCondition nc = (NegativeCondition) condition;
407                 return !selects(browserVersion, nc.getCondition(), element);
408             case Condition.SAC_ONLY_CHILD_CONDITION:
409                 return element.getParentNode().getChildNodes().getLength() == 1;
410             case Condition.SAC_CONTENT_CONDITION:
411                 final ContentCondition cc = (ContentCondition) condition;
412                 return element.asText().contains(cc.getData());
413             case Condition.SAC_LANG_CONDITION:
414                 if (!browserVersion.hasFeature(BrowserVersionFeatures.CSS_SELECTOR_LANG)) {
415                     return false;
416                 }
417                 final String lcLang = ((LangCondition) condition).getLang();
418                 for (DomNode node = element; node instanceof HtmlElement; node = node.getParentNode()) {
419                     final String nodeLang = ((HtmlElement) node).getAttribute("lang");
420                     // "en", "en-GB" should be matched by "en" but not "english"
421                     if (nodeLang.startsWith(lcLang)
422                         && (nodeLang.length() == lcLang.length() || '-' == nodeLang.charAt(lcLang.length()))) {
423                         return true;
424                     }
425                 }
426                 return false;
427             case Condition.SAC_ONLY_TYPE_CONDITION:
428                 final String tagName = element.getTagName();
429                 return ((HtmlPage) element.getPage()).getElementsByTagName(tagName).getLength() == 1;
430             case Condition.SAC_PSEUDO_CLASS_CONDITION:
431                 return selectsPseudoClass(browserVersion, (AttributeCondition) condition, element);
432             case Condition.SAC_POSITIONAL_CONDITION:
433                 return false;
434             default:
435                 LOG.error("Unknown CSS condition type '" + condition.getConditionType() + "'.");
436                 return false;
437         }
438     }
439 
440     private static boolean selectsPseudoClass(final BrowserVersion browserVersion,
441             final AttributeCondition condition, final HtmlElement element) {
442         if (!browserVersion.hasFeature(BrowserVersionFeatures.CSS_SPECIAL_PSEUDO_CLASSES)) {
443             return false;
444         }
445 
446         final String value = condition.getValue();
447         if ("root".equals(value)) {
448             return element == element.getPage().getDocumentElement();
449         }
450         else if ("enabled".equals(value)) {
451             return (element instanceof HtmlInput && !((HtmlInput) element).isDisabled())
452                 || (element instanceof HtmlSelect && !((HtmlSelect) element).isDisabled());
453         }
454         else if ("disabled".equals(value)) {
455             return (element instanceof HtmlInput && ((HtmlInput) element).isDisabled())
456                 || (element instanceof HtmlSelect && ((HtmlSelect) element).isDisabled());
457         }
458         else if ("checked".equals(value)) {
459             return (element instanceof HtmlCheckBoxInput && ((HtmlCheckBoxInput) element).isChecked())
460                 || (element instanceof HtmlRadioButtonInput && ((HtmlRadioButtonInput) element).isChecked());
461         }
462         return false;
463     }
464 
465     /**
466      * Parses the CSS at the specified input source. If anything at all goes wrong, this method
467      * returns an empty stylesheet.
468      *
469      * @param source the source from which to retrieve the CSS to be parsed
470      * @return the stylesheet parsed from the specified input source
471      */
472     private org.w3c.dom.css.CSSStyleSheet parseCSS(final InputSource source) {
473         org.w3c.dom.css.CSSStyleSheet ss;
474         try {
475             final ErrorHandler errorHandler = getWindow().getWebWindow().getWebClient().getCssErrorHandler();
476             final CSSOMParser parser = new CSSOMParser(new SACParserCSS21());
477             parser.setErrorHandler(errorHandler);
478             ss = parser.parseStyleSheet(source, null, null);
479         }
480         catch (final Exception e) {
481             LOG.error("Error parsing CSS from '" + toString(source) + "': " + e.getMessage(), e);
482             ss = new CSSStyleSheetImpl();
483         }
484         catch (final Error e) {
485             // SACParser sometimes throws Error: "Missing return statement in function"
486             LOG.error("Error parsing CSS from '" + toString(source) + "': " + e.getMessage(), e);
487             ss = new CSSStyleSheetImpl();
488         }
489         return ss;
490     }
491 
492     /**
493      * Parses the selectors at the specified input source. If anything at all goes wrong, this
494      * method returns an empty selector list.
495      *
496      * @param source the source from which to retrieve the selectors to be parsed
497      * @return the selectors parsed from the specified input source
498      */
499     public SelectorList parseSelectors(final InputSource source) {
500         SelectorList selectors;
501         try {
502             final ErrorHandler errorHandler = getWindow().getWebWindow().getWebClient().getCssErrorHandler();
503             final CSSOMParser parser = new CSSOMParser(new SACParserCSS21());
504             parser.setErrorHandler(errorHandler);
505             selectors = parser.parseSelectors(source);
506             // in case of error parseSelectors returns null
507             if (null == selectors) {
508                 selectors = new SelectorListImpl();
509             }
510         }
511         catch (final Exception e) {
512             LOG.error("Error parsing CSS selectors from '" + toString(source) + "': " + e.getMessage(), e);
513             selectors = new SelectorListImpl();
514         }
515         catch (final Error e) {
516             // SACParser sometimes throws Error: "Missing return statement in function"
517             LOG.error("Error parsing CSS selectors from '" + toString(source) + "': " + e.getMessage(), e);
518             selectors = new SelectorListImpl();
519         }
520         return selectors;
521     }
522 
523     /**
524      * Returns the contents of the specified input source, ignoring any {@link IOException}s.
525      * @param source the input source from which to read
526      * @return the contents of the specified input source, or an empty string if an {@link IOException} occurs
527      */
528     private static String toString(final InputSource source) {
529         try {
530             final Reader reader = source.getCharacterStream();
531             if (null != reader) {
532                 // try to reset to produce some output
533                 if (reader instanceof StringReader) {
534                     final StringReader sr = (StringReader) reader;
535                     sr.reset();
536                 }
537                 return IOUtils.toString(reader);
538             }
539             final InputStream is = source.getByteStream();
540             if (null != is) {
541                 // try to reset to produce some output
542                 if (is instanceof ByteArrayInputStream) {
543                     final ByteArrayInputStream bis = (ByteArrayInputStream) is;
544                     bis.reset();
545                 }
546                 return IOUtils.toString(is);
547             }
548             return "";
549         }
550         catch (final IOException e) {
551             return "";
552         }
553     }
554 
555     /**
556      * For Firefox.
557      * @return the owner
558      */
559     public HTMLElement jsxGet_ownerNode() {
560         return ownerNode_;
561     }
562 
563     /**
564      * For Internet Explorer.
565      * @return the owner
566      */
567     public HTMLElement jsxGet_owningElement() {
568         return ownerNode_;
569     }
570 
571     /**
572      * Retrieves the collection of rules defined in this style sheet.
573      * @return the collection of rules defined in this style sheet
574      */
575     public com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList jsxGet_rules() {
576         return jsxGet_cssRules();
577     }
578 
579     /**
580      * Returns the collection of rules defined in this style sheet.
581      * @return the collection of rules defined in this style sheet
582      */
583     public com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList jsxGet_cssRules() {
584         if (cssRules_ == null) {
585             cssRules_ = new com.gargoylesoftware.htmlunit.javascript.host.css.CSSRuleList(this);
586         }
587         return cssRules_;
588     }
589 
590     /**
591      * Returns the URL of the stylesheet.
592      * @return the URL of the stylesheet
593      */
594     public String jsxGet_href() {
595         final BrowserVersion version = getBrowserVersion();
596 
597         if (ownerNode_ != null) {
598             final DomNode node = ownerNode_.getDomNodeOrDie();
599             if (node instanceof HtmlLink) {
600                 // <link rel="stylesheet" type="text/css" href="..." />
601                 final HtmlLink link = (HtmlLink) node;
602                 final HtmlPage page = (HtmlPage) link.getPage();
603                 final String href = link.getHrefAttribute();
604                 if (!version.hasFeature(BrowserVersionFeatures.STYLESHEET_HREF_EXPANDURL)) {
605                     // Don't expand relative URLs.
606                     return href;
607                 }
608                 // Expand relative URLs.
609                 try {
610                     final URL url = page.getFullyQualifiedUrl(href);
611                     return url.toExternalForm();
612                 }
613                 catch (final MalformedURLException e) {
614                     // Log the error and fall through to the return values below.
615                     LOG.warn(e.getMessage(), e);
616                 }
617             }
618         }
619 
620         // <style type="text/css"> ... </style>
621         if (version.hasFeature(BrowserVersionFeatures.STYLESHEET_HREF_STYLE_EMPTY)) {
622             return "";
623         }
624         else if (version.hasFeature(BrowserVersionFeatures.STYLESHEET_HREF_STYLE_NULL)) {
625             return null;
626         }
627         else {
628             final DomNode node = ownerNode_.getDomNodeOrDie();
629             final HtmlPage page = (HtmlPage) node.getPage();
630             final URL url = page.getWebResponse().getWebRequest().getUrl();
631             return url.toExternalForm();
632         }
633     }
634 
635     /**
636      * Inserts a new rule.
637      * @param rule the CSS rule
638      * @param position the position at which to insert the rule
639      * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a>
640      * @return the position of the inserted rule
641      */
642     public int jsxFunction_insertRule(final String rule, final int position) {
643         return wrapped_.insertRule(rule.trim(), position);
644     }
645 
646     /**
647      * Deletes an existing rule.
648      * @param position the position of the rule to be deleted
649      * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a>
650      */
651     public void jsxFunction_deleteRule(final int position) {
652         wrapped_.deleteRule(position);
653     }
654 
655     /**
656      * Adds a new rule.
657      * @see <a href="http://msdn.microsoft.com/en-us/library/aa358796.aspx">MSDN</a>
658      * @param selector the selector name
659      * @param rule the rule
660      * @return always return -1 as of MSDN documentation
661      */
662     public int jsxFunction_addRule(final String selector, final String rule) {
663         final String completeRule = selector.trim() + " {" + rule + "}";
664         wrapped_.insertRule(completeRule, wrapped_.getCssRules().getLength());
665         return -1;
666     }
667 
668     /**
669      * Deletes an existing rule.
670      * @param position the position of the rule to be deleted
671      * @see <a href="http://msdn.microsoft.com/en-us/library/ms531195(v=VS.85).aspx">MSDN</a>
672      */
673     public void jsxFunction_removeRule(final int position) {
674         wrapped_.deleteRule(position);
675     }
676 
677     /**
678      * Returns this stylesheet's URI (used to resolved contained @import rules).
679      * @return this stylesheet's URI (used to resolved contained @import rules)
680      */
681     public String getUri() {
682         return uri_;
683     }
684 
685     /**
686      * Returns <tt>true</tt> if this stylesheet is active, based on the media types it is associated with (if any).
687      * @return <tt>true</tt> if this stylesheet is active, based on the media types it is associated with (if any)
688      */
689     public boolean isActive() {
690         final String media;
691         final HtmlElement e = ownerNode_.getDomNodeOrNull();
692         if (e instanceof HtmlStyle) {
693             final HtmlStyle style = (HtmlStyle) e;
694             media = style.getMediaAttribute();
695         }
696         else if (e instanceof HtmlLink) {
697             final HtmlLink link = (HtmlLink) e;
698             media = link.getMediaAttribute();
699         }
700         else {
701             media = "";
702         }
703         return isActive(media);
704     }
705 
706     private static boolean isActive(final String media) {
707         if (StringUtils.isBlank(media)) {
708             return true;
709         }
710         for (String s : StringUtils.split(media, ',')) {
711             final String mediaType = s.trim();
712             if ("screen".equals(mediaType) || "all".equals(mediaType)) {
713                 return true;
714             }
715         }
716         return false;
717     }
718 
719 }