/*
 * Copyright (c) 2011, 2018, 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 javafx.css.converter;

import javafx.application.Application;
import javafx.css.ParsedValue;
import javafx.css.StyleConverter;
import javafx.scene.text.Font;
import com.sun.javafx.logging.PlatformLogger;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;

Converts a parsed value representing URL to a URL string that is resolved relative to the location of the stylesheet. The input value is in the form: url("<path>").
Since:9
/** * Converts a parsed value representing URL to a URL string that is * resolved relative to the location of the stylesheet. * The input value is in the form: {@code url("<path>")}. * * @since 9 */
public final class URLConverter extends StyleConverter<ParsedValue[], String> { // lazy, thread-safe instatiation private static class Holder { static final URLConverter INSTANCE = new URLConverter(); static final SequenceConverter SEQUENCE_INSTANCE = new SequenceConverter(); } public static StyleConverter<ParsedValue[], String> getInstance() { return Holder.INSTANCE; } private URLConverter() { super(); } @Override public String convert(ParsedValue<ParsedValue[], String> value, Font font) { String url = null; ParsedValue[] values = value.getValue(); String resource = values.length > 0 ? StringConverter.getInstance().convert(values[0], font) : null; if (resource != null && resource.trim().isEmpty() == false) { if (resource.startsWith("url(")) { resource = com.sun.javafx.util.Utils.stripQuotes(resource.substring(4, resource.length() - 1)); } else { resource = com.sun.javafx.util.Utils.stripQuotes(resource); } String stylesheetURL = values.length > 1 && values[1] != null ? (String)values[1].getValue() : null; URL resolvedURL = resolve(stylesheetURL, resource); if (resolvedURL != null) url = resolvedURL.toExternalForm(); } return url; } // package for testing URL resolve(String stylesheetUrl, String resource) { final String resourcePath = (resource != null) ? resource.trim() : null; if (resourcePath == null || resourcePath.isEmpty()) return null; try { // Note: the same code (pretty much) also appears in StyleManager // if stylesheetUri is null, then we're dealing with an in-line style. // If there is no scheme part, then the url is interpreted as being relative to the application's class-loader. URI resourceUri = new URI(resourcePath); if (resourceUri.isAbsolute()) { return resourceUri.toURL(); } URL rtJarUrl = resolveRuntimeImport(resourceUri); if (rtJarUrl != null) { return rtJarUrl; } final String path = resourceUri.getPath(); if (path.startsWith("/")) { final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); // FIXME: JIGSAW -- use Class.getResource if resource is in a module return contextClassLoader.getResource(path.substring(1)); } final String stylesheetPath = (stylesheetUrl != null) ? stylesheetUrl.trim() : null; if (stylesheetPath != null && stylesheetPath.isEmpty() == false) { URI stylesheetUri = new URI(stylesheetPath); if (stylesheetUri.isOpaque() == false) { URI resolved = stylesheetUri.resolve(resourceUri); return resolved.toURL(); } else { // stylesheet URI is something like jar:file: URL url = stylesheetUri.toURL(); return new URL(url, resourceUri.getPath()); } } // URL doesn't have scheme or stylesheetUrl is null final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); // FIXME: JIGSAW -- use Class.getResource if resource is in a module return contextClassLoader.getResource(path); } catch (final MalformedURLException|URISyntaxException e) { PlatformLogger cssLogger = com.sun.javafx.util.Logging.getCSSLogger(); if (cssLogger.isLoggable(PlatformLogger.Level.WARNING)) { cssLogger.warning(e.getLocalizedMessage()); } return null; } } // // Resolve a path from an @import that implies jfxrt.jar, // e.g., @import "com/sun/javafx/scene/control/skin/modena/modena.css". // // See also StyleSheet#loadStylesheet(String) // private URL resolveRuntimeImport(final URI resourceUri) { // FIXME: JIGSAW -- this method needs to be rewritten for Jigsaw. // There is no jfxrt.jar any more, and resource encapsulation will // prevent it from being resolved anyway. final String path = resourceUri.getPath(); final String resourcePath = path.startsWith("/") ? path.substring(1) : path; if ((resourcePath.startsWith("com/sun/javafx/scene/control/skin/modena/") || resourcePath.startsWith("com/sun/javafx/scene/control/skin/caspian/")) && (resourcePath.endsWith(".css") || resourcePath.endsWith(".bss"))) { System.err.println("WARNING: resolveRuntimeImport cannot resolve: " + resourcePath); final SecurityManager sm = System.getSecurityManager(); if (sm == null) { // If the SecurityManager is not null, then just look up the resource on the class-path. // If there is a SecurityManager, the URLClassPath getResource call will return null, // so fall through and create a URL from the code-source URI final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); // FIXME: JIGSAW -- use Class.getResource if resource is in a module final URL resolved = contextClassLoader.getResource(resourcePath); return resolved; } // check whether the path is file from our runtime jar try { final URL rtJarURL = AccessController.doPrivileged((PrivilegedExceptionAction<URL>) () -> { // getProtectionDomain either throws a SecurityException or returns a non-null value final ProtectionDomain protectionDomain = Application.class.getProtectionDomain(); // If we're running with a SecurityManager, then the ProtectionDomain will have a CodeSource final CodeSource codeSource = protectionDomain.getCodeSource(); // The CodeSource location will be our runtime jar return codeSource.getLocation(); }); final URI rtJarURI = rtJarURL.toURI(); String scheme = rtJarURI.getScheme(); String rtJarPath = rtJarURI.getPath(); // // Just because we're running with a SecurityManager doesn't mean the jfxrt jar path is // a jar: URL. But the code in StyleManager wants it to be. So if we have // file:/blah/lib/jfxrt.jar make it jar:file:/blah/lib/jfxrt.jar!/ // // If the path doesn't end with .jar, then we are just dealing with a normal file: path // if ("file".equals(scheme) && rtJarPath.endsWith(".jar")) { if ("file".equals(scheme)) { scheme = "jar:file"; rtJarPath = rtJarPath.concat("!/"); } } rtJarPath = rtJarPath.concat(resourcePath); final String rtJarUserInfo = rtJarURI.getUserInfo(); final String rtJarHost = rtJarURI.getHost(); final int rtJarPort = rtJarURI.getPort(); // // Put together a new URI from the pieces of rtJarURI. We cannot use resolve here since // the scheme and path may have been munged. // URI resolved = new URI(scheme, rtJarUserInfo, rtJarHost, rtJarPort, rtJarPath, null, null); return resolved.toURL(); } catch (URISyntaxException | MalformedURLException | PrivilegedActionException ignored) { // Allow this method to return null so the caller will try to further resolve the path. // If nothing else, an error message will result when the converted URL is consumed. } } return null; } @Override public String toString() { return "URLType"; } public static final class SequenceConverter extends StyleConverter<ParsedValue<ParsedValue[], String>[], String[]> { public static SequenceConverter getInstance() { return Holder.SEQUENCE_INSTANCE; } private SequenceConverter() { super(); } @Override public String[] convert(ParsedValue<ParsedValue<ParsedValue[], String>[], String[]> value, Font font) { ParsedValue<ParsedValue[], String>[] layers = value.getValue(); String[] urls = new String[layers.length]; for (int layer = 0; layer < layers.length; layer++) { urls[layer] = URLConverter.getInstance().convert(layers[layer], font); } return urls; } @Override public String toString() { return "URLSeqType"; } } }