1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.css;
  27 
  28 import javafx.css.Styleable;
  29 import java.io.FileNotFoundException;
  30 import java.io.FilePermission;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.lang.ref.Reference;
  34 import java.lang.ref.WeakReference;
  35 import java.net.MalformedURLException;
  36 import java.net.URI;
  37 import java.net.URISyntaxException;
  38 import java.net.URL;
  39 import java.security.AccessControlContext;
  40 import java.security.AccessController;
  41 import java.security.DigestInputStream;
  42 import java.security.MessageDigest;
  43 import java.security.NoSuchAlgorithmException;
  44 import java.security.PermissionCollection;
  45 import java.security.PrivilegedAction;
  46 import java.security.PrivilegedActionException;
  47 import java.security.PrivilegedExceptionAction;
  48 import java.security.ProtectionDomain;
  49 import java.util.*;
  50 import java.util.jar.JarEntry;
  51 import java.util.jar.JarFile;
  52 import javafx.collections.FXCollections;
  53 import javafx.collections.ListChangeListener.Change;
  54 import javafx.collections.ObservableList;
  55 import javafx.scene.Node;
  56 import javafx.scene.Parent;
  57 import javafx.scene.Scene;
  58 import javafx.scene.SubScene;
  59 import javafx.scene.text.Font;
  60 import javafx.stage.Window;
  61 import com.sun.javafx.css.parser.CSSParser;
  62 import java.util.Map.Entry;
  63 
  64 import javafx.css.CssMetaData;
  65 import javafx.css.PseudoClass;
  66 import javafx.css.StyleOrigin;
  67 import javafx.scene.image.Image;
  68 import sun.util.logging.PlatformLogger;
  69 import sun.util.logging.PlatformLogger.Level;
  70 
  71 /**
  72  * Contains the stylesheet state for a single scene. This includes both the
  73  * Stylesheets defined on the Scene itself as well as a map of stylesheets for
  74  * "style"s defined on the Node itself. These containers are kept in the
  75  * containerMap, key'd by the Scene to which they belong. <p> One of the key
  76  * responsibilities of the StylesheetContainer is to create and maintain an
  77  * admittedly elaborate series of caches so as to minimize the amount of time it
  78  * takes to match a Node to its eventual StyleHelper, and to reuse the
  79  * StyleHelper as much as possible. <p> Initially, the cache is empty. It is
  80  * recreated whenever the userStylesheets on the container change, or whenever
  81  * the userAgentStylesheet changes. The cache is built up as nodes are looked
  82  * for, and thus there is some overhead associated with the first lookup but
  83  * which is then not repeated for subsequent lookups. <p> The cache system used
  84  * is a two level cache. The first level cache simply maps the
  85  * classname/id/styleclass combination of the request node to a 2nd level cache.
  86  * If the node has "styles" specified then we still use this 2nd level cache,
  87  * but must combine its selectors with the selectors specified in "styles" and perform
  88  * more work to cascade properly. <p> The 2nd level cache contains a data
  89  * structure called the Cache. The Cache contains an ordered sequence of Rules,
  90  * a Long, and a Map. The ordered sequence of selectors are the selectors that *may*
  91  * match a node with the given classname, id, and style class. For example,
  92  * selectors which may apply are any selector where the simple selector of the selector
  93  * contains a reference to the id, style class, or classname of the Node, or a
  94  * compound selector who's "descendant" part is a simple selector which contains
  95  * a reference to the id, style class, or classname of the Node. <p> During
  96  * lookup, we will iterate over all the potential selectors and discover if they
  97  * apply to this particular node. If so, then we toggle a bit position in the
  98  * Long corresponding to the position of the selector that matched. This long then
  99  * becomes our key into the final map. <p> Once we have established our key, we
 100  * will visit the map and look for an existing StyleHelper. If we find a
 101  * StyleHelper, then we will return it. If not, then we will take the Rules that
 102  * matched and construct a new StyleHelper from their various parts. <p> This
 103  * system, while elaborate, also provides for numerous fast paths and sharing of
 104  * data structures which should dramatically reduce the memory and runtime
 105  * performance overhead associated with CSS by reducing the matching overhead
 106  * and caching as much as possible. We make no attempt to use weak references
 107  * here, so if memory issues result one work around would be to toggle the root
 108  * user agent stylesheet or stylesheets on the scene to cause the cache to be
 109  * flushed.
 110  */
 111 
 112 final public class StyleManager {
 113 
 114     private static PlatformLogger LOGGER;
 115     private static PlatformLogger getLogger() {
 116         if (LOGGER == null) {
 117             LOGGER = com.sun.javafx.Logging.getCSSLogger();
 118         }
 119         return LOGGER;
 120     }
 121 
 122     private static class InstanceHolder {
 123         final static StyleManager INSTANCE = new StyleManager();
 124     }
 125     /**
 126      * Return the StyleManager instance.
 127      */
 128     public static StyleManager getInstance() {
 129         return InstanceHolder.INSTANCE;
 130     }
 131 
 132     /**
 133      *
 134      * @param styleable
 135      * @return
 136      * @deprecated Use {@link javafx.css.Styleable#getCssMetaData()}
 137      */
 138     // TODO: is this used anywhere?
 139     @Deprecated public static List<CssMetaData<? extends Styleable, ?>> getStyleables(final Styleable styleable) {
 140 
 141         return styleable != null
 142             ? styleable.getCssMetaData()
 143             : Collections.<CssMetaData<? extends Styleable, ?>>emptyList();
 144     }
 145 
 146     /**
 147      *
 148      * @param node
 149      * @return
 150      * @deprecated Use {@link javafx.scene.Node#getCssMetaData()}
 151      */
 152     // TODO: is this used anywhere?
 153     @Deprecated public static List<CssMetaData<? extends Styleable, ?>> getStyleables(final Node node) {
 154 
 155         return node != null
 156             ? node.getCssMetaData()
 157             : Collections.<CssMetaData<? extends Styleable, ?>>emptyList();
 158     }
 159 
 160     private StyleManager() {
 161     }
 162 
 163     /**
 164      * A map from a parent to its style cache. The parent is either a Scene root, or a
 165      * Parent with author stylesheets. If a Scene or Parent is removed from the scene,
 166      * it's cache is annihilated.
 167      */
 168     // package for testing
 169     static final Map<Parent, CacheContainer> cacheContainerMap = new WeakHashMap<>();
 170 
 171     // package for testing
 172     CacheContainer getCacheContainer(Styleable styleable, SubScene subScene) {
 173 
 174         if (styleable == null && subScene == null) return null;
 175 
 176         Parent root = null;
 177 
 178         if (subScene != null) {
 179             root = subScene.getRoot();
 180 
 181         } else if (styleable instanceof Node) {
 182 
 183             Node node = (Node)styleable;
 184             Scene scene = node.getScene();
 185             if (scene != null) root = scene.getRoot();
 186 
 187         } else if (styleable instanceof Window) {
 188             // this catches the PopupWindow case
 189             Scene scene = ((Window)styleable).getScene();
 190             if (scene != null) root = scene.getRoot();
 191         }
 192         // todo: what other Styleables need to be handled here?
 193 
 194         if (root == null) return null;
 195 
 196         CacheContainer container = cacheContainerMap.computeIfAbsent(
 197                 root,
 198                 (key) -> {
 199                     return new CacheContainer();
 200                 }
 201         );
 202 
 203         return container;
 204 
 205     }
 206     /** 
 207      * StyleHelper uses this cache but it lives here so it can be cleared
 208      * when style-sheets change.
 209      */
 210     public StyleCache getSharedCache(Styleable styleable, SubScene subScene, StyleCache.Key key) {
 211 
 212         CacheContainer container = getCacheContainer(styleable, subScene);
 213         if (container == null) return null;
 214 
 215         Map<StyleCache.Key,StyleCache> styleCache = container.getStyleCache();
 216         if (styleCache == null) return null;
 217 
 218         StyleCache sharedCache = styleCache.get(key);
 219         if (sharedCache == null) {
 220             sharedCache = new StyleCache();
 221             styleCache.put(new StyleCache.Key(key), sharedCache);
 222         }
 223 
 224         return sharedCache;
 225     }
 226     
 227     public StyleMap getStyleMap(Styleable styleable, SubScene subScene, int smapId) {
 228 
 229         if (smapId == -1) return StyleMap.EMPTY_MAP;
 230         
 231         CacheContainer container = getCacheContainer(styleable, subScene);
 232         if (container == null) return StyleMap.EMPTY_MAP;
 233 
 234         return container.getStyleMap(smapId);
 235     }
 236 
 237     /**
 238      * A list of user-agent stylesheets from Scene or SubScene.
 239      * The order of the entries in this list does not matter since a Scene or
 240      * SubScene will only have zero or one user-agent stylesheets.
 241      */
 242     // package for testing
 243     final List<StylesheetContainer> userAgentStylesheetContainers = new ArrayList<>();
 244     /**
 245      * A list of user-agent stylesheet urls from calling setDefaultUserAgentStylesheet and
 246      * addUserAgentStylesheet. The order of entries this list matters. The zeroth element is
 247      * _the_ platform default.
 248      */
 249     // package for testing
 250     final List<StylesheetContainer> platformUserAgentStylesheetContainers = new ArrayList<>();
 251     // package for testing
 252     boolean hasDefaultUserAgentStylesheet = false;
 253 
 254     ////////////////////////////////////////////////////////////////////////////
 255     //
 256     // stylesheet handling
 257     //
 258     ////////////////////////////////////////////////////////////////////////////
 259 
 260     /*
 261      * A container for stylesheets and the Parents or Scenes that use them.
 262      * If a stylesheet is removed, then all other Parents or Scenes
 263      * that use that stylesheet should get new styles if the
 264      * stylesheet is added back in since the stylesheet may have been
 265      * removed and re-added because it was edited (typical of SceneBuilder).
 266      * This container provides the hooks to get back to those Parents or Scenes.
 267      *
 268      * StylesheetContainer<Parent> are created and added to stylesheetContainerMap
 269      * in the method gatherParentStylesheets.
 270      *
 271      * StylesheetContainer<Scene> are created and added to sceneStylesheetMap in
 272      * the method updateStylesheets
 273      */
 274     // package for testing
 275     static class StylesheetContainer {
 276 
 277         // the stylesheet uri
 278         final String fname;
 279         // the parsed stylesheet so we don't reparse for every parent that uses it
 280         final Stylesheet stylesheet;
 281         // the parents or scenes that use this stylesheet. Typically, this list
 282         //  should be very small.
 283         final SelectorPartitioning selectorPartitioning;
 284 
 285         // who uses this stylesheet?
 286         final RefList<Parent> parentUsers;
 287 
 288         // RT-24516 -- cache images coming from this stylesheet.
 289         // This just holds a hard reference to the image.
 290         final List<Image> imageCache;
 291 
 292         final int hash;
 293         final byte[] checksum;
 294         boolean checksumInvalid = false;
 295 
 296         StylesheetContainer(String fname, Stylesheet stylesheet) {
 297             this(fname, stylesheet, stylesheet != null ? calculateCheckSum(stylesheet.getUrl()) : new byte[0]);
 298         }
 299 
 300         StylesheetContainer(String fname, Stylesheet stylesheet, byte[] checksum) {
 301 
 302             this.fname = fname;
 303             hash = (fname != null) ? fname.hashCode() : 127;
 304 
 305             this.stylesheet = stylesheet;
 306             if (stylesheet != null) {
 307                 selectorPartitioning = new SelectorPartitioning();
 308                 final List<Rule> rules = stylesheet.getRules();
 309                 final int rMax = rules == null || rules.isEmpty() ? 0 : rules.size();
 310                 for (int r=0; r<rMax; r++) {
 311 
 312                     final Rule rule = rules.get(r);
 313                     final List<Selector> selectors = rule.getUnobservedSelectorList();
 314                     final int sMax = selectors == null || selectors.isEmpty() ? 0 : selectors.size();
 315                     for (int s=0; s < sMax; s++) {
 316 
 317                         final Selector selector = selectors.get(s);
 318                         selectorPartitioning.partition(selector);
 319 
 320                     }
 321                 }
 322 
 323             } else {
 324                 selectorPartitioning = null;
 325             }
 326 
 327             this.parentUsers = new RefList<Parent>();
 328 
 329             // this just holds a hard reference to the image
 330             this.imageCache = new ArrayList<Image>();
 331 
 332             this.checksum = checksum;
 333         }
 334 
 335         void invalidateChecksum() {
 336             // if checksum is byte[0], then it is forever valid.
 337             checksumInvalid = checksum.length > 0 ? true : false;
 338         }
 339         @Override
 340         public int hashCode() {
 341             return hash;
 342         }
 343 
 344         @Override
 345         public boolean equals(Object obj) {
 346             if (obj == null) {
 347                 return false;
 348             }
 349             if (getClass() != obj.getClass()) {
 350                 return false;
 351             }
 352             final StylesheetContainer other = (StylesheetContainer) obj;
 353             if ((this.fname == null) ? (other.fname != null) : !this.fname.equals(other.fname)) {
 354                 return false;
 355             }
 356             return true;
 357         }
 358 
 359         @Override public String toString() {
 360             return fname;
 361         }
 362 
 363     }
 364 
 365     /*
 366      * A list that holds references. Used by StylesheetContainer.
 367      */
 368     // package for testing
 369     static class RefList<K> {
 370 
 371         final List<Reference<K>> list = new ArrayList<Reference<K>>();
 372 
 373         void add(K key) {
 374 
 375             for (int n=list.size()-1; 0<=n; --n) {
 376                 final Reference<K> ref = list.get(n);
 377                 final K k = ref.get();
 378                 if (k == null) {
 379                     // stale reference, remove it.
 380                     list.remove(n);
 381                 } else {
 382                     // already have it, bail
 383                     if (k == key) {
 384                         return;
 385                     }
 386                 }
 387             }
 388             // not found, add it.
 389             list.add(new WeakReference<K>(key));
 390         }
 391 
 392         void remove(K key) {
 393 
 394             for (int n=list.size()-1; 0<=n; --n) {
 395                 final Reference<K> ref = list.get(n);
 396                 final K k = ref.get();
 397                 if (k == null) {
 398                     // stale reference, remove it.
 399                     list.remove(n);
 400                 } else {
 401                     // already have it, bail
 402                     if (k == key) {
 403                         list.remove(n);
 404                         return;
 405                     }
 406                 }
 407             }
 408         }
 409 
 410         // for unit testing
 411         boolean contains(K key) {
 412             for (int n=list.size()-1; 0<=n; --n) {
 413                 final Reference<K> ref = list.get(n);
 414                 final K k = ref.get();
 415                 if (k == key) {
 416                     return true;
 417                 }
 418             }
 419             return false;
 420         }
 421     }
 422 
 423     /**
 424      * A map from String => Stylesheet. If a stylesheet for the
 425      * given URL has already been loaded then we'll simply reuse the stylesheet
 426      * rather than loading a duplicate.
 427      * This list is for author stylesheets and not for user-agent stylesheets. User-agent
 428      * stylesheets are either platformUserAgentStylesheetContainers or userAgentStylesheetContainers
 429      */
 430     // package for unit testing
 431     final Map<String,StylesheetContainer> stylesheetContainerMap = new HashMap<>();
 432 
 433 
 434     /**
 435      * called from Window when the scene is closed.
 436      */
 437     public void forget(final Scene scene) {
 438 
 439         if (scene == null) return;
 440 
 441         forget(scene.getRoot());
 442 
 443         //
 444         // if this scene has user-agent stylesheets, clean up the userAgentStylesheetContainers list
 445         //
 446         String sceneUserAgentStylesheet = null;
 447         if ((scene.getUserAgentStylesheet() != null) &&
 448                 (!(sceneUserAgentStylesheet = scene.getUserAgentStylesheet().trim()).isEmpty())) {
 449 
 450             for(int n=0,nMax=userAgentStylesheetContainers.size(); n<nMax; n++) {
 451                 StylesheetContainer container = userAgentStylesheetContainers.get(n);
 452                 if (sceneUserAgentStylesheet.equals(container.fname)) {
 453                     container.parentUsers.remove(scene.getRoot());
 454                     if (container.parentUsers.list.size() == 0) {
 455                         userAgentStylesheetContainers.remove(n);
 456                     }
 457                 }
 458             }
 459         }
 460 
 461         //
 462         // remove any parents belonging to this scene from the stylesheetContainerMap
 463         //
 464         Set<Entry<String,StylesheetContainer>> stylesheetContainers = stylesheetContainerMap.entrySet();
 465         Iterator<Entry<String,StylesheetContainer>> iter = stylesheetContainers.iterator();
 466 
 467         while(iter.hasNext()) {
 468 
 469             Entry<String,StylesheetContainer> entry = iter.next();
 470             StylesheetContainer container = entry.getValue();
 471 
 472             Iterator<Reference<Parent>> parentIter = container.parentUsers.list.iterator();
 473             while (parentIter.hasNext()) {
 474 
 475                 Reference<Parent> ref = parentIter.next();
 476                 Parent _parent = ref.get();
 477 
 478                 if (_parent == null || _parent.getScene() == scene || _parent.getScene() == null) {
 479                     ref.clear();
 480                     parentIter.remove();
 481                 }
 482             }
 483 
 484             if (container.parentUsers.list.isEmpty()) {
 485                 iter.remove();
 486             }
 487         }
 488 
 489     }
 490 
 491     /**
 492      * called from Scene's stylesheets property's onChanged method
 493      */
 494     public void stylesheetsChanged(Scene scene, Change<String> c) {
 495 
 496         // Clear the cache so the cache will be rebuilt.
 497         Set<Entry<Parent,CacheContainer>> entrySet = cacheContainerMap.entrySet();
 498         for(Entry<Parent,CacheContainer> entry : entrySet) {
 499             Parent parent = entry.getKey();
 500             CacheContainer container = entry.getValue();
 501             if (parent.getScene() == scene) {
 502                 container.clearCache();
 503             }
 504 
 505         }
 506 
 507         c.reset();
 508         while(c.next()) {
 509             if (c.wasRemoved()) {
 510                 for (String fname : c.getRemoved()) {
 511                     stylesheetRemoved(scene, fname);
 512 
 513                     StylesheetContainer stylesheetContainer = stylesheetContainerMap.get(fname);
 514                     if (stylesheetContainer != null) {
 515                         stylesheetContainer.invalidateChecksum();
 516                     }
 517 
 518                 }
 519             }
 520         }
 521 
 522     }
 523 
 524     private void stylesheetRemoved(Scene scene, String fname) {
 525         stylesheetRemoved(scene.getRoot(), fname);
 526     }
 527 
 528     /**
 529      * Called from Parent's scenesChanged method when the Parent's scene is set to null.
 530      * @param parent The Parent being removed from the scene-graph
 531      */
 532     public void forget(Parent parent) {
 533 
 534         if (parent == null) return;
 535 
 536         // RT-34863 - clean up CSS cache when Parent is removed from scene-graph
 537         Set<Entry<Parent, CacheContainer>> entrySet = cacheContainerMap.entrySet();
 538         Iterator<Entry<Parent, CacheContainer>> iterator = entrySet.iterator();
 539         while (iterator.hasNext()) {
 540             Entry<Parent, CacheContainer> entry = iterator.next();
 541             Parent key = entry.getKey();
 542             CacheContainer container = entry.getValue();
 543             if (parent == key) {
 544                 iterator.remove();
 545                 container.clearCache();
 546             }
 547         }
 548 
 549         final List<String> stylesheets = parent.getStylesheets();
 550         if (stylesheets != null && !stylesheets.isEmpty()) {
 551             for (String fname : stylesheets) {
 552                 stylesheetRemoved(parent, fname);
 553             }
 554         }
 555         // Do not iterate over children since this method will be called on each from Parent#scenesChanged
 556     }
 557 
 558     /**
 559      * called from Parent's stylesheets property's onChanged method
 560      */
 561     public void stylesheetsChanged(Parent parent, Change<String> c) {
 562         c.reset();
 563         while(c.next()) {
 564             if (c.wasRemoved()) {
 565                 for (String fname : c.getRemoved()) {
 566                     stylesheetRemoved(parent, fname);
 567 
 568                     StylesheetContainer stylesheetContainer = stylesheetContainerMap.get(fname);
 569                     if (stylesheetContainer != null) {
 570                         stylesheetContainer.invalidateChecksum();
 571                     }
 572                 }
 573             }
 574         }
 575     }
 576 
 577     private void stylesheetRemoved(Parent parent, String fname) {
 578 
 579         StylesheetContainer stylesheetContainer = stylesheetContainerMap.get(fname);
 580 
 581         if (stylesheetContainer == null) return;
 582 
 583         stylesheetContainer.parentUsers.remove(parent);
 584 
 585         if (stylesheetContainer.parentUsers.list.isEmpty()) {
 586             removeStylesheetContainer(stylesheetContainer);
 587         }
 588     }
 589 
 590     /**
 591      * called from Window when the scene is closed.
 592      */
 593     public void forget(final SubScene subScene) {
 594 
 595         if (subScene == null) return;
 596         final Parent subSceneRoot = subScene.getRoot();
 597 
 598         if (subSceneRoot == null) return;
 599         forget(subSceneRoot);
 600 
 601         //
 602         // if this scene has user-agent stylesheets, clean up the userAgentStylesheetContainers list
 603         //
 604         String sceneUserAgentStylesheet = null;
 605         if ((subScene.getUserAgentStylesheet() != null) &&
 606                 (!(sceneUserAgentStylesheet = subScene.getUserAgentStylesheet().trim()).isEmpty())) {
 607 
 608             for(int n=0,nMax=userAgentStylesheetContainers.size(); n<nMax; n++) {
 609                 StylesheetContainer container = userAgentStylesheetContainers.get(n);
 610                 if (sceneUserAgentStylesheet.equals(container.fname)) {
 611                     container.parentUsers.remove(subScene.getRoot());
 612                     if (container.parentUsers.list.size() == 0) {
 613                         userAgentStylesheetContainers.remove(n);
 614                     }
 615                 }
 616             }
 617         }
 618 
 619         //
 620         // remove any parents belonging to this SubScene from the stylesheetContainerMap
 621         //
 622         Set<Entry<String,StylesheetContainer>> stylesheetContainers = stylesheetContainerMap.entrySet();
 623         Iterator<Entry<String,StylesheetContainer>> iter = stylesheetContainers.iterator();
 624 
 625         while(iter.hasNext()) {
 626 
 627             Entry<String,StylesheetContainer> entry = iter.next();
 628             StylesheetContainer container = entry.getValue();
 629 
 630             Iterator<Reference<Parent>> parentIter = container.parentUsers.list.iterator();
 631             while (parentIter.hasNext()) {
 632 
 633                 final Reference<Parent> ref = parentIter.next();
 634                 final Parent _parent = ref.get();
 635 
 636                 if (_parent != null) {
 637                     // if this stylesheet refererent is a child of this subscene, nuke it.
 638                     Parent p = _parent;
 639                     while (p != null) {
 640                         if (subSceneRoot == p.getParent()) {
 641                             ref.clear();
 642                             parentIter.remove();
 643                             forget(_parent); // _parent, not p!
 644                             break;
 645                         }
 646                         p = p.getParent();
 647                     }
 648                 }
 649             }
 650 
 651             // forget(_parent) will remove the container if the parentUser's list is empty
 652             // if (container.parentUsers.list.isEmpty()) {
 653             //    iter.remove();
 654             // }
 655         }
 656 
 657     }
 658 
 659     private void removeStylesheetContainer(StylesheetContainer stylesheetContainer) {
 660 
 661         if (stylesheetContainer == null) return;
 662 
 663         final String fname = stylesheetContainer.fname;
 664 
 665         stylesheetContainerMap.remove(fname);
 666 
 667         if (stylesheetContainer.selectorPartitioning != null) {
 668             stylesheetContainer.selectorPartitioning.reset();
 669         }
 670 
 671         // if container has no references, then remove it
 672         for(Entry<Parent,CacheContainer> entry : cacheContainerMap.entrySet()) {
 673 
 674             CacheContainer container = entry.getValue();
 675             List<List<String>> entriesToRemove = new ArrayList<>();
 676 
 677             for (Entry<List<String>, Map<Key,Cache>> cacheMapEntry : container.cacheMap.entrySet()) {
 678                 List<String> cacheMapKey = cacheMapEntry.getKey();
 679                 if (cacheMapKey != null ? cacheMapKey.contains(fname) : fname == null) {
 680                     entriesToRemove.add(cacheMapKey);
 681                 }
 682             }
 683 
 684             if (!entriesToRemove.isEmpty()) {
 685                 for (List<String> cacheMapKey : entriesToRemove) {
 686                     Map<Key,Cache> cacheEntry = container.cacheMap.remove(cacheMapKey);
 687                     if (cacheEntry != null) {
 688                         cacheEntry.clear();
 689                     }
 690                 }
 691 
 692                 if (container.cacheMap.isEmpty()) {
 693                     // TODO:
 694                     System.out.println("container.cacheMap.isEmpty");
 695                 }
 696             }
 697         }
 698 
 699         // clean up image cache by removing images from the cache that
 700         // might have come from this stylesheet
 701         cleanUpImageCache(fname);
 702 
 703         final List<Reference<Parent>> parentList = stylesheetContainer.parentUsers.list;
 704 
 705         for (int n=parentList.size()-1; 0<=n; --n) {
 706 
 707             final Reference<Parent> ref = parentList.remove(n);
 708             final Parent parent = ref.get();
 709             ref.clear();
 710             if (parent == null || parent.getScene() == null) {
 711                 continue;
 712             }
 713 
 714             //
 715             // tell parent it needs to reapply css
 716             // No harm is done if parent is in a scene that has had
 717             // impl_reapplyCSS called on the root.
 718             //
 719             parent.impl_reapplyCSS();
 720         }
 721 
 722     }
 723 
 724     ////////////////////////////////////////////////////////////////////////////
 725     //
 726     // Image caching
 727     //
 728     ////////////////////////////////////////////////////////////////////////////
 729 
 730     Map<String,Image> imageCache = new HashMap<String,Image>();
 731 
 732     public Image getCachedImage(String url) {
 733 
 734         Image image = null;
 735         if (imageCache.containsKey(url)) {
 736 
 737             image = imageCache.get(url);
 738 
 739         } else {
 740 
 741             try {
 742 
 743                 image = new Image(url);
 744 
 745                 // RT-31865
 746                 if (image.isError()) {
 747 
 748                     final PlatformLogger logger = getLogger();
 749                     if (logger != null && logger.isLoggable(Level.WARNING)) {
 750                         logger.warning("Error loading image: " + url);
 751                     }
 752 
 753                     image = null;
 754                 }
 755 
 756                 imageCache.put(url, image);
 757 
 758             } catch (IllegalArgumentException iae) {
 759                 // url was empty!
 760                 final PlatformLogger logger = getLogger();
 761                 if (logger != null && logger.isLoggable(Level.WARNING)) {
 762                     logger.warning(iae.getLocalizedMessage());
 763                 }
 764 
 765             } catch (NullPointerException npe) {
 766                 // url was null!
 767                 final PlatformLogger logger = getLogger();
 768                 if (logger != null && logger.isLoggable(Level.WARNING)) {
 769                     logger.warning(npe.getLocalizedMessage());
 770                 }
 771             }
 772         }
 773 
 774         return image;
 775     }
 776 
 777     private void cleanUpImageCache(String fname) {
 778 
 779         if (fname == null && imageCache.isEmpty()) return;
 780         if (fname.trim().isEmpty()) return;
 781 
 782         int len = fname.lastIndexOf('/');
 783         final String path = (len > 0) ? fname.substring(0,len) : fname;
 784         final int plen = path.length();
 785 
 786         final String[] entriesToRemove = new String[imageCache.size()];
 787         int count = 0;
 788 
 789         final Set<Entry<String, Image>> entrySet = imageCache.entrySet();
 790         for(Entry<String, Image> entry : entrySet) {
 791 
 792             final String key = entry.getKey();
 793             len = key.lastIndexOf('/');
 794             final String kpath = (len > 0) ? key.substring(0, len) : key;
 795             final int klen = kpath.length();
 796 
 797             // if the longer path begins with the shorter path,
 798             // then assume the image came from this path.
 799             boolean match = (klen > plen) ? kpath.startsWith(path) : path.startsWith(kpath);
 800             if (match) entriesToRemove[count++] = key;
 801         }
 802 
 803         for (int n=0; n<count; n++) {
 804            Image img = imageCache.remove(entriesToRemove[n]);
 805        }
 806     }
 807 
 808     ////////////////////////////////////////////////////////////////////////////
 809     //
 810     // Stylesheet loading
 811     //
 812     ////////////////////////////////////////////////////////////////////////////
 813 
 814     private static URL getURL(final String str) {
 815 
 816         // Note: this code is duplicated, more or less, in URLConverter
 817 
 818         if (str == null || str.trim().isEmpty()) return null;
 819 
 820         try {
 821 
 822             URI uri =  new URI(str.trim());
 823 
 824             // if url doesn't have a scheme
 825             if (uri.isAbsolute() == false) {
 826 
 827                 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
 828                 final String path = uri.getPath();
 829 
 830                 URL resource = null;
 831 
 832                 if (path.startsWith("/")) {
 833                     resource = contextClassLoader.getResource(path.substring(1));
 834                 } else {
 835                     resource = contextClassLoader.getResource(path);
 836                 }
 837 
 838                 return resource;
 839             }
 840 
 841             // else, url does have a scheme
 842             return uri.toURL();
 843 
 844         } catch (MalformedURLException malf) {
 845             // Do not log exception here - caller will handle null return.
 846             // For example, we might be looking for a .bss that doesn't exist
 847             return null;
 848         } catch (URISyntaxException urise) {
 849             return null;
 850         }
 851     }
 852 
 853     // Calculate checksum for stylesheet file. Return byte[0] if checksum could not be calculated.
 854     static byte[] calculateCheckSum(String fname) {
 855 
 856         if (fname == null || fname.isEmpty()) return new byte[0];
 857 
 858         try {
 859             URL url = getURL(fname);
 860 
 861             // We only care about stylesheets from file: URLs.
 862             if (url != null && "file".equals(url.getProtocol())) {
 863 
 864                 try (InputStream stream = url.openStream()) {
 865 
 866                     // not looking for security, just a checksum. MD5 should be faster than SHA
 867                     final DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("MD5"));
 868                     while (dis.read() != -1) { /* empty loop body is intentional */ }
 869                     return dis.getMessageDigest().digest();
 870                 }
 871 
 872             }
 873 
 874         } catch (IllegalArgumentException | NoSuchAlgorithmException | IOException | SecurityException e) {
 875             // IOException also covers MalformedURLException
 876             // SecurityException means some untrusted applet
 877 
 878             // Fall through...
 879         }
 880         return new byte[0];
 881     }
 882 
 883     private static Stylesheet loadStylesheet(final String fname) {
 884         try {
 885             return loadStylesheetUnPrivileged(fname);
 886         } catch (java.security.AccessControlException ace) {
 887             if (getLogger().isLoggable(Level.INFO)) {
 888                 getLogger().info("Could not load the stylesheet, trying with FilePermissions : " + fname);
 889             }
 890 
 891             /*
 892             ** we got an access control exception, so
 893             ** we could be running from an applet/jnlp/or with a security manager.
 894             ** we'll allow the app to read a css file from our runtime jar,
 895             ** and give it one more chance.
 896             */
 897 
 898             /*
 899             ** check that there are enough chars after the !/ to have a valid .css or .bss file name
 900             */
 901             if ((fname.length() < 7) && (fname.indexOf("!/") < fname.length()-7)) {
 902                 return null;
 903             }
 904 
 905             /*
 906             **
 907             ** first check that it's actually looking for the same runtime jar
 908             ** that we're running from, and not some other file.
 909             */
 910             try {
 911                 URI requestedFileUrI = new URI(fname);
 912 
 913                 /*
 914                 ** is the requested file in a jar
 915                 */
 916                 if ("jar".equals(requestedFileUrI.getScheme())) {
 917                     /*
 918                     ** let's check that the css file is being requested from our
 919                     ** runtime jar
 920                     */
 921                     URI styleManagerJarURI = AccessController.doPrivileged((PrivilegedExceptionAction<URI>) () -> StyleManager.class.getProtectionDomain().getCodeSource().getLocation().toURI());
 922 
 923                     final String styleManagerJarPath = styleManagerJarURI.getSchemeSpecificPart();
 924                     String requestedFilePath = requestedFileUrI.getSchemeSpecificPart();
 925                     String requestedFileJarPart = requestedFilePath.substring(requestedFilePath.indexOf('/'), requestedFilePath.indexOf("!/"));
 926                     /*
 927                     ** it's the correct jar, check it's a file access
 928                     ** strip off the leading jar
 929                     */
 930                     if (styleManagerJarPath.equals(requestedFileJarPart)) {
 931                         /*
 932                         ** strip off the leading "jar",
 933                         ** the css file name is past the last '!'
 934                         */
 935                         String requestedFileJarPathNoLeadingSlash = fname.substring(fname.indexOf("!/")+2);
 936                         /*
 937                         ** check that it's looking for a css file in the runtime jar
 938                         */
 939                         if (fname.endsWith(".css") || fname.endsWith(".bss")) {
 940                             /*
 941                             ** set up a read permission for the jar
 942                             */
 943                             FilePermission perm = new FilePermission(styleManagerJarPath, "read");
 944 
 945                             PermissionCollection perms = perm.newPermissionCollection();
 946                             perms.add(perm);
 947                             AccessControlContext permsAcc = new AccessControlContext(
 948                                 new ProtectionDomain[] {
 949                                     new ProtectionDomain(null, perms)
 950                                 });
 951                             /*
 952                             ** check that the jar file exists, and that we're allowed to
 953                             ** read it.
 954                             */
 955                             JarFile jar = null;
 956                             try {
 957                                 jar = AccessController.doPrivileged((PrivilegedExceptionAction<JarFile>) () -> new JarFile(styleManagerJarPath), permsAcc);
 958                             } catch (PrivilegedActionException pae) {
 959                                 /*
 960                                 ** we got either a FileNotFoundException or an IOException
 961                                 ** in the privileged read. Return the same error as we
 962                                 ** would have returned if the css file hadn't of existed.
 963                                 */
 964                                 return null;
 965                             }
 966                             if (jar != null) {
 967                                 /*
 968                                 ** check that the file is in the jar
 969                                 */
 970                                 JarEntry entry = jar.getJarEntry(requestedFileJarPathNoLeadingSlash);
 971                                 if (entry != null) {
 972                                     /*
 973                                     ** allow read access to the jar
 974                                     */
 975                                     return AccessController.doPrivileged(
 976                                             (PrivilegedAction<Stylesheet>) () -> loadStylesheetUnPrivileged(fname), permsAcc);
 977                                 }
 978                             }
 979                         }
 980                     }
 981                 }
 982                 /*
 983                 ** no matter what happen, we return the same error that would
 984                 ** be returned if the css file hadn't of existed.
 985                 ** That way there in no information leaked.
 986                 */
 987                 return null;
 988             }
 989             /*
 990             ** no matter what happen, we return the same error that would
 991             ** be returned if the css file hadn't of existed.
 992             ** That way there in no information leaked.
 993             */
 994             catch (java.net.URISyntaxException e) {
 995                 return null;
 996             }
 997             catch (java.security.PrivilegedActionException e) {
 998                 return null;
 999             }
1000        }
1001     }
1002 
1003 
1004     private static Stylesheet loadStylesheetUnPrivileged(final String fname) {
1005 
1006         Boolean parse = AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> {
1007 
1008             final String bss = System.getProperty("binary.css");
1009             // binary.css is true by default.
1010             // parse only if the file is not a .bss
1011             // and binary.css is set to false
1012             return (!fname.endsWith(".bss") && bss != null) ?
1013                 !Boolean.valueOf(bss) : Boolean.FALSE;
1014         });
1015 
1016         try {
1017             final String ext = (parse) ? (".css") : (".bss");
1018             java.net.URL url = null;
1019             Stylesheet stylesheet = null;
1020             // check if url has extension, if not then just url as is and always parse as css text
1021             if (!(fname.endsWith(".css") || fname.endsWith(".bss"))) {
1022                 url = getURL(fname);
1023                 parse = true;
1024             } else {
1025                 final String name = fname.substring(0, fname.length() - 4);
1026 
1027                 url = getURL(name+ext);
1028                 if (url == null && (parse = !parse)) {
1029                     // If we failed to get the URL for the .bss file,
1030                     // fall back to the .css file.
1031                     // Note that 'parse' is toggled in the test.
1032                     url = getURL(name+".css");
1033                 }
1034 
1035                 if ((url != null) && !parse) {
1036 
1037                     try {
1038                         // RT-36332: if loadBinary throws an IOException, make sure to try .css
1039                         stylesheet = Stylesheet.loadBinary(url);
1040                     } catch (IOException ioe) {
1041                         stylesheet = null;
1042                     }
1043 
1044                     if (stylesheet == null && (parse = !parse)) {
1045                         // If we failed to load the .bss file,
1046                         // fall back to the .css file.
1047                         // Note that 'parse' is toggled in the test.
1048                         url = getURL(fname);
1049                     }
1050                 }
1051             }
1052 
1053             // either we failed to load the .bss file, or parse
1054             // was set to true.
1055             if ((url != null) && parse) {
1056                 stylesheet = CSSParser.getInstance().parse(url);
1057             }
1058 
1059             if (stylesheet == null) {
1060                 if (errors != null) {
1061                     CssError error =
1062                         new CssError(
1063                             "Resource \""+fname+"\" not found."
1064                         );
1065                     errors.add(error);
1066                 }
1067                 if (getLogger().isLoggable(Level.WARNING)) {
1068                     getLogger().warning(
1069                         String.format("Resource \"%s\" not found.", fname)
1070                     );
1071                 }
1072             }
1073 
1074             // load any fonts from @font-face
1075             if (stylesheet != null) {
1076                 faceLoop: for(FontFace fontFace: stylesheet.getFontFaces()) {
1077                     for(FontFace.FontFaceSrc src: fontFace.getSources()) {
1078                         if (src.getType() == FontFace.FontFaceSrcType.URL) {
1079                             Font loadedFont = Font.loadFont(src.getSrc(),10);
1080                             if (loadedFont == null) {
1081                                 getLogger().info("Could not load @font-face font [" + src.getSrc() + "]");
1082                             }
1083                             continue faceLoop;
1084                         }
1085                     }
1086                 }
1087             }
1088 
1089             return stylesheet;
1090 
1091         } catch (FileNotFoundException fnfe) {
1092             if (errors != null) {
1093                 CssError error =
1094                     new CssError(
1095                         "Stylesheet \""+fname+"\" not found."
1096                     );
1097                 errors.add(error);
1098             }
1099             if (getLogger().isLoggable(Level.INFO)) {
1100                 getLogger().info("Could not find stylesheet: " + fname);//, fnfe);
1101             }
1102         } catch (IOException ioe) {
1103                 if (errors != null) {
1104                     CssError error =
1105                         new CssError(
1106                             "Could not load stylesheet: " + fname
1107                         );
1108                     errors.add(error);
1109                 }
1110             if (getLogger().isLoggable(Level.INFO)) {
1111                 getLogger().info("Could not load stylesheet: " + fname);//, ioe);
1112             }
1113         }
1114         return null;
1115     }
1116 
1117     ////////////////////////////////////////////////////////////////////////////
1118     //
1119     // User Agent stylesheet handling
1120     //
1121     ////////////////////////////////////////////////////////////////////////////
1122 
1123     /**
1124      * Add a user agent stylesheet, possibly overriding styles in the default
1125      * user agent stylesheet.
1126      *
1127      * @param fname The file URL, either relative or absolute, as a String.
1128      */
1129     public void addUserAgentStylesheet(String fname) {
1130         addUserAgentStylesheet(null, fname);
1131     }
1132 
1133     /**
1134      * Add a user agent stylesheet, possibly overriding styles in the default
1135      * user agent stylesheet.
1136      * @param scene Only used in CssError for tracking back to the scene that loaded the stylesheet
1137      * @param url  The file URL, either relative or absolute, as a String.
1138      */
1139     // For RT-20643
1140     public void addUserAgentStylesheet(Scene scene, String url) {
1141 
1142         if (url == null ) {
1143             throw new IllegalArgumentException("null arg url");
1144         }
1145 
1146         final String fname = url.trim();
1147         if (fname.isEmpty()) {
1148             return;
1149         }
1150 
1151         // if we already have this stylesheet, bail
1152         for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
1153             StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
1154             if (fname.equals(container.fname)) {
1155                 return;
1156             }
1157         }
1158 
1159         // RT-20643
1160         CssError.setCurrentScene(scene);
1161 
1162         final Stylesheet ua_stylesheet = loadStylesheet(fname);
1163         platformUserAgentStylesheetContainers.add(new StylesheetContainer(fname, ua_stylesheet));
1164 
1165         if (ua_stylesheet != null) {
1166             ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
1167         }
1168         userAgentStylesheetsChanged();
1169 
1170         // RT-20643
1171         CssError.setCurrentScene(null);
1172 
1173     }
1174 
1175     /**
1176      * Add a user agent stylesheet, possibly overriding styles in the default
1177      * user agent stylesheet.
1178      * @param scene Only used in CssError for tracking back to the scene that loaded the stylesheet
1179      * @param ua_stylesheet  The stylesheet to add as a user-agent stylesheet
1180      */
1181     public void addUserAgentStylesheet(Scene scene, Stylesheet ua_stylesheet) {
1182 
1183         if (ua_stylesheet == null ) {
1184             throw new IllegalArgumentException("null arg ua_stylesheet");
1185         }
1186 
1187         // null url is ok, just means that it is a stylesheet not loaded from a file
1188         String url = ua_stylesheet.getUrl();
1189         final String fname = url != null ? url.trim() : "";
1190 
1191         // if we already have this stylesheet, bail
1192         for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
1193             StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
1194             if (fname.equals(container.fname)) {
1195                 return;
1196             }
1197         }
1198 
1199         // RT-20643
1200         CssError.setCurrentScene(scene);
1201 
1202         platformUserAgentStylesheetContainers.add(new StylesheetContainer(fname, ua_stylesheet));
1203 
1204         if (ua_stylesheet != null) {
1205             ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
1206         }
1207         userAgentStylesheetsChanged();
1208 
1209         // RT-20643
1210         CssError.setCurrentScene(null);
1211 
1212     }
1213 
1214     /**
1215      * Set the default user agent stylesheet.
1216      *
1217      * @param fname The file URL, either relative or absolute, as a String.
1218      */
1219     public void setDefaultUserAgentStylesheet(String fname) {
1220         setDefaultUserAgentStylesheet(null, fname);
1221     }
1222 
1223     /**
1224      * Set the default user agent stylesheet
1225      * @param scene Only used in CssError for tracking back to the scene that loaded the stylesheet
1226      * @param url  The file URL, either relative or absolute, as a String.
1227      */
1228     // For RT-20643
1229     public void setDefaultUserAgentStylesheet(Scene scene, String url) {
1230 
1231         final String fname = (url != null) ? url.trim() : null;
1232         if (fname == null || fname.isEmpty()) {
1233             throw new IllegalArgumentException("null arg url");
1234         }
1235 
1236         // if we already have this stylesheet, make sure it is the first element
1237         for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
1238             StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
1239             if (fname.equals(container.fname)) {
1240                 if (n > 0) {
1241                     platformUserAgentStylesheetContainers.remove(n);
1242                     if (hasDefaultUserAgentStylesheet) {
1243                         platformUserAgentStylesheetContainers.set(0, container);
1244                     } else {
1245                         platformUserAgentStylesheetContainers.add(0, container);
1246                     }
1247                 }
1248                 return;
1249             }
1250         }
1251 
1252         // RT-20643
1253         CssError.setCurrentScene(scene);
1254 
1255         final Stylesheet ua_stylesheet = loadStylesheet(fname);
1256         final StylesheetContainer sc = new StylesheetContainer(fname, ua_stylesheet);
1257         if (platformUserAgentStylesheetContainers.size() == 0) {
1258             platformUserAgentStylesheetContainers.add(sc);
1259         } else if (hasDefaultUserAgentStylesheet) {
1260             platformUserAgentStylesheetContainers.set(0,sc);
1261         } else {
1262             platformUserAgentStylesheetContainers.add(0,sc);
1263         }
1264         hasDefaultUserAgentStylesheet = true;
1265 
1266         if (ua_stylesheet != null) {
1267             ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
1268         }
1269         userAgentStylesheetsChanged();
1270 
1271         // RT-20643
1272         CssError.setCurrentScene(null);
1273 
1274     }
1275 
1276     /**
1277      * Set the user agent stylesheet. This is the base default stylesheet for
1278      * the platform
1279      */
1280     public void setDefaultUserAgentStylesheet(Stylesheet ua_stylesheet) {
1281 
1282         if (ua_stylesheet == null ) {
1283             throw new IllegalArgumentException("null arg ua_stylesheet");
1284         }
1285 
1286         // null url is ok, just means that it is a stylesheet not loaded from a file
1287         String url = ua_stylesheet.getUrl();
1288         final String fname = url != null ? url.trim() : "";
1289 
1290         // if we already have this stylesheet, make sure it is the first element
1291         for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
1292             StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
1293             if (fname.equals(container.fname)) {
1294                 if (n > 0) {
1295                     platformUserAgentStylesheetContainers.remove(n);
1296                     if (hasDefaultUserAgentStylesheet) {
1297                         platformUserAgentStylesheetContainers.set(0, container);
1298                     } else {
1299                         platformUserAgentStylesheetContainers.add(0, container);
1300                     }
1301                 }
1302                 return;
1303             }
1304         }
1305 
1306         StylesheetContainer sc = new StylesheetContainer(fname, ua_stylesheet);
1307         if (platformUserAgentStylesheetContainers.size() == 0) {
1308             platformUserAgentStylesheetContainers.add(sc);
1309         } else if (hasDefaultUserAgentStylesheet) {
1310             platformUserAgentStylesheetContainers.set(0,sc);
1311         } else {
1312             platformUserAgentStylesheetContainers.add(0,sc);
1313         }
1314         hasDefaultUserAgentStylesheet = true;
1315 
1316         ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
1317         userAgentStylesheetsChanged();
1318 
1319         // RT-20643
1320         CssError.setCurrentScene(null);
1321     }
1322 
1323     /*
1324      * If the userAgentStylesheets change, then all scenes are updated.
1325      */
1326     private void userAgentStylesheetsChanged() {
1327 
1328         for (CacheContainer container : cacheContainerMap.values()) {
1329             container.clearCache();
1330         }
1331 
1332         StyleConverterImpl.clearCache();
1333 
1334         for (Parent root : cacheContainerMap.keySet()) {
1335             if (root == null) {
1336                 continue;
1337             }
1338             root.impl_reapplyCSS();
1339         }
1340 
1341     }
1342 
1343     private List<StylesheetContainer> processStylesheets(List<String> stylesheets, Parent parent) {
1344 
1345         final List<StylesheetContainer> list = new ArrayList<StylesheetContainer>();
1346         for (int n = 0, nMax = stylesheets.size(); n < nMax; n++) {
1347             final String fname = stylesheets.get(n);
1348 
1349             StylesheetContainer container = null;
1350             if (stylesheetContainerMap.containsKey(fname)) {
1351                 container = stylesheetContainerMap.get(fname);
1352 
1353                 if (!list.contains(container)) {
1354                     // minor optimization: if existing checksum in byte[0], then don't bother recalculating
1355                     if (container.checksumInvalid) {
1356                         final byte[] checksum = calculateCheckSum(fname);
1357                         if (!Arrays.equals(checksum, container.checksum)) {
1358                             removeStylesheetContainer(container);
1359 
1360                             // Stylesheet did change. Re-load the stylesheet and update the container map.
1361                             Stylesheet stylesheet = loadStylesheet(fname);
1362                             container = new StylesheetContainer(fname, stylesheet, checksum);
1363                             stylesheetContainerMap.put(fname, container);
1364                         } else {
1365                             container.checksumInvalid = false;
1366                         }
1367                     }
1368                     list.add(container);
1369                 }
1370 
1371                 // RT-22565: remember that this parent or scene uses this stylesheet.
1372                 // Later, if the cache is cleared, the parent or scene is told to
1373                 // reapply css.
1374                 container.parentUsers.add(parent);
1375 
1376             } else {
1377                 final Stylesheet stylesheet = loadStylesheet(fname);
1378                 // stylesheet may be null which would mean that some IOException
1379                 // was thrown while trying to load it. Add it to the
1380                 // stylesheetContainerMap anyway as this will prevent further
1381                 // attempts to parse the file
1382                 container = new StylesheetContainer(fname, stylesheet);
1383                 // RT-22565: remember that this parent or scene uses this stylesheet.
1384                 // Later, if the cache is cleared, the parent or scene is told to
1385                 // reapply css.
1386                 container.parentUsers.add(parent);
1387                 stylesheetContainerMap.put(fname, container);
1388 
1389                 list.add(container);
1390             }
1391         }
1392         return list;
1393     }
1394 
1395     //
1396     // recurse so that stylesheets of Parents closest to the root are
1397     // added to the list first. The ensures that declarations for
1398     // stylesheets further down the tree (closer to the leaf) have
1399     // a higher ordinal in the cascade.
1400     //
1401     private List<StylesheetContainer> gatherParentStylesheets(final Parent parent) {
1402 
1403         if (parent == null) {
1404             return Collections.<StylesheetContainer>emptyList();
1405         }
1406 
1407         final List<String> parentStylesheets = parent.impl_getAllParentStylesheets();
1408 
1409         if (parentStylesheets == null || parentStylesheets.isEmpty()) {
1410             return Collections.<StylesheetContainer>emptyList();
1411         }
1412 
1413         // RT-20643
1414         CssError.setCurrentScene(parent.getScene());
1415 
1416         final List<StylesheetContainer> list = processStylesheets(parentStylesheets, parent);
1417 
1418         // RT-20643
1419         CssError.setCurrentScene(null);
1420 
1421         return list;
1422     }
1423 
1424     //
1425     //
1426     //
1427     private List<StylesheetContainer> gatherSceneStylesheets(final Scene scene) {
1428 
1429         if (scene == null) {
1430             return Collections.<StylesheetContainer>emptyList();
1431         }
1432 
1433         final List<String> sceneStylesheets = scene.getStylesheets();
1434 
1435         if (sceneStylesheets == null || sceneStylesheets.isEmpty()) {
1436             return Collections.<StylesheetContainer>emptyList();
1437         }
1438 
1439         // RT-20643
1440         CssError.setCurrentScene(scene);
1441 
1442         final List<StylesheetContainer> list = processStylesheets(sceneStylesheets, scene.getRoot());
1443 
1444         // RT-20643
1445         CssError.setCurrentScene(null);
1446 
1447         return list;
1448     }
1449 
1450     // return true if this node or any of its parents has an inline style.
1451     private static Node nodeWithInlineStyles(Node node) {
1452 
1453         Node parent = node;
1454 
1455         while (parent != null) {
1456 
1457             final String inlineStyle = parent.getStyle();
1458             if (inlineStyle != null && inlineStyle.isEmpty() == false) {
1459                 return parent;
1460             }
1461             parent = parent.getParent();
1462 
1463         }
1464 
1465         return null;
1466     }
1467 
1468     // reuse key to avoid creation of numerous small objects
1469     private Key key = null;
1470 
1471     /**
1472      * Finds matching styles for this Node.
1473      */
1474     public StyleMap findMatchingStyles(Node node, SubScene subScene, Set<PseudoClass>[] triggerStates) {
1475 
1476         final Scene scene = node.getScene();
1477         if (scene == null) {
1478             return StyleMap.EMPTY_MAP;
1479         }
1480 
1481         CacheContainer cacheContainer = getCacheContainer(node, subScene);
1482         if (cacheContainer == null) {
1483             assert false : node.toString();
1484             return StyleMap.EMPTY_MAP;
1485         }
1486 
1487         final Parent parent =
1488             (node instanceof Parent)
1489                 ? (Parent) node : node.getParent();
1490 
1491         final List<StylesheetContainer> parentStylesheets =
1492                     gatherParentStylesheets(parent);
1493 
1494         final boolean hasParentStylesheets = parentStylesheets.isEmpty() == false;
1495 
1496         final List<StylesheetContainer> sceneStylesheets = gatherSceneStylesheets(scene);
1497 
1498         final boolean hasSceneStylesheets = sceneStylesheets.isEmpty() == false;
1499 
1500         final String inlineStyle = node.getStyle();
1501         final boolean hasInlineStyles = inlineStyle != null && inlineStyle.trim().isEmpty() == false;
1502 
1503         final String sceneUserAgentStylesheet = scene.getUserAgentStylesheet();
1504         final boolean hasSceneUserAgentStylesheet =
1505                 sceneUserAgentStylesheet != null && sceneUserAgentStylesheet.trim().isEmpty() == false;
1506 
1507         final String subSceneUserAgentStylesheet =
1508                 (subScene != null) ? subScene.getUserAgentStylesheet() : null;
1509         final boolean hasSubSceneUserAgentStylesheet =
1510                 subSceneUserAgentStylesheet != null && subSceneUserAgentStylesheet.trim().isEmpty() == false;
1511 
1512         //
1513         // Are there any stylesheets at all?
1514         // If not, then there is nothing to match and the
1515         // resulting StyleMap is going to end up empty
1516         //
1517         if (hasInlineStyles == false
1518                 && hasParentStylesheets == false
1519                 && hasSceneStylesheets == false
1520                 && hasSceneUserAgentStylesheet == false
1521                 && hasSubSceneUserAgentStylesheet == false
1522                 && platformUserAgentStylesheetContainers.isEmpty()) {
1523             return StyleMap.EMPTY_MAP;
1524         }
1525 
1526         final String cname = node.getTypeSelector();
1527         final String id = node.getId();
1528         final List<String> styleClasses = node.getStyleClass();
1529 
1530         if (key == null) {
1531             key = new Key();
1532         }
1533 
1534         key.className = cname;
1535         key.id = id;
1536         for(int n=0, nMax=styleClasses.size(); n<nMax; n++) {
1537 
1538             final String styleClass = styleClasses.get(n);
1539             if (styleClass == null || styleClass.isEmpty()) continue;
1540 
1541             key.styleClasses.add(StyleClassSet.getStyleClass(styleClass));
1542         }
1543 
1544         Map<Key, Cache> cacheMap = cacheContainer.getCacheMap(parentStylesheets);
1545         Cache cache = cacheMap.get(key);
1546 
1547         if (cache != null) {
1548             // key will be reused, so clear the styleClasses for next use
1549             key.styleClasses.clear();
1550 
1551         } else {
1552 
1553             // If the cache is null, then we need to create a new Cache and
1554             // add it to the cache map
1555 
1556             // Construct the list of Selectors that could possibly apply
1557             final List<Selector> selectorData = new ArrayList<>();
1558 
1559             // User agent stylesheets have lowest precedence and go first
1560             if (hasSubSceneUserAgentStylesheet || hasSceneUserAgentStylesheet) {
1561 
1562                 // if has both, use SubScene
1563                 final String uaFileName = hasSubSceneUserAgentStylesheet ?
1564                         subScene.getUserAgentStylesheet().trim() :
1565                         scene.getUserAgentStylesheet().trim();
1566 
1567 
1568                 StylesheetContainer container = null;
1569                 for (int n=0, nMax=userAgentStylesheetContainers.size(); n<nMax; n++) {
1570                     container = userAgentStylesheetContainers.get(n);
1571                     if (uaFileName.equals(container.fname)) {
1572                         break;
1573                     }
1574                     container = null;
1575                 }
1576 
1577                 if (container == null) {
1578                     Stylesheet stylesheet = loadStylesheet(uaFileName);
1579                     if (stylesheet != null) {
1580                         stylesheet.setOrigin(StyleOrigin.USER_AGENT);
1581                     }
1582                     container = new StylesheetContainer(uaFileName, stylesheet);
1583                     userAgentStylesheetContainers.add(container);
1584                 }
1585 
1586                 if (container.selectorPartitioning != null) {
1587 
1588                     final Parent root = hasSubSceneUserAgentStylesheet ? subScene.getRoot() : scene.getRoot();
1589                     container.parentUsers.add(root);
1590 
1591                     final List<Selector> matchingRules =
1592                             container.selectorPartitioning.match(id, cname, key.styleClasses);
1593                     selectorData.addAll(matchingRules);
1594                 }
1595 
1596             } else if (platformUserAgentStylesheetContainers.isEmpty() == false) {
1597                 for(int n=0, nMax= platformUserAgentStylesheetContainers.size(); n<nMax; n++) {
1598                     final StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
1599                     if (container != null && container.selectorPartitioning != null) {
1600                         final List<Selector> matchingRules =
1601                                 container.selectorPartitioning.match(id, cname, key.styleClasses);
1602                         selectorData.addAll(matchingRules);
1603                     }
1604                 }
1605             }
1606 
1607             // Scene stylesheets come next since declarations from
1608             // parent stylesheets should take precedence.
1609             if (sceneStylesheets.isEmpty() == false) {
1610                 for(int n=0, nMax=sceneStylesheets.size(); n<nMax; n++) {
1611                     final StylesheetContainer container = sceneStylesheets.get(n);
1612                     if (container != null && container.selectorPartitioning != null) {
1613                         final List<Selector> matchingRules =
1614                                 container.selectorPartitioning.match(id, cname, key.styleClasses);
1615                         selectorData.addAll(matchingRules);
1616                     }
1617                 }
1618             }
1619 
1620             // lastly, parent stylesheets
1621             if (hasParentStylesheets) {
1622                 final int nMax = parentStylesheets == null ? 0 : parentStylesheets.size();
1623                 for(int n=0; n<nMax; n++) {
1624                     final StylesheetContainer container = parentStylesheets.get(n);
1625                     if (container.selectorPartitioning != null) {
1626                         final List<Selector> matchingRules =
1627                                 container.selectorPartitioning.match(id, cname, key.styleClasses);
1628                         selectorData.addAll(matchingRules);
1629                     }
1630                 }
1631             }
1632 
1633             // create a new Cache from these selectors.
1634             cache = new Cache(selectorData);
1635             cacheMap.put(key, cache);
1636 
1637             // cause a new Key to be created the next time this method is called
1638             key = null;
1639         }
1640 
1641         //
1642         // Create a style helper for this node from the styles that match.
1643         //
1644         StyleMap smap = cache.getStyleMap(cacheContainer, node, triggerStates, hasInlineStyles);
1645 
1646         return smap;
1647     }
1648 
1649     ////////////////////////////////////////////////////////////////////////////
1650     //
1651     // CssError reporting
1652     //
1653     ////////////////////////////////////////////////////////////////////////////
1654 
1655     private static ObservableList<CssError> errors = null;
1656     /**
1657      * Errors that may have occurred during css processing.
1658      * This list is null until errorsProperty() is called.
1659      * @return
1660      */
1661     public static ObservableList<CssError> errorsProperty() {
1662         if (errors == null) {
1663             errors = FXCollections.observableArrayList();
1664         }
1665         return errors;
1666     }
1667 
1668     /**
1669      * Errors that may have occurred during css processing.
1670      * This list is null until errorsProperty() is called and is used
1671      * internally to figure out whether or  not anyone is interested in
1672      * receiving CssError.
1673      * Not meant for general use - call errorsProperty() instead.
1674      * @return
1675      */
1676     public static ObservableList<CssError> getErrors() {
1677         return errors;
1678     }
1679 
1680     ////////////////////////////////////////////////////////////////////////////
1681     //
1682     // Classes and routines for mapping styles to a Node
1683     //
1684     ////////////////////////////////////////////////////////////////////////////
1685 
1686     private static List<String> cacheMapKey;
1687 
1688     // Each Scene has its own cache
1689     // package for testing
1690     static class CacheContainer {
1691 
1692         private Map<StyleCache.Key,StyleCache> getStyleCache() {
1693             if (styleCache == null) styleCache = new HashMap<StyleCache.Key, StyleCache>();
1694             return styleCache;
1695         }
1696 
1697         private Map<Key,Cache> getCacheMap(List<StylesheetContainer> parentStylesheets) {
1698 
1699             if (cacheMap == null) {
1700                 cacheMap = new HashMap<List<String>,Map<Key,Cache>>();
1701             }
1702 
1703             if (parentStylesheets == null || parentStylesheets.isEmpty()) {
1704 
1705                 Map<Key,Cache> cmap = cacheMap.get(null);
1706                 if (cmap == null) {
1707                     cmap = new HashMap<Key,Cache>();
1708                     cacheMap.put(null, cmap);
1709                 }
1710                 return cmap;
1711 
1712             } else {
1713 
1714                 final int nMax = parentStylesheets.size();
1715                 if (cacheMapKey == null) {
1716                     cacheMapKey = new ArrayList<String>(nMax);
1717                 }
1718                 for (int n=0; n<nMax; n++) {
1719                     StylesheetContainer sc = parentStylesheets.get(n);
1720                     if (sc == null || sc.fname == null || sc.fname.isEmpty()) continue;
1721                     cacheMapKey.add(sc.fname);
1722                 }
1723                 Map<Key,Cache> cmap = cacheMap.get(cacheMapKey);
1724                 if (cmap == null) {
1725                     cmap = new HashMap<Key,Cache>();
1726                     cacheMap.put(cacheMapKey, cmap);
1727                     // create a new cacheMapKey the next time this method is called
1728                     cacheMapKey = null;
1729                 } else {
1730                     // reuse cacheMapKey, but not the data, the next time this method is called
1731                     cacheMapKey.clear();
1732                 }
1733                 return cmap;
1734 
1735             }
1736 
1737         }
1738 
1739         private List<StyleMap> getStyleMapList() {
1740             if (styleMapList == null) styleMapList = new ArrayList<StyleMap>();
1741             return styleMapList;
1742         }
1743 
1744         private int nextSmapId() {
1745             styleMapId = baseStyleMapId + getStyleMapList().size();
1746             return styleMapId;
1747         }
1748 
1749         private void addStyleMap(StyleMap smap) {
1750             assert ((smap.getId() - baseStyleMapId) == getStyleMapList().size());
1751             getStyleMapList().add(smap);
1752         }
1753 
1754         public StyleMap getStyleMap(int smapId) {
1755 
1756             final int correctedId = smapId - baseStyleMapId;
1757 
1758             if (0 <= correctedId && correctedId < getStyleMapList().size()) {
1759                 return getStyleMapList().get(correctedId);
1760             }
1761 
1762             return StyleMap.EMPTY_MAP;
1763         }
1764 
1765         private void clearCache() {
1766 
1767             if (cacheMap != null) cacheMap.clear();
1768             if (styleCache != null) styleCache.clear();
1769             if (styleMapList != null) styleMapList.clear();
1770 
1771             baseStyleMapId = styleMapId;
1772             // 7/8ths is totally arbitrary
1773             if (baseStyleMapId > Integer.MAX_VALUE/8*7) {
1774                 baseStyleMapId = styleMapId = 0;
1775             }
1776         }
1777 
1778         /**
1779          * Get the mapping of property to style from Node.style for this node.
1780          */
1781         private Selector getInlineStyleSelector(String inlineStyle) {
1782 
1783             // If there are no styles for this property then we can just bail
1784             if ((inlineStyle == null) || inlineStyle.trim().isEmpty()) return null;
1785 
1786             if (inlineStylesCache != null && inlineStylesCache.containsKey(inlineStyle)) {
1787                 // Value of Map entry may be null!
1788                 return inlineStylesCache.get(inlineStyle);
1789             }
1790 
1791             //
1792             // inlineStyle wasn't in the inlineStylesCache, or inlineStylesCache was null
1793             //
1794 
1795             if (inlineStylesCache == null) {
1796                 inlineStylesCache = new HashMap<>();
1797             }
1798 
1799             final Stylesheet inlineStylesheet =
1800                     CSSParser.getInstance().parse("*{"+inlineStyle+"}");
1801 
1802             if (inlineStylesheet != null) {
1803 
1804                 inlineStylesheet.setOrigin(StyleOrigin.INLINE);
1805 
1806                 List<Rule> rules = inlineStylesheet.getRules();
1807                 Rule rule = rules != null && !rules.isEmpty() ? rules.get(0) : null;
1808 
1809                 List<Selector> selectors = rule != null ? rule.getUnobservedSelectorList() : null;
1810                 Selector selector = selectors != null && !selectors.isEmpty() ? selectors.get(0) : null;
1811 
1812                 // selector might be null if parser throws some exception
1813                 if (selector != null) {
1814                     selector.setOrdinal(-1);
1815 
1816                     inlineStylesCache.put(inlineStyle, selector);
1817                     return selector;
1818                 }
1819                 // if selector is null, fall through
1820 
1821             }
1822 
1823             // even if selector is null, put it in cache so we don't
1824             // bother with trying to parse it again.
1825             inlineStylesCache.put(inlineStyle, null);
1826             return null;
1827 
1828         }
1829 
1830         private Map<StyleCache.Key,StyleCache> styleCache;
1831 
1832         private Map<List<String>, Map<Key,Cache>> cacheMap;
1833 
1834         private List<StyleMap> styleMapList;
1835 
1836         /**
1837          * Cache of parsed, inline styles. The key is Node.style.
1838          * The value is the Selector from the inline stylesheet.
1839          */
1840         private Map<String,Selector> inlineStylesCache;
1841 
1842         /*
1843          * A simple counter used to generate a unique id for a StyleMap.
1844          * This unique id is used by StyleHelper in figuring out which
1845          * style cache to use.
1846          */
1847         private int styleMapId = 0;
1848 
1849         // When the cache is cleared, styleMapId counting begins here.
1850         // If a StyleHelper calls getStyleMap with an id less than the
1851         // baseStyleMapId, then that StyleHelper is working with an old
1852         // cache and is no longer valid.
1853         private int baseStyleMapId = 0;
1854 
1855             }
1856 
1857     /**
1858      * Creates and caches maps of styles, reusing them as often as practical.
1859      */
1860     private static class Cache {
1861 
1862         private static class Key {
1863             final long[] key;
1864             final String inlineStyle;
1865 
1866             Key(long[] key, String inlineStyle) {
1867                 this.key = key;
1868                 // let inlineStyle be null if it is empty
1869                 this.inlineStyle =  (inlineStyle != null && inlineStyle.trim().isEmpty() ? null : inlineStyle);
1870             }
1871 
1872             @Override public String toString() {
1873                 return Arrays.toString(key) + (inlineStyle != null ? "* {" + inlineStyle + "}" : "");
1874             }
1875 
1876             @Override
1877             public int hashCode() {
1878                 int hash = 3;
1879                 hash = 17 * hash + Arrays.hashCode(this.key);
1880                 if (inlineStyle != null) hash = 17 * hash + inlineStyle.hashCode();
1881                 return hash;
1882             }
1883 
1884             @Override
1885             public boolean equals(Object obj) {
1886                 if (obj == null) {
1887                     return false;
1888                 }
1889                 if (getClass() != obj.getClass()) {
1890                     return false;
1891                 }
1892                 final Key other = (Key) obj;
1893                 if (inlineStyle == null ? other.inlineStyle != null : !inlineStyle.equals(other.inlineStyle)) {
1894                     return false;
1895                 }
1896                 if (!Arrays.equals(this.key, other.key)) {
1897                     return false;
1898                 }
1899                 return true;
1900             }
1901 
1902         }
1903 
1904         // this must be initialized to the appropriate possible selectors when
1905         // the helper cache is created by the StylesheetContainer. Note that
1906         // SelectorPartioning sorts the matched selectors by ordinal, so this
1907         // list of selectors will be in the same order in which the selectors
1908         // appear in the stylesheets.
1909         private final List<Selector> selectors;
1910         private final Map<Key, Integer> cache;
1911 
1912         Cache(List<Selector> selectors) {
1913             this.selectors = selectors;
1914             this.cache = new HashMap<Key, Integer>();
1915         }
1916 
1917         private StyleMap getStyleMap(CacheContainer cacheContainer, Node node, Set<PseudoClass>[] triggerStates, boolean hasInlineStyle) {
1918 
1919             if ((selectors == null || selectors.isEmpty()) && !hasInlineStyle) {
1920                 return StyleMap.EMPTY_MAP;
1921             }
1922 
1923             final int selectorDataSize = selectors.size();
1924 
1925             //
1926             // Since the list of selectors is found by matching only the
1927             // rightmost selector, the set of selectors may larger than those
1928             // selectors that actually match the node. The following loop
1929             // whittles the list down to those selectors that apply.
1930             //
1931             //
1932             // To lookup from the cache, we construct a key from a Long
1933             // where the selectors that match this particular node are
1934             // represented by bits on the long[].
1935             //
1936             long key[] = new long[selectorDataSize/Long.SIZE + 1];
1937             boolean nothingMatched = true;
1938 
1939             for (int s = 0; s < selectorDataSize; s++) {
1940 
1941                 final Selector sel = selectors.get(s);
1942 
1943                 //
1944                 // This particular flavor of applies takes a PseudoClassState[]
1945                 // fills in the pseudo-class states from the selectors where
1946                 // they apply to a node. This is an expedient to looking the
1947                 // applies loopa second time on the matching selectors. This has to
1948                 // be done ahead of the cache lookup since not all nodes that
1949                 // have the same set of selectors will have the same node hierarchy.
1950                 //
1951                 // For example, if I have .foo:hover:focused .bar:selected {...}
1952                 // and the "bar" node is 4 away from the root and the foo
1953                 // node is two away from the root, pseudoclassBits would be
1954                 // [selected, 0, hover:focused, 0]
1955                 // Note that the states run from leaf to root. This is how
1956                 // the code in StyleHelper expects things.
1957                 // Note also that, if the selector does not apply, the triggerStates
1958                 // is unchanged.
1959                 //
1960 
1961                 if (sel.applies(node, triggerStates, 0)) {
1962                     final int index = s / Long.SIZE;
1963                     final long mask = key[index] | 1l << s;
1964                     key[index] = mask;
1965                     nothingMatched = false;
1966                 }
1967             }
1968 
1969             // nothing matched!
1970             if (nothingMatched && hasInlineStyle == false) {
1971                 return StyleMap.EMPTY_MAP;
1972             }
1973 
1974             final String inlineStyle = node.getStyle();
1975             final Key keyObj = new Key(key, inlineStyle);
1976 
1977             if (cache.containsKey(keyObj)) {
1978                 Integer styleMapId = cache.get(keyObj);
1979                 final StyleMap styleMap = styleMapId != null
1980                         ? cacheContainer.getStyleMap(styleMapId.intValue())
1981                         : StyleMap.EMPTY_MAP;
1982                 return styleMap;
1983             }
1984 
1985             final List<Selector> selectors = new ArrayList<>();
1986 
1987             if (hasInlineStyle) {
1988                 Selector selector = cacheContainer.getInlineStyleSelector(inlineStyle);
1989                 if (selector != null) selectors.add(selector);
1990             }
1991 
1992             for (int k = 0; k<key.length; k++) {
1993 
1994                 if (key[k] == 0) continue;
1995 
1996                 final int offset = k * Long.SIZE;
1997 
1998                 for (int b = 0; b<Long.SIZE; b++) {
1999 
2000                     // bit at b in key[k] set?
2001                     final long mask = 1l << b;
2002                     if ((mask & key[k]) != mask) continue;
2003 
2004                     final Selector pair = this.selectors.get(offset + b);
2005                     selectors.add(pair);
2006                 }
2007             }
2008 
2009             int id = cacheContainer.nextSmapId();
2010             cache.put(keyObj, Integer.valueOf(id));
2011 
2012             final StyleMap styleMap = new StyleMap(id, selectors);
2013             cacheContainer.addStyleMap(styleMap);
2014             return styleMap;
2015         }
2016 
2017     }
2018 
2019     /**
2020      * Get the map of property to style from the rules and declarations
2021      * in the stylesheet. There is no need to do selector matching here since
2022      * the stylesheet is from parsing Node.style.
2023      */
2024     public StyleMap createInlineStyleMap(Styleable styleable) {
2025 
2026         Stylesheet inlineStylesheet =
2027                 CSSParser.getInstance().parseInlineStyle(styleable);
2028         if (inlineStylesheet == null)  return StyleMap.EMPTY_MAP;
2029 
2030         inlineStylesheet.setOrigin(StyleOrigin.INLINE);
2031 
2032         List<Selector> pairs = new ArrayList<>(1);
2033 
2034         int ordinal = 0;
2035 
2036         final List<Rule> stylesheetRules = inlineStylesheet.getRules();
2037 
2038         List<Selector> selectorList =  null;
2039 
2040         for (int i = 0, imax = stylesheetRules.size(); i < imax; i++) {
2041             
2042             final Rule rule = stylesheetRules.get(i);
2043             if (rule == null) continue;
2044 
2045             List<Selector> selectors = rule.getUnobservedSelectorList();
2046             if (selectorList == null || selectors.isEmpty()) continue;
2047 
2048             selectorList.addAll(selectors);
2049         }
2050 
2051         // TODO: should have a cacheContainer for inline styles?
2052         return new StyleMap(-1, selectorList);
2053     }
2054 
2055 
2056     /**
2057      * The key used in the cacheMap of the StylesheetContainer
2058      */
2059     private static class Key {
2060         // note that the class name here is the *full* class name, such as
2061         // javafx.scene.control.Button. We only bother parsing this down to the
2062         // last part when doing matching against selectors, and so want to avoid
2063         // having to do a bunch of preliminary parsing in places where it isn't
2064         // necessary.
2065         String className;
2066         String id;
2067         final StyleClassSet styleClasses;
2068 
2069         private Key() {
2070             styleClasses = new StyleClassSet();
2071         }
2072 
2073         @Override
2074         public boolean equals(Object o) {
2075             if (this == o) {
2076                 return true;
2077             }
2078             if (o instanceof Key) {
2079                 Key other = (Key)o;
2080 
2081                 if (className == null ? other.className != null : (className.equals(other.className) == false)) {
2082                     return false;
2083                 }
2084 
2085                 if (id == null ? other.id != null : (id.equals(other.id) == false)) {
2086                     return false;
2087                 }
2088 
2089                 return this.styleClasses.equals(other.styleClasses);
2090             }
2091             return true;
2092         }
2093 
2094         @Override
2095         public int hashCode() {
2096             int hash = 7;
2097             hash = 29 * hash + (this.className != null ? this.className.hashCode() : 0);
2098             hash = 29 * hash + (this.id != null ? this.id.hashCode() : 0);
2099             hash = 29 * hash + this.styleClasses.hashCode();
2100             return hash;
2101         }
2102 
2103     }
2104 
2105 
2106         }