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 }