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