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