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