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