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