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