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