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