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