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