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