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