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