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