com.sun.javafx.css.StyleManager Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javafx.css;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.ParentHelper;
import com.sun.javafx.util.DataURI;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.CssParser;
import javafx.css.FontFace;
import javafx.css.PseudoClass;
import javafx.css.Rule;
import javafx.css.Selector;
import javafx.css.StyleOrigin;
import javafx.css.Styleable;
import javafx.css.StyleConverter;
import javafx.css.Stylesheet;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.scene.text.Font;
import javafx.stage.Window;
import com.sun.javafx.logging.PlatformLogger;
import com.sun.javafx.logging.PlatformLogger.Level;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PermissionCollection;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Contains the stylesheet state for a single scene. This includes both the
* Stylesheets defined on the Scene itself as well as a map of stylesheets for
* "style"s defined on the Node itself. These containers are kept in the
* containerMap, key'd by the Scene to which they belong. One of the key
* responsibilities of the StylesheetContainer is to create and maintain an
* admittedly elaborate series of caches so as to minimize the amount of time it
* takes to match a Node to its eventual StyleHelper, and to reuse the
* StyleHelper as much as possible.
Initially, the cache is empty. It is
* recreated whenever the userStylesheets on the container change, or whenever
* the userAgentStylesheet changes. The cache is built up as nodes are looked
* for, and thus there is some overhead associated with the first lookup but
* which is then not repeated for subsequent lookups.
The cache system used
* is a two level cache. The first level cache simply maps the
* classname/id/styleclass combination of the request node to a 2nd level cache.
* If the node has "styles" specified then we still use this 2nd level cache,
* but must combine its selectors with the selectors specified in "styles" and perform
* more work to cascade properly.
The 2nd level cache contains a data
* structure called the Cache. The Cache contains an ordered sequence of Rules,
* a Long, and a Map. The ordered sequence of selectors are the selectors that *may*
* match a node with the given classname, id, and style class. For example,
* selectors which may apply are any selector where the simple selector of the selector
* contains a reference to the id, style class, or classname of the Node, or a
* compound selector whose "descendant" part is a simple selector which contains
* a reference to the id, style class, or classname of the Node.
During
* lookup, we will iterate over all the potential selectors and discover if they
* apply to this particular node. If so, then we toggle a bit position in the
* Long corresponding to the position of the selector that matched. This long then
* becomes our key into the final map.
Once we have established our key, we
* will visit the map and look for an existing StyleHelper. If we find a
* StyleHelper, then we will return it. If not, then we will take the Rules that
* matched and construct a new StyleHelper from their various parts.
This
* system, while elaborate, also provides for numerous fast paths and sharing of
* data structures which should dramatically reduce the memory and runtime
* performance overhead associated with CSS by reducing the matching overhead
* and caching as much as possible. We make no attempt to use weak references
* here, so if memory issues result one work around would be to toggle the root
* user agent stylesheet or stylesheets on the scene to cause the cache to be
* flushed.
*/
final public class StyleManager {
/**
* Global lock object for the StyleManager. StyleManager is a singleton,
* which loads CSS stylesheets and manages style caches for all scenes.
* It needs to be thread-safe since a Node or Scene can be constructed and
* load its stylesheets on an arbitrary thread, meaning that multiple Scenes
* can load or apply their stylesheets concurrently. The global lock is used
* to serialize access to the various state in the StyleManager.
*/
private static final Object styleLock = new Object();
private static PlatformLogger LOGGER;
private static PlatformLogger getLogger() {
if (LOGGER == null) {
LOGGER = com.sun.javafx.util.Logging.getCSSLogger();
}
return LOGGER;
}
private static class InstanceHolder {
final static StyleManager INSTANCE = new StyleManager();
}
/**
* Return the StyleManager instance.
*/
public static StyleManager getInstance() {
return InstanceHolder.INSTANCE;
}
private StyleManager() {
}
/**
* A map from a parent to its style cache. The parent is either a Scene root, or a
* Parent with author stylesheets. If a Scene or Parent is removed from the scene,
* it's cache is annihilated.
*/
// public for testing
public static final Map cacheContainerMap = new WeakHashMap<>();
// package for testing
CacheContainer getCacheContainer(Styleable styleable, SubScene subScene) {
if (styleable == null && subScene == null) return null;
Parent root = null;
if (subScene != null) {
root = subScene.getRoot();
} else if (styleable instanceof Node) {
Node node = (Node)styleable;
Scene scene = node.getScene();
if (scene != null) root = scene.getRoot();
} else if (styleable instanceof Window) {
// this catches the PopupWindow case
Scene scene = ((Window)styleable).getScene();
if (scene != null) root = scene.getRoot();
}
// todo: what other Styleables need to be handled here?
if (root == null) return null;
synchronized (styleLock) {
CacheContainer container = cacheContainerMap.get(root);
if (container == null) {
container = new CacheContainer();
cacheContainerMap.put(root, container);
}
return container;
}
}
/**
* StyleHelper uses this cache but it lives here so it can be cleared
* when style-sheets change.
*/
public StyleCache getSharedCache(Styleable styleable, SubScene subScene, StyleCache.Key key) {
CacheContainer container = getCacheContainer(styleable, subScene);
if (container == null) return null;
Map styleCache = container.getStyleCache();
if (styleCache == null) return null;
StyleCache sharedCache = styleCache.get(key);
if (sharedCache == null) {
sharedCache = new StyleCache();
styleCache.put(new StyleCache.Key(key), sharedCache);
}
return sharedCache;
}
public StyleMap getStyleMap(Styleable styleable, SubScene subScene, int smapId) {
if (smapId == -1) return StyleMap.EMPTY_MAP;
CacheContainer container = getCacheContainer(styleable, subScene);
if (container == null) return StyleMap.EMPTY_MAP;
return container.getStyleMap(smapId);
}
/**
* A list of user-agent stylesheets from Scene or SubScene.
* The order of the entries in this list does not matter since a Scene or
* SubScene will only have zero or one user-agent stylesheets.
*/
// public for testing
public final List userAgentStylesheetContainers = new ArrayList<>();
/**
* A list of user-agent stylesheet urls from calling setDefaultUserAgentStylesheet and
* addUserAgentStylesheet. The order of entries this list matters. The zeroth element is
* _the_ platform default.
*/
// public for testing
public final List platformUserAgentStylesheetContainers = new ArrayList<>();
// public for testing
public boolean hasDefaultUserAgentStylesheet = false;
////////////////////////////////////////////////////////////////////////////
//
// stylesheet handling
//
////////////////////////////////////////////////////////////////////////////
/*
* A container for stylesheets and the Parents or Scenes that use them.
* If a stylesheet is removed, then all other Parents or Scenes
* that use that stylesheet should get new styles if the
* stylesheet is added back in since the stylesheet may have been
* removed and re-added because it was edited (typical of SceneBuilder).
* This container provides the hooks to get back to those Parents or Scenes.
*
* StylesheetContainer are created and added to stylesheetContainerMap
* in the method gatherParentStylesheets.
*
* StylesheetContainer are created and added to sceneStylesheetMap in
* the method updateStylesheets
*/
// package for testing
static class StylesheetContainer {
// the stylesheet uri
final String fname;
// the parsed stylesheet so we don't reparse for every parent that uses it
final Stylesheet stylesheet;
// the parents or scenes that use this stylesheet. Typically, this list
// should be very small.
final SelectorPartitioning selectorPartitioning;
// who uses this stylesheet?
final RefList parentUsers;
final int hash;
final byte[] checksum;
boolean checksumInvalid = false;
StylesheetContainer(String fname, Stylesheet stylesheet) {
this(fname, stylesheet, stylesheet != null ? calculateCheckSum(stylesheet.getUrl()) : new byte[0]);
}
StylesheetContainer(String fname, Stylesheet stylesheet, byte[] checksum) {
this.fname = fname;
hash = (fname != null) ? fname.hashCode() : 127;
this.stylesheet = stylesheet;
if (stylesheet != null) {
selectorPartitioning = new SelectorPartitioning();
final List rules = stylesheet.getRules();
final int rMax = rules == null || rules.isEmpty() ? 0 : rules.size();
for (int r=0; r selectors = rule.getUnobservedSelectorList();
final List selectors = rule.getSelectors();
final int sMax = selectors == null || selectors.isEmpty() ? 0 : selectors.size();
for (int s=0; s < sMax; s++) {
final Selector selector = selectors.get(s);
selectorPartitioning.partition(selector);
}
}
} else {
selectorPartitioning = null;
}
this.parentUsers = new RefList<>();
this.checksum = checksum;
}
void invalidateChecksum() {
// if checksum is byte[0], then it is forever valid.
checksumInvalid = checksum.length > 0;
}
@Override
public int hashCode() {
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final StylesheetContainer other = (StylesheetContainer) obj;
if ((this.fname == null) ? (other.fname != null) : !this.fname.equals(other.fname)) {
return false;
}
return true;
}
@Override public String toString() {
return fname;
}
}
/*
* A list that holds references. Used by StylesheetContainer.
*/
// package for testing
static class RefList {
final List> list = new ArrayList<>();
void add(K key) {
for (int n=list.size()-1; 0<=n; --n) {
final Reference ref = list.get(n);
final K k = ref.get();
if (k == null) {
// stale reference, remove it.
list.remove(n);
} else {
// already have it, bail
if (k == key) {
return;
}
}
}
// not found, add it.
list.add(new WeakReference<>(key));
}
void remove(K key) {
for (int n=list.size()-1; 0<=n; --n) {
final Reference ref = list.get(n);
final K k = ref.get();
if (k == null) {
// stale reference, remove it.
list.remove(n);
} else {
// already have it, bail
if (k == key) {
list.remove(n);
return;
}
}
}
}
// for unit testing
boolean contains(K key) {
for (int n=list.size()-1; 0<=n; --n) {
final Reference ref = list.get(n);
final K k = ref.get();
if (k == key) {
return true;
}
}
return false;
}
}
/**
* A map from String => Stylesheet. If a stylesheet for the
* given URL has already been loaded then we'll simply reuse the stylesheet
* rather than loading a duplicate.
* This list is for author stylesheets and not for user-agent stylesheets. User-agent
* stylesheets are either platformUserAgentStylesheetContainers or userAgentStylesheetContainers
*/
// public for unit testing
public final Map stylesheetContainerMap = new HashMap<>();
/**
* called from Window when the scene is closed.
*/
public void forget(final Scene scene) {
if (scene == null) return;
forget(scene.getRoot());
synchronized (styleLock) {
//
// if this scene has user-agent stylesheets, clean up the userAgentStylesheetContainers list
//
String sceneUserAgentStylesheet = null;
if ((scene.getUserAgentStylesheet() != null) &&
(!(sceneUserAgentStylesheet = scene.getUserAgentStylesheet().trim()).isEmpty())) {
for(int n=userAgentStylesheetContainers.size()-1; 0<=n; --n) {
StylesheetContainer container = userAgentStylesheetContainers.get(n);
if (sceneUserAgentStylesheet.equals(container.fname)) {
container.parentUsers.remove(scene.getRoot());
if (container.parentUsers.list.size() == 0) {
userAgentStylesheetContainers.remove(n);
}
}
}
}
//
// remove any parents belonging to this scene from the stylesheetContainerMap
//
Set> stylesheetContainers = stylesheetContainerMap.entrySet();
Iterator> iter = stylesheetContainers.iterator();
while(iter.hasNext()) {
Entry entry = iter.next();
StylesheetContainer container = entry.getValue();
Iterator> parentIter = container.parentUsers.list.iterator();
while (parentIter.hasNext()) {
Reference ref = parentIter.next();
Parent _parent = ref.get();
if (_parent == null || _parent.getScene() == scene || _parent.getScene() == null) {
ref.clear();
parentIter.remove();
}
}
if (container.parentUsers.list.isEmpty()) {
iter.remove();
}
}
}
}
/**
* called from Scene's stylesheets property's onChanged method
*/
public void stylesheetsChanged(Scene scene, Change c) {
synchronized (styleLock) {
// Clear the cache so the cache will be rebuilt.
Set> entrySet = cacheContainerMap.entrySet();
for(Entry entry : entrySet) {
Parent parent = entry.getKey();
CacheContainer container = entry.getValue();
if (parent.getScene() == scene) {
container.clearCache();
}
}
c.reset();
while(c.next()) {
if (c.wasRemoved()) {
for (String fname : c.getRemoved()) {
stylesheetRemoved(scene, fname);
StylesheetContainer stylesheetContainer = stylesheetContainerMap.get(fname);
if (stylesheetContainer != null) {
stylesheetContainer.invalidateChecksum();
}
}
}
}
}
}
private void stylesheetRemoved(Scene scene, String fname) {
stylesheetRemoved(scene.getRoot(), fname);
}
/**
* Called from Parent's scenesChanged method when the Parent's scene is set to null.
* @param parent The Parent being removed from the scene-graph
*/
public void forget(Parent parent) {
if (parent == null) return;
synchronized (styleLock) {
// RT-34863 - clean up CSS cache when Parent is removed from scene-graph
CacheContainer removedContainer = cacheContainerMap.remove(parent);
if (removedContainer != null) {
removedContainer.clearCache();
}
final List stylesheets = parent.getStylesheets();
if (stylesheets != null && !stylesheets.isEmpty()) {
for (String fname : stylesheets) {
stylesheetRemoved(parent, fname);
}
}
Iterator> containerIterator = stylesheetContainerMap.entrySet().iterator();
while (containerIterator.hasNext()) {
Entry entry = containerIterator.next();
StylesheetContainer container = entry.getValue();
container.parentUsers.remove(parent);
if (container.parentUsers.list.isEmpty()) {
containerIterator.remove();
if (container.selectorPartitioning != null) {
container.selectorPartitioning.reset();
}
// clean up image cache by removing images from the cache that
// might have come from this stylesheet
final String fname = container.fname;
imageCache.cleanUpImageCache(fname);
}
}
// Do not iterate over children since this method will be called on each from Parent#scenesChanged
}
}
/**
* called from Parent's stylesheets property's onChanged method
*/
public void stylesheetsChanged(Parent parent, Change c) {
synchronized (styleLock) {
c.reset();
while(c.next()) {
if (c.wasRemoved()) {
for (String fname : c.getRemoved()) {
stylesheetRemoved(parent, fname);
StylesheetContainer stylesheetContainer = stylesheetContainerMap.get(fname);
if (stylesheetContainer != null) {
stylesheetContainer.invalidateChecksum();
}
}
}
}
}
}
private void stylesheetRemoved(Parent parent, String fname) {
synchronized (styleLock) {
StylesheetContainer stylesheetContainer = stylesheetContainerMap.get(fname);
if (stylesheetContainer == null) return;
stylesheetContainer.parentUsers.remove(parent);
if (stylesheetContainer.parentUsers.list.isEmpty()) {
removeStylesheetContainer(stylesheetContainer);
}
}
}
/**
* called from Window when the scene is closed.
*/
public void forget(final SubScene subScene) {
if (subScene == null) return;
final Parent subSceneRoot = subScene.getRoot();
if (subSceneRoot == null) return;
forget(subSceneRoot);
synchronized (styleLock) {
//
// if this scene has user-agent stylesheets, clean up the userAgentStylesheetContainers list
//
String sceneUserAgentStylesheet = null;
if ((subScene.getUserAgentStylesheet() != null) &&
(!(sceneUserAgentStylesheet = subScene.getUserAgentStylesheet().trim()).isEmpty())) {
Iterator iterator = userAgentStylesheetContainers.iterator();
while(iterator.hasNext()) {
StylesheetContainer container = iterator.next();
if (sceneUserAgentStylesheet.equals(container.fname)) {
container.parentUsers.remove(subScene.getRoot());
if (container.parentUsers.list.size() == 0) {
iterator.remove();
}
}
}
}
//
// remove any parents belonging to this SubScene from the stylesheetContainerMap
//
// copy the list to avoid concurrent mod.
List stylesheetContainers = new ArrayList<>(stylesheetContainerMap.values());
Iterator iter = stylesheetContainers.iterator();
while(iter.hasNext()) {
StylesheetContainer container = iter.next();
Iterator> parentIter = container.parentUsers.list.iterator();
while (parentIter.hasNext()) {
final Reference ref = parentIter.next();
final Parent _parent = ref.get();
if (_parent != null) {
// if this stylesheet refererent is a child of this subscene, nuke it.
Parent p = _parent;
while (p != null) {
if (subSceneRoot == p.getParent()) {
ref.clear();
parentIter.remove();
forget(_parent); // _parent, not p!
break;
}
p = p.getParent();
}
}
}
// forget(_parent) will remove the container if the parentUser's list is empty
// if (container.parentUsers.list.isEmpty()) {
// iter.remove();
// }
}
}
}
private void removeStylesheetContainer(StylesheetContainer stylesheetContainer) {
if (stylesheetContainer == null) return;
synchronized (styleLock) {
final String fname = stylesheetContainer.fname;
stylesheetContainerMap.remove(fname);
if (stylesheetContainer.selectorPartitioning != null) {
stylesheetContainer.selectorPartitioning.reset();
}
// if container has no references, then remove it
for(Entry entry : cacheContainerMap.entrySet()) {
CacheContainer container = entry.getValue();
if (container == null || container.cacheMap == null || container.cacheMap.isEmpty()) {
continue;
}
List> entriesToRemove = new ArrayList<>();
for (Entry, Map> cacheMapEntry : container.cacheMap.entrySet()) {
List cacheMapKey = cacheMapEntry.getKey();
if (cacheMapKey != null ? cacheMapKey.contains(fname) : fname == null) {
entriesToRemove.add(cacheMapKey);
}
}
if (!entriesToRemove.isEmpty()) {
for (List cacheMapKey : entriesToRemove) {
Map cacheEntry = container.cacheMap.remove(cacheMapKey);
if (cacheEntry != null) {
cacheEntry.clear();
}
}
}
}
// clean up image cache by removing images from the cache that
// might have come from this stylesheet
imageCache.cleanUpImageCache(fname);
final List> parentList = stylesheetContainer.parentUsers.list;
for (int n=parentList.size()-1; 0<=n; --n) {
final Reference ref = parentList.remove(n);
final Parent parent = ref.get();
ref.clear();
if (parent == null || parent.getScene() == null) {
continue;
}
//
// tell parent it needs to reapply css
// No harm is done if parent is in a scene that has had
// NodeHelper.reapplyCSS called on the root.
//
NodeHelper.reapplyCSS(parent);
}
}
}
////////////////////////////////////////////////////////////////////////////
//
// Image caching
//
////////////////////////////////////////////////////////////////////////////
private final static class ImageCache {
private Map> imageCache = new HashMap<>();
Image getCachedImage(String url) {
synchronized (styleLock) {
Image image = null;
if (imageCache.containsKey(url)) {
image = imageCache.get(url).get();
}
if (image == null) {
try {
image = new Image(url);
// RT-31865
if (image.isError()) {
final PlatformLogger logger = getLogger();
if (logger != null && logger.isLoggable(Level.WARNING)) {
// If we have a "data" URL, we should use DataURI.toString() instead
// of just logging the entire URL. This truncates the data contained
// in the URL and prevents cluttering the log.
DataURI dataUri = DataURI.tryParse(url);
if (dataUri != null) {
logger.warning("Error loading image: " + dataUri);
} else {
logger.warning("Error loading image: " + url);
}
}
image = null;
}
imageCache.put(url, new SoftReference<>(image));
} catch (IllegalArgumentException | NullPointerException ex) {
// url was empty!
final PlatformLogger logger = getLogger();
if (logger != null && logger.isLoggable(Level.WARNING)) {
logger.warning(ex.getLocalizedMessage());
}
} // url was null!
}
return image;
}
}
void cleanUpImageCache(String imgFname) {
synchronized (styleLock) {
if (imgFname == null || imageCache.isEmpty()) return;
final String fname = imgFname.trim();
if (fname.isEmpty()) return;
int len = fname.lastIndexOf('/');
final String path = (len > 0) ? fname.substring(0,len) : fname;
final int plen = path.length();
final String[] entriesToRemove = new String[imageCache.size()];
int count = 0;
final Set>> entrySet = imageCache.entrySet();
for (Entry> entry : entrySet) {
final String key = entry.getKey();
if (entry.getValue().get() == null) {
entriesToRemove[count++] = key;
continue;
}
len = key.lastIndexOf('/');
final String kpath = (len > 0) ? key.substring(0, len) : key;
final int klen = kpath.length();
// If the longer path begins with the shorter path,
// then assume the image came from this path.
boolean match = (klen > plen) ? kpath.startsWith(path) : path.startsWith(kpath);
if (match) {
entriesToRemove[count++] = key;
}
}
for (int n = 0; n < count; n++) {
imageCache.remove(entriesToRemove[n]);
}
}
}
}
private final ImageCache imageCache = new ImageCache();
public Image getCachedImage(String url) {
return imageCache.getCachedImage(url);
}
////////////////////////////////////////////////////////////////////////////
//
// Stylesheet loading
//
////////////////////////////////////////////////////////////////////////////
private static final String skinPrefix = "com/sun/javafx/scene/control/skin/";
private static final String skinUtilsClassName = "com.sun.javafx.scene.control.skin.Utils";
private static URL getURL(final String str) {
// Note: this code is duplicated, more or less, in URLConverter
if (str == null || str.trim().isEmpty()) return null;
try {
URI uri = new URI(str.trim());
// if url doesn't have a scheme
if (uri.isAbsolute() == false) {
// FIXME: JIGSAW -- move this into a utility method, since it will
// likely be needed elsewhere (e.g., in URLConverter)
if (str.startsWith(skinPrefix) &&
(str.endsWith(".css") || str.endsWith(".bss"))) {
try {
ClassLoader cl = StyleManager.class.getClassLoader();
Class> clz = Class.forName(skinUtilsClassName, true, cl);
Method m_getResource = clz.getMethod("getResource", String.class);
return (URL)m_getResource.invoke(null, str.substring(skinPrefix.length()));
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InvocationTargetException ex) {
ex.printStackTrace();
return null;
}
}
final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
final String path = uri.getPath();
URL resource = null;
// FIXME: JIGSAW -- The following will only find resources not in a module
if (path.startsWith("/")) {
resource = contextClassLoader.getResource(path.substring(1));
} else {
resource = contextClassLoader.getResource(path);
}
return resource;
}
// else, url does have a scheme
return uri.toURL();
} catch (MalformedURLException malf) {
// Do not log exception here - caller will handle null return.
// For example, we might be looking for a .bss that doesn't exist
return null;
} catch (URISyntaxException urise) {
return null;
}
}
// Calculate checksum for stylesheet file. Return byte[0] if checksum could not be calculated.
static byte[] calculateCheckSum(String fname) {
if (fname == null || fname.isEmpty()) return new byte[0];
try {
final URL url = getURL(fname);
// We only care about stylesheets from file: URLs.
if (url != null && "file".equals(url.getProtocol())) {
// not looking for security, just a checksum. MD5 should be faster than SHA
try (final InputStream stream = url.openStream();
final DigestInputStream dis = new DigestInputStream(stream, MessageDigest.getInstance("MD5")); ) {
dis.getMessageDigest().reset();
byte[] buffer = new byte[4096];
while (dis.read(buffer) != -1) { /* empty loop body is intentional */ }
return dis.getMessageDigest().digest();
}
}
} catch (IllegalArgumentException | NoSuchAlgorithmException | IOException | SecurityException e) {
// IOException also covers MalformedURLException
// SecurityException means some untrusted app
// Fall through...
}
return new byte[0];
}
@SuppressWarnings("removal")
public static Stylesheet loadStylesheet(final String fname) {
try {
return loadStylesheetUnPrivileged(fname);
} catch (java.security.AccessControlException ace) {
// FIXME: JIGSAW -- we no longer are in a jar file, so this code path
// is obsolete and needs to be redone or eliminated. Fortunately, I
// don't think it is actually needed.
System.err.println("WARNING: security exception trying to load: " + fname);
/*
** we got an access control exception, so
** we could be running with a security manager.
** we'll allow the app to read a css file from our runtime jar,
** and give it one more chance.
*/
/*
** check that there are enough chars after the !/ to have a valid .css or .bss file name
*/
if ((fname.length() < 7) && (fname.indexOf("!/") < fname.length()-7)) {
return null;
}
/*
**
** first check that it's actually looking for the same runtime jar
** that we're running from, and not some other file.
*/
try {
URI requestedFileUrI = new URI(fname);
/*
** is the requested file in a jar
*/
if ("jar".equals(requestedFileUrI.getScheme())) {
/*
** let's check that the css file is being requested from our
** runtime jar
*/
URI styleManagerJarURI = AccessController.doPrivileged((PrivilegedExceptionAction) () -> StyleManager.class.getProtectionDomain().getCodeSource().getLocation().toURI());
final String styleManagerJarPath = styleManagerJarURI.getSchemeSpecificPart();
String requestedFilePath = requestedFileUrI.getSchemeSpecificPart();
String requestedFileJarPart = requestedFilePath.substring(requestedFilePath.indexOf('/'), requestedFilePath.indexOf("!/"));
/*
** it's the correct jar, check it's a file access
** strip off the leading jar
*/
if (styleManagerJarPath.equals(requestedFileJarPart)) {
/*
** strip off the leading "jar",
** the css file name is past the last '!'
*/
String requestedFileJarPathNoLeadingSlash = fname.substring(fname.indexOf("!/")+2);
/*
** check that it's looking for a css file in the runtime jar
*/
if (fname.endsWith(".css") || fname.endsWith(".bss")) {
/*
** set up a read permission for the jar
*/
FilePermission perm = new FilePermission(styleManagerJarPath, "read");
PermissionCollection perms = perm.newPermissionCollection();
perms.add(perm);
AccessControlContext permsAcc = new AccessControlContext(
new ProtectionDomain[] {
new ProtectionDomain(null, perms)
});
/*
** check that the jar file exists, and that we're allowed to
** read it.
*/
JarFile jar = null;
try {
jar = AccessController.doPrivileged((PrivilegedExceptionAction) () -> new JarFile(styleManagerJarPath), permsAcc);
} catch (PrivilegedActionException pae) {
/*
** we got either a FileNotFoundException or an IOException
** in the privileged read. Return the same error as we
** would have returned if the css file hadn't of existed.
*/
return null;
}
if (jar != null) {
/*
** check that the file is in the jar
*/
JarEntry entry = jar.getJarEntry(requestedFileJarPathNoLeadingSlash);
if (entry != null) {
/*
** allow read access to the jar
*/
return AccessController.doPrivileged(
(PrivilegedAction) () -> loadStylesheetUnPrivileged(fname), permsAcc);
}
}
}
}
}
/*
** no matter what happen, we return the same error that would
** be returned if the css file hadn't of existed.
** That way there in no information leaked.
*/
return null;
}
/*
** no matter what happen, we return the same error that would
** be returned if the css file hadn't of existed.
** That way there in no information leaked.
*/
catch (java.net.URISyntaxException e) {
return null;
}
catch (java.security.PrivilegedActionException e) {
return null;
}
}
}
private static Stylesheet loadStylesheetUnPrivileged(final String fname) {
synchronized (styleLock) {
@SuppressWarnings("removal")
Boolean parse = AccessController.doPrivileged((PrivilegedAction) () -> {
final String bss = System.getProperty("binary.css");
// binary.css is true by default.
// parse only if the file is not a .bss
// and binary.css is set to false
return (!fname.endsWith(".bss") && bss != null) ?
!Boolean.valueOf(bss) : Boolean.FALSE;
});
try {
final String ext = (parse) ? (".css") : (".bss");
java.net.URL url = null;
Stylesheet stylesheet = null;
// check if url has extension, if not then just url as is and always parse as css text
if (!(fname.endsWith(".css") || fname.endsWith(".bss"))) {
url = getURL(fname);
} else {
final String name = fname.substring(0, fname.length() - 4);
url = getURL(name+ext);
if (url == null && (parse = !parse)) {
// If we failed to get the URL for the .bss file,
// fall back to the .css file.
// Note that 'parse' is toggled in the test.
url = getURL(name+".css");
}
if ((url != null) && !parse) {
try {
// RT-36332: if loadBinary throws an IOException, make sure to try .css
stylesheet = Stylesheet.loadBinary(url);
} catch (IOException ignored) {
}
if (stylesheet == null) {
// If we failed to load the .bss file,
// fall back to the .css file.
url = getURL(fname);
}
}
}
if (stylesheet == null) {
DataURI dataUri = null;
if (url != null) {
stylesheet = new CssParser().parse(url);
} else {
dataUri = DataURI.tryParse(fname);
}
if (dataUri != null) {
boolean isText =
"text".equalsIgnoreCase(dataUri.getMimeType())
&& ("css".equalsIgnoreCase(dataUri.getMimeSubtype())
|| "plain".equalsIgnoreCase(dataUri.getMimeSubtype()));
boolean isBinary =
"application".equalsIgnoreCase(dataUri.getMimeType())
&& "octet-stream".equalsIgnoreCase(dataUri.getMimeSubtype());
if (isText) {
String charsetName = dataUri.getParameters().get("charset");
Charset charset;
try {
charset = charsetName != null ? Charset.forName(charsetName) : Charset.defaultCharset();
} catch (IllegalCharsetNameException | UnsupportedCharsetException ex) {
String message = String.format(
"Unsupported charset \"%s\" in stylesheet URI \"%s\"", charsetName, dataUri);
if (errors != null) {
errors.add(new CssParser.ParseError(message));
}
if (getLogger().isLoggable(Level.WARNING)) {
getLogger().warning(message);
}
return null;
}
var stylesheetText = new String(dataUri.getData(), charset);
stylesheet = new CssParser().parse(stylesheetText);
} else if (isBinary) {
try (InputStream stream = new ByteArrayInputStream(dataUri.getData())) {
stylesheet = Stylesheet.loadBinary(stream);
}
} else {
String message = String.format("Unexpected MIME type \"%s/%s\" in stylesheet URI \"%s\"",
dataUri.getMimeType(), dataUri.getMimeSubtype(), dataUri);
if (errors != null) {
errors.add(new CssParser.ParseError(message));
}
if (getLogger().isLoggable(Level.WARNING)) {
getLogger().warning(message);
}
return null;
}
}
}
if (stylesheet == null) {
if (errors != null) {
CssParser.ParseError error =
new CssParser.ParseError(
"Resource \""+fname+"\" not found."
);
errors.add(error);
}
if (getLogger().isLoggable(Level.WARNING)) {
getLogger().warning(
String.format("Resource \"%s\" not found.", fname)
);
}
}
// load any fonts from @font-face
if (stylesheet != null) {
faceLoop: for(FontFace fontFace: stylesheet.getFontFaces()) {
if (fontFace instanceof FontFaceImpl) {
for(FontFaceImpl.FontFaceSrc src: ((FontFaceImpl)fontFace).getSources()) {
if (src.getType() == FontFaceImpl.FontFaceSrcType.URL) {
Font loadedFont = Font.loadFont(src.getSrc(),10);
if (loadedFont == null) {
getLogger().info("Could not load @font-face font [" + src.getSrc() + "]");
}
continue faceLoop;
}
}
}
}
}
return stylesheet;
} catch (FileNotFoundException fnfe) {
if (errors != null) {
CssParser.ParseError error =
new CssParser.ParseError(
"Stylesheet \""+fname+"\" not found."
);
errors.add(error);
}
if (getLogger().isLoggable(Level.INFO)) {
getLogger().info("Could not find stylesheet: " + fname);//, fnfe);
}
} catch (IOException ioe) {
// For data URIs, use the pretty-printed version for logging
var dataUri = DataURI.tryParse(fname);
String stylesheetName = dataUri != null ? dataUri.toString() : fname;
if (errors != null) {
errors.add(new CssParser.ParseError("Could not load stylesheet: " + stylesheetName));
}
if (getLogger().isLoggable(Level.INFO)) {
getLogger().info("Could not load stylesheet: " + stylesheetName);
}
}
return null;
}
}
////////////////////////////////////////////////////////////////////////////
//
// User Agent stylesheet handling
//
////////////////////////////////////////////////////////////////////////////
/**
* Set a bunch of user agent stylesheets all at once. The order of the stylesheets in the list
* is the order of their styles in the cascade. Passing null, an empty list, or a list full of empty
* strings does nothing.
*
* @param urls The list of stylesheet URLs as Strings.
*/
public void setUserAgentStylesheets(List urls) {
if (urls == null || urls.size() == 0) return;
synchronized (styleLock) {
// Avoid resetting user agent stylesheets if they haven't changed.
if (urls.size() == platformUserAgentStylesheetContainers.size()) {
boolean isSame = true;
for (int n=0, nMax=urls.size(); n < nMax && isSame; n++) {
final String url = urls.get(n);
final String fname = (url != null) ? url.trim() : null;
if (fname == null || fname.isEmpty()) break;
StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
isSame = fname.equals(container.fname);
if (isSame) {
// don't use fname in calculateCheckSum since it is just the key to
// find the StylesheetContainer. Rather, use the URL of the
// stylesheet that was already loaded. For example, we could have
// fname = "com/sun/javafx/scene/control/skin/modena/modena.css, but
// the stylesheet URL could be jar:file://some/path/!com/sun/javafx/scene/control/skin/modena/modena.bss
String stylesheetUrl = container.stylesheet.getUrl();
byte[] checksum = calculateCheckSum(stylesheetUrl);
isSame = Arrays.equals(checksum, container.checksum);
}
}
if (isSame) return;
}
boolean modified = false;
for (int n=0, nMax=urls.size(); n < nMax; n++) {
final String url = urls.get(n);
final String fname = (url != null) ? url.trim() : null;
if (fname == null || fname.isEmpty()) continue;
if (!modified) {
// we have at least one non null or non-empty url
platformUserAgentStylesheetContainers.clear();
modified = true;
}
if (n==0) {
_setDefaultUserAgentStylesheet(fname);
} else {
_addUserAgentStylesheet(fname);
}
}
if (modified) {
userAgentStylesheetsChanged();
}
}
}
/**
* Add a user agent stylesheet, possibly overriding styles in the default
* user agent stylesheet.
*
* @param fname The file URL, either relative or absolute, as a String.
*/
public void addUserAgentStylesheet(String fname) {
addUserAgentStylesheet(null, fname);
}
/**
* Add a user agent stylesheet, possibly overriding styles in the default
* user agent stylesheet.
* @param scene Only used in CssError for tracking back to the scene that loaded the stylesheet
* @param url The file URL, either relative or absolute, as a String.
*/
// For RT-20643
public void addUserAgentStylesheet(Scene scene, String url) {
final String fname = (url != null) ? url.trim() : null;
if (fname == null || fname.isEmpty()) {
return;
}
synchronized (styleLock) {
if (_addUserAgentStylesheet(fname)) {
userAgentStylesheetsChanged();
}
}
}
// fname is assumed to be non null and non empty
private boolean _addUserAgentStylesheet(String fname) {
synchronized (styleLock) {
// if we already have this stylesheet, bail
for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
if (fname.equals(container.fname)) {
return false;
}
}
final Stylesheet ua_stylesheet = loadStylesheet(fname);
if (ua_stylesheet == null) return false;
ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
platformUserAgentStylesheetContainers.add(new StylesheetContainer(fname, ua_stylesheet));
return true;
}
}
/**
* Add a user agent stylesheet, possibly overriding styles in the default
* user agent stylesheet.
* @param scene Only used in CssError for tracking back to the scene that loaded the stylesheet
* @param ua_stylesheet The stylesheet to add as a user-agent stylesheet
*/
public void addUserAgentStylesheet(Scene scene, Stylesheet ua_stylesheet) {
if (ua_stylesheet == null ) {
throw new IllegalArgumentException("null arg ua_stylesheet");
}
// null url is ok, just means that it is a stylesheet not loaded from a file
String url = ua_stylesheet.getUrl();
final String fname = url != null ? url.trim() : "";
synchronized (styleLock) {
// if we already have this stylesheet, bail
for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
if (fname.equals(container.fname)) {
return;
}
}
platformUserAgentStylesheetContainers.add(new StylesheetContainer(fname, ua_stylesheet));
if (ua_stylesheet != null) {
ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
}
userAgentStylesheetsChanged();
}
}
/**
* Set the default user agent stylesheet.
*
* @param fname The file URL, either relative or absolute, as a String.
*/
public void setDefaultUserAgentStylesheet(String fname) {
setDefaultUserAgentStylesheet(null, fname);
}
/**
* Set the default user agent stylesheet
* @param scene Only used in CssError for tracking back to the scene that loaded the stylesheet
* @param url The file URL, either relative or absolute, as a String.
*/
// For RT-20643
public void setDefaultUserAgentStylesheet(Scene scene, String url) {
final String fname = (url != null) ? url.trim() : null;
if (fname == null || fname.isEmpty()) {
return;
}
synchronized (styleLock) {
if(_setDefaultUserAgentStylesheet(fname)) {
userAgentStylesheetsChanged();
}
}
}
// fname is expected to be non null and non empty
private boolean _setDefaultUserAgentStylesheet(String fname) {
synchronized (styleLock) {
// if we already have this stylesheet, make sure it is the first element
for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
if (fname.equals(container.fname)) {
if (n > 0) {
platformUserAgentStylesheetContainers.remove(n);
if (hasDefaultUserAgentStylesheet) {
platformUserAgentStylesheetContainers.set(0, container);
} else {
platformUserAgentStylesheetContainers.add(0, container);
}
}
// return true only if platformUserAgentStylesheetContainers was modified
return n > 0;
}
}
final Stylesheet ua_stylesheet = loadStylesheet(fname);
if (ua_stylesheet == null) return false;
ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
final StylesheetContainer sc = new StylesheetContainer(fname, ua_stylesheet);
if (platformUserAgentStylesheetContainers.size() == 0) {
platformUserAgentStylesheetContainers.add(sc);
}
else if (hasDefaultUserAgentStylesheet) {
platformUserAgentStylesheetContainers.set(0,sc);
}
else {
platformUserAgentStylesheetContainers.add(0,sc);
}
hasDefaultUserAgentStylesheet = true;
return true;
}
}
/**
* Removes the specified stylesheet from the application default user agent
* stylesheet list.
* @param url The file URL, either relative or absolute, as a String.
*/
public void removeUserAgentStylesheet(String url) {
final String fname = (url != null) ? url.trim() : null;
if (fname == null || fname.isEmpty()) {
return;
}
synchronized (styleLock) {
// if we already have this stylesheet, remove it!
boolean removed = false;
for (int n = platformUserAgentStylesheetContainers.size() - 1; n >= 0; n--) {
// don't remove the platform default user agent stylesheet
if (fname.equals(Application.getUserAgentStylesheet())) {
continue;
}
StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
if (fname.equals(container.fname)) {
platformUserAgentStylesheetContainers.remove(n);
removed = true;
}
}
if (removed) {
userAgentStylesheetsChanged();
}
}
}
/**
* Set the user agent stylesheet. This is the base default stylesheet for
* the platform
*/
public void setDefaultUserAgentStylesheet(Stylesheet ua_stylesheet) {
if (ua_stylesheet == null ) {
return;
}
// null url is ok, just means that it is a stylesheet not loaded from a file
String url = ua_stylesheet.getUrl();
final String fname = url != null ? url.trim() : "";
synchronized (styleLock) {
// if we already have this stylesheet, make sure it is the first element
for (int n=0, nMax= platformUserAgentStylesheetContainers.size(); n < nMax; n++) {
StylesheetContainer container = platformUserAgentStylesheetContainers.get(n);
if (fname.equals(container.fname)) {
if (n > 0) {
platformUserAgentStylesheetContainers.remove(n);
if (hasDefaultUserAgentStylesheet) {
platformUserAgentStylesheetContainers.set(0, container);
} else {
platformUserAgentStylesheetContainers.add(0, container);
}
}
return;
}
}
StylesheetContainer sc = new StylesheetContainer(fname, ua_stylesheet);
if (platformUserAgentStylesheetContainers.size() == 0) {
platformUserAgentStylesheetContainers.add(sc);
} else if (hasDefaultUserAgentStylesheet) {
platformUserAgentStylesheetContainers.set(0,sc);
} else {
platformUserAgentStylesheetContainers.add(0,sc);
}
hasDefaultUserAgentStylesheet = true;
ua_stylesheet.setOrigin(StyleOrigin.USER_AGENT);
userAgentStylesheetsChanged();
}
}
/*
* If the userAgentStylesheets change, then all scenes are updated.
*/
private void userAgentStylesheetsChanged() {
List parents = new ArrayList<>();
synchronized (styleLock) {
for (CacheContainer container : cacheContainerMap.values()) {
container.clearCache();
}
StyleConverter.clearCache();
for (Parent root : cacheContainerMap.keySet()) {
if (root == null) {
continue;
}
parents.add(root);
}
}
for (Parent root : parents) NodeHelper.reapplyCSS(root);
}
private List processStylesheets(List stylesheets, Parent parent) {
synchronized (styleLock) {
final List list = new ArrayList<>();
for (int n = 0, nMax = stylesheets.size(); n < nMax; n++) {
final String fname = stylesheets.get(n);
StylesheetContainer container = null;
if (stylesheetContainerMap.containsKey(fname)) {
container = stylesheetContainerMap.get(fname);
if (!list.contains(container)) {
// minor optimization: if existing checksum in byte[0], then don't bother recalculating
if (container.checksumInvalid) {
final byte[] checksum = calculateCheckSum(fname);
if (!Arrays.equals(checksum, container.checksum)) {
removeStylesheetContainer(container);
// Stylesheet did change. Re-load the stylesheet and update the container map.
Stylesheet stylesheet = loadStylesheet(fname);
container = new StylesheetContainer(fname, stylesheet, checksum);
stylesheetContainerMap.put(fname, container);
} else {
container.checksumInvalid = false;
}
}
list.add(container);
}
// RT-22565: remember that this parent or scene uses this stylesheet.
// Later, if the cache is cleared, the parent or scene is told to
// reapply css.
container.parentUsers.add(parent);
} else {
final Stylesheet stylesheet = loadStylesheet(fname);
// stylesheet may be null which would mean that some IOException
// was thrown while trying to load it. Add it to the
// stylesheetContainerMap anyway as this will prevent further
// attempts to parse the file
container = new StylesheetContainer(fname, stylesheet);
// RT-22565: remember that this parent or scene uses this stylesheet.
// Later, if the cache is cleared, the parent or scene is told to
// reapply css.
container.parentUsers.add(parent);
stylesheetContainerMap.put(fname, container);
list.add(container);
}
}
return list;
}
}
//
// recurse so that stylesheets of Parents closest to the root are
// added to the list first. The ensures that declarations for
// stylesheets further down the tree (closer to the leaf) have
// a higher ordinal in the cascade.
//
private List gatherParentStylesheets(final Parent parent) {
if (parent == null) {
return Collections.emptyList();
}
final List parentStylesheets = ParentHelper.getAllParentStylesheets(parent);
if (parentStylesheets == null || parentStylesheets.isEmpty()) {
return Collections.emptyList();
}
synchronized (styleLock) {
return processStylesheets(parentStylesheets, parent);
}
}
//
//
//
private List gatherSceneStylesheets(final Scene scene) {
if (scene == null) {
return Collections.emptyList();
}
final List sceneStylesheets = scene.getStylesheets();
if (sceneStylesheets == null || sceneStylesheets.isEmpty()) {
return Collections.emptyList();
}
synchronized (styleLock) {
return processStylesheets(sceneStylesheets, scene.getRoot());
}
}
// reuse key to avoid creation of numerous small objects
private Key key = null;
// Stores weak references to regions which return non-null user agent stylesheets
private final WeakHashMap weakRegionUserAgentStylesheetMap = new WeakHashMap<>();
/**
* Finds matching styles for this Node.
*/
public StyleMap findMatchingStyles(Node node, SubScene subScene, Set[] triggerStates) {
final Scene scene = node.getScene();
if (scene == null) {
return StyleMap.EMPTY_MAP;
}
CacheContainer cacheContainer = getCacheContainer(node, subScene);
if (cacheContainer == null) {
assert false : node.toString();
return StyleMap.EMPTY_MAP;
}
synchronized (styleLock) {
final Parent parent =
(node instanceof Parent)
? (Parent) node : node.getParent();
final List parentStylesheets =
gatherParentStylesheets(parent);
final boolean hasParentStylesheets = parentStylesheets.isEmpty() == false;
final List sceneStylesheets = gatherSceneStylesheets(scene);
final boolean hasSceneStylesheets = sceneStylesheets.isEmpty() == false;
final String inlineStyle = node.getStyle();
final boolean hasInlineStyles = inlineStyle != null && inlineStyle.trim().isEmpty() == false;
final String sceneUserAgentStylesheet = scene.getUserAgentStylesheet();
final boolean hasSceneUserAgentStylesheet =
sceneUserAgentStylesheet != null && sceneUserAgentStylesheet.trim().isEmpty() == false;
final String subSceneUserAgentStylesheet =
(subScene != null) ? subScene.getUserAgentStylesheet() : null;
final boolean hasSubSceneUserAgentStylesheet =
subSceneUserAgentStylesheet != null && subSceneUserAgentStylesheet.trim().isEmpty() == false;
String regionUserAgentStylesheet = null;
// is this node in a region that has its own stylesheet?
Node region = node;
while (region != null) {
if (region instanceof Region) {
regionUserAgentStylesheet = weakRegionUserAgentStylesheetMap.computeIfAbsent(
(Region)region, Region::getUserAgentStylesheet);
if (regionUserAgentStylesheet != null) {
// We want 'region' to be the node that has the user agent stylesheet.
// 'region' is used below - look for if (hasRegionUserAgentStylesheet) block
break;
}
}
region = region.getParent();
}
final boolean hasRegionUserAgentStylesheet =
regionUserAgentStylesheet != null && regionUserAgentStylesheet.trim().isEmpty() == false;
//
// Are there any stylesheets at all?
// If not, then there is nothing to match and the
// resulting StyleMap is going to end up empty
//
if (hasInlineStyles == false
&& hasParentStylesheets == false
&& hasSceneStylesheets == false
&& hasSceneUserAgentStylesheet == false
&& hasSubSceneUserAgentStylesheet == false
&& hasRegionUserAgentStylesheet == false
&& platformUserAgentStylesheetContainers.isEmpty()) {
return StyleMap.EMPTY_MAP;
}
final String cname = node.getTypeSelector();
final String id = node.getId();
final List styleClasses = node.getStyleClass();
if (key == null) {
key = new Key();
}
key.className = cname;
key.id = id;
for(int n=0, nMax=styleClasses.size(); n cacheMap = cacheContainer.getCacheMap(parentStylesheets,regionUserAgentStylesheet);
Cache cache = cacheMap.get(key);
if (cache != null) {
// key will be reused, so clear the styleClasses for next use
key.styleClasses.clear();
} else {
// If the cache is null, then we need to create a new Cache and
// add it to the cache map
// Construct the list of Selectors that could possibly apply
final List selectorData = new ArrayList<>();
// User agent stylesheets have lowest precedence and go first
if (hasSubSceneUserAgentStylesheet || hasSceneUserAgentStylesheet) {
// if has both, use SubScene
final String uaFileName = hasSubSceneUserAgentStylesheet ?
subScene.getUserAgentStylesheet().trim() :
scene.getUserAgentStylesheet().trim();
StylesheetContainer container = null;
for (int n=0, nMax=userAgentStylesheetContainers.size(); n matchingRules =
container.selectorPartitioning.match(id, cname, key.styleClasses);
selectorData.addAll(matchingRules);
}
} else if (platformUserAgentStylesheetContainers.isEmpty() == false) {
for(int n=0, nMax= platformUserAgentStylesheetContainers.size(); n matchingRules =
container.selectorPartitioning.match(id, cname, key.styleClasses);
selectorData.addAll(matchingRules);
}
}
}
if (hasRegionUserAgentStylesheet) {
// Unfortunate duplication of code from previous block. No time to refactor.
StylesheetContainer container = null;
for (int n=0, nMax=userAgentStylesheetContainers.size(); n matchingRules =
container.selectorPartitioning.match(id, cname, key.styleClasses);
selectorData.addAll(matchingRules);
}
}
// Scene stylesheets come next since declarations from
// parent stylesheets should take precedence.
if (sceneStylesheets.isEmpty() == false) {
for(int n=0, nMax=sceneStylesheets.size(); n matchingRules =
container.selectorPartitioning.match(id, cname, key.styleClasses);
selectorData.addAll(matchingRules);
}
}
}
// lastly, parent stylesheets
if (hasParentStylesheets) {
final int nMax = parentStylesheets == null ? 0 : parentStylesheets.size();
for(int n=0; n matchingRules =
container.selectorPartitioning.match(id, cname, key.styleClasses);
selectorData.addAll(matchingRules);
}
}
}
// create a new Cache from these selectors.
cache = new Cache(selectorData);
cacheMap.put(key, cache);
// cause a new Key to be created the next time this method is called
key = null;
}
//
// Create a style helper for this node from the styles that match.
//
StyleMap smap = cache.getStyleMap(cacheContainer, node, triggerStates, hasInlineStyles);
return smap;
}
}
////////////////////////////////////////////////////////////////////////////
//
// CssError reporting
//
////////////////////////////////////////////////////////////////////////////
private static ObservableList errors = null;
/**
* Errors that may have occurred during css processing.
* This list is null until errorsProperty() is called.
*
* NOTE: this is not thread-safe, and cannot readily be made so given the
* nature of the API.
* Currently it is only used by SceneBuilder. If a public API is ever
* needed, then a new API should be designed.
*
* @return
*/
public static ObservableList errorsProperty() {
if (errors == null) {
errors = FXCollections.observableArrayList();
}
return errors;
}
/**
* Errors that may have occurred during css processing.
* This list is null until errorsProperty() is called and is used
* internally to figure out whether or not anyone is interested in
* receiving CssError.
* Not meant for general use - call errorsProperty() instead.
* @return
*/
public static ObservableList getErrors() {
return errors;
}
////////////////////////////////////////////////////////////////////////////
//
// Classes and routines for mapping styles to a Node
//
////////////////////////////////////////////////////////////////////////////
private static List cacheMapKey;
// Each Scene has its own cache
// package for testing
static class CacheContainer {
private Map getStyleCache() {
if (styleCache == null) styleCache = new HashMap<>();
return styleCache;
}
private Map getCacheMap(List parentStylesheets, String regionUserAgentStylesheet) {
if (cacheMap == null) {
cacheMap = new HashMap<>();
}
synchronized (styleLock) {
if ((parentStylesheets == null || parentStylesheets.isEmpty()) &&
(regionUserAgentStylesheet == null || regionUserAgentStylesheet.isEmpty())) {
Map cmap = cacheMap.get(null);
if (cmap == null) {
cmap = new HashMap<>();
cacheMap.put(null, cmap);
}
return cmap;
} else {
final int nMax = parentStylesheets.size();
if (cacheMapKey == null) {
cacheMapKey = new ArrayList<>(nMax);
}
for (int n=0; n cmap = cacheMap.get(cacheMapKey);
if (cmap == null) {
cmap = new HashMap<>();
cacheMap.put(cacheMapKey, cmap);
// create a new cacheMapKey the next time this method is called
cacheMapKey = null;
} else {
// reuse cacheMapKey, but not the data, the next time this method is called
cacheMapKey.clear();
}
return cmap;
}
}
}
private List getStyleMapList() {
if (styleMapList == null) styleMapList = new ArrayList<>();
return styleMapList;
}
private int nextSmapId() {
styleMapId = baseStyleMapId + getStyleMapList().size();
return styleMapId;
}
private void addStyleMap(StyleMap smap) {
getStyleMapList().add(smap);
}
public StyleMap getStyleMap(int smapId) {
final int correctedId = smapId - baseStyleMapId;
if (0 <= correctedId && correctedId < getStyleMapList().size()) {
return getStyleMapList().get(correctedId);
}
return StyleMap.EMPTY_MAP;
}
private void clearCache() {
if (cacheMap != null) cacheMap.clear();
if (styleCache != null) styleCache.clear();
if (styleMapList != null) styleMapList.clear();
baseStyleMapId = styleMapId;
// 7/8ths is totally arbitrary
if (baseStyleMapId > Integer.MAX_VALUE/8*7) {
baseStyleMapId = styleMapId = 0;
}
}
/**
* Get the mapping of property to style from Node.style for this node.
*/
private Selector getInlineStyleSelector(String inlineStyle) {
// If there are no styles for this property then we can just bail
if ((inlineStyle == null) || inlineStyle.trim().isEmpty()) return null;
if (inlineStylesCache != null && inlineStylesCache.containsKey(inlineStyle)) {
// Value of Map entry may be null!
return inlineStylesCache.get(inlineStyle);
}
//
// inlineStyle wasn't in the inlineStylesCache, or inlineStylesCache was null
//
if (inlineStylesCache == null) {
inlineStylesCache = new HashMap<>();
}
final Stylesheet inlineStylesheet =
new CssParser().parse("*{"+inlineStyle+"}");
if (inlineStylesheet != null) {
inlineStylesheet.setOrigin(StyleOrigin.INLINE);
List rules = inlineStylesheet.getRules();
Rule rule = rules != null && !rules.isEmpty() ? rules.get(0) : null;
//List selectors = rule != null ? rule.getUnobservedSelectorList() : null;
List selectors = rule != null ? rule.getSelectors() : null;
Selector selector = selectors != null && !selectors.isEmpty() ? selectors.get(0) : null;
// selector might be null if parser throws some exception
if (selector != null) {
selector.setOrdinal(-1);
inlineStylesCache.put(inlineStyle, selector);
return selector;
}
// if selector is null, fall through
}
// even if selector is null, put it in cache so we don't
// bother with trying to parse it again.
inlineStylesCache.put(inlineStyle, null);
return null;
}
private Map styleCache;
private Map, Map> cacheMap;
private List styleMapList;
/**
* Cache of parsed, inline styles. The key is Node.style.
* The value is the Selector from the inline stylesheet.
*/
private Map inlineStylesCache;
/*
* A simple counter used to generate a unique id for a StyleMap.
* This unique id is used by StyleHelper in figuring out which
* style cache to use.
*/
private int styleMapId = 0;
// When the cache is cleared, styleMapId counting begins here.
// If a StyleHelper calls getStyleMap with an id less than the
// baseStyleMapId, then that StyleHelper is working with an old
// cache and is no longer valid.
private int baseStyleMapId = 0;
}
/**
* Creates and caches maps of styles, reusing them as often as practical.
*/
private static class Cache {
private static class Key {
final long[] key;
final String inlineStyle;
Key(long[] key, String inlineStyle) {
this.key = key;
// let inlineStyle be null if it is empty
this.inlineStyle = (inlineStyle != null && inlineStyle.trim().isEmpty() ? null : inlineStyle);
}
@Override public String toString() {
return Arrays.toString(key) + (inlineStyle != null ? "* {" + inlineStyle + "}" : "");
}
@Override
public int hashCode() {
int hash = 3;
hash = 17 * hash + Arrays.hashCode(this.key);
if (inlineStyle != null) hash = 17 * hash + inlineStyle.hashCode();
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Key other = (Key) obj;
if (inlineStyle == null ? other.inlineStyle != null : !inlineStyle.equals(other.inlineStyle)) {
return false;
}
if (!Arrays.equals(this.key, other.key)) {
return false;
}
return true;
}
}
// this must be initialized to the appropriate possible selectors when
// the helper cache is created by the StylesheetContainer. Note that
// SelectorPartioning sorts the matched selectors by ordinal, so this
// list of selectors will be in the same order in which the selectors
// appear in the stylesheets.
private final List selectors;
private final Map cache;
Cache(List selectors) {
this.selectors = selectors;
this.cache = new HashMap<>();
}
private StyleMap getStyleMap(CacheContainer cacheContainer, Node node, Set[] triggerStates, boolean hasInlineStyle) {
if ((selectors == null || selectors.isEmpty()) && !hasInlineStyle) {
return StyleMap.EMPTY_MAP;
}
final int selectorDataSize = selectors.size();
//
// Since the list of selectors is found by matching only the
// rightmost selector, the set of selectors may larger than those
// selectors that actually match the node. The following loop
// whittles the list down to those selectors that apply.
//
//
// To lookup from the cache, we construct a key from a Long
// where the selectors that match this particular node are
// represented by bits on the long[].
//
long key[] = new long[selectorDataSize/Long.SIZE + 1];
boolean nothingMatched = true;
for (int s = 0; s < selectorDataSize; s++) {
final Selector sel = selectors.get(s);
//
// This particular flavor of applies takes a PseudoClassState[]
// fills in the pseudo-class states from the selectors where
// they apply to a node. This is an expedient to looking the
// applies loopa second time on the matching selectors. This has to
// be done ahead of the cache lookup since not all nodes that
// have the same set of selectors will have the same node hierarchy.
//
// For example, if I have .foo:hover:focused .bar:selected {...}
// and the "bar" node is 4 away from the root and the foo
// node is two away from the root, pseudoclassBits would be
// [selected, 0, hover:focused, 0]
// Note that the states run from leaf to root. This is how
// the code in StyleHelper expects things.
// Note also that, if the selector does not apply, the triggerStates
// is unchanged.
//
if (sel.applies(node, triggerStates, 0)) {
final int index = s / Long.SIZE;
final long mask = key[index] | 1l << s;
key[index] = mask;
nothingMatched = false;
}
}
// nothing matched!
if (nothingMatched && hasInlineStyle == false) {
return StyleMap.EMPTY_MAP;
}
final String inlineStyle = node.getStyle();
final Key keyObj = new Key(key, inlineStyle);
if (cache.containsKey(keyObj)) {
Integer styleMapId = cache.get(keyObj);
final StyleMap styleMap = styleMapId != null
? cacheContainer.getStyleMap(styleMapId.intValue())
: StyleMap.EMPTY_MAP;
return styleMap;
}
final List selectors = new ArrayList<>();
if (hasInlineStyle) {
Selector selector = cacheContainer.getInlineStyleSelector(inlineStyle);
if (selector != null) selectors.add(selector);
}
for (int k = 0; k