1 /* 2 * Copyright (c) 2011, 2017, 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 javafx.css.converter; 27 28 import javafx.application.Application; 29 import javafx.css.ParsedValue; 30 import javafx.css.StyleConverter; 31 import javafx.scene.text.Font; 32 import sun.util.logging.PlatformLogger; 33 34 import java.net.MalformedURLException; 35 import java.net.URI; 36 import java.net.URISyntaxException; 37 import java.net.URL; 38 import java.security.AccessController; 39 import java.security.CodeSource; 40 import java.security.PrivilegedActionException; 41 import java.security.PrivilegedExceptionAction; 42 import java.security.ProtectionDomain; 43 44 /** 45 * Converts a parsed value representing URL to a URL string that is 46 * resolved relative to the location of the stylesheet. 47 * The input value is in the form: {@code url("<path>")}. 48 * 49 * @since 9 50 */ 51 public final class URLConverter extends StyleConverter<ParsedValue[], String> { 52 53 // lazy, thread-safe instatiation 54 private static class Holder { 55 static final URLConverter INSTANCE = new URLConverter(); 56 static final SequenceConverter SEQUENCE_INSTANCE = new SequenceConverter(); 57 } 58 59 public static StyleConverter<ParsedValue[], String> getInstance() { 60 return Holder.INSTANCE; 61 } 62 63 private URLConverter() { 64 super(); 65 } 66 67 @Override 68 public String convert(ParsedValue<ParsedValue[], String> value, Font font) { 69 70 String url = null; 71 72 ParsedValue[] values = value.getValue(); 73 74 String resource = values.length > 0 ? StringConverter.getInstance().convert(values[0], font) : null; 75 76 if (resource != null && resource.trim().isEmpty() == false) { 77 78 if (resource.startsWith("url(")) { 79 resource = com.sun.javafx.util.Utils.stripQuotes(resource.substring(4, resource.length() - 1)); 80 } else { 81 resource = com.sun.javafx.util.Utils.stripQuotes(resource); 82 } 83 84 String stylesheetURL = values.length > 1 && values[1] != null ? (String)values[1].getValue() : null; 85 URL resolvedURL = resolve(stylesheetURL, resource); 86 87 if (resolvedURL != null) url = resolvedURL.toExternalForm(); 88 } 89 90 return url; 91 } 92 93 // package for testing 94 URL resolve(String stylesheetUrl, String resource) { 95 96 97 final String resourcePath = (resource != null) ? resource.trim() : null; 98 if (resourcePath == null || resourcePath.isEmpty()) return null; 99 100 try { 101 102 // Note: the same code (pretty much) also appears in StyleManager 103 104 // if stylesheetUri is null, then we're dealing with an in-line style. 105 // If there is no scheme part, then the url is interpreted as being relative to the application's class-loader. 106 URI resourceUri = new URI(resourcePath); 107 108 if (resourceUri.isAbsolute()) { 109 return resourceUri.toURL(); 110 } 111 112 URL rtJarUrl = resolveRuntimeImport(resourceUri); 113 if (rtJarUrl != null) { 114 return rtJarUrl; 115 } 116 117 final String path = resourceUri.getPath(); 118 if (path.startsWith("/")) { 119 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 120 // FIXME: JIGSAW -- use Class.getResource if resource is in a module 121 return contextClassLoader.getResource(path.substring(1)); 122 } 123 124 final String stylesheetPath = (stylesheetUrl != null) ? stylesheetUrl.trim() : null; 125 126 if (stylesheetPath != null && stylesheetPath.isEmpty() == false) { 127 128 URI stylesheetUri = new URI(stylesheetPath); 129 130 if (stylesheetUri.isOpaque() == false) { 131 132 URI resolved = stylesheetUri.resolve(resourceUri); 133 return resolved.toURL(); 134 135 } else { 136 137 // stylesheet URI is something like jar:file: 138 URL url = stylesheetUri.toURL(); 139 return new URL(url, resourceUri.getPath()); 140 } 141 } 142 143 144 // URL doesn't have scheme or stylesheetUrl is null 145 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 146 // FIXME: JIGSAW -- use Class.getResource if resource is in a module 147 return contextClassLoader.getResource(path); 148 149 150 } catch (final MalformedURLException|URISyntaxException e) { 151 PlatformLogger cssLogger = com.sun.javafx.util.Logging.getCSSLogger(); 152 if (cssLogger.isLoggable(PlatformLogger.Level.WARNING)) { 153 cssLogger.warning(e.getLocalizedMessage()); 154 } 155 156 return null; 157 } 158 159 } 160 161 162 // 163 // Resolve a path from an @import that implies jfxrt.jar, 164 // e.g., @import "com/sun/javafx/scene/control/skin/modena/modena.css". 165 // 166 // See also StyleSheet#loadStylesheet(String) 167 // 168 private URL resolveRuntimeImport(final URI resourceUri) { 169 170 // FIXME: JIGSAW -- this method needs to be rewritten for Jigsaw. 171 // There is no jfxrt.jar any more, and resource encapsulation will 172 // prevent it from being resolved anyway. 173 174 final String path = resourceUri.getPath(); 175 final String resourcePath = path.startsWith("/") ? path.substring(1) : path; 176 177 if ((resourcePath.startsWith("com/sun/javafx/scene/control/skin/modena/") || 178 resourcePath.startsWith("com/sun/javafx/scene/control/skin/caspian/")) && 179 (resourcePath.endsWith(".css") || resourcePath.endsWith(".bss"))) { 180 181 System.err.println("WARNING: resolveRuntimeImport cannot resolve: " + resourcePath); 182 183 final SecurityManager sm = System.getSecurityManager(); 184 if (sm == null) { 185 // If the SecurityManager is not null, then just look up the resource on the class-path. 186 // If there is a SecurityManager, the URLClassPath getResource call will return null, 187 // so fall through and create a URL from the code-source URI 188 final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); 189 // FIXME: JIGSAW -- use Class.getResource if resource is in a module 190 final URL resolved = contextClassLoader.getResource(resourcePath); 191 return resolved; 192 } 193 194 // check whether the path is file from our runtime jar 195 try { 196 final URL rtJarURL = AccessController.doPrivileged((PrivilegedExceptionAction<URL>) () -> { 197 // getProtectionDomain either throws a SecurityException or returns a non-null value 198 final ProtectionDomain protectionDomain = Application.class.getProtectionDomain(); 199 // If we're running with a SecurityManager, then the ProtectionDomain will have a CodeSource 200 final CodeSource codeSource = protectionDomain.getCodeSource(); 201 // The CodeSource location will be our runtime jar 202 return codeSource.getLocation(); 203 }); 204 205 final URI rtJarURI = rtJarURL.toURI(); 206 207 String scheme = rtJarURI.getScheme(); 208 String rtJarPath = rtJarURI.getPath(); 209 210 // 211 // Just because we're running with a SecurityManager doesn't mean the jfxrt jar path is 212 // a jar: URL. But the code in StyleManager wants it to be. So if we have 213 // file:/blah/lib/jfxrt.jar make it jar:file:/blah/lib/jfxrt.jar!/ 214 // 215 // If the path doesn't end with .jar, then we are just dealing with a normal file: path 216 // 217 if ("file".equals(scheme) && rtJarPath.endsWith(".jar")) { 218 if ("file".equals(scheme)) { 219 scheme = "jar:file"; 220 rtJarPath = rtJarPath.concat("!/"); 221 } 222 } 223 rtJarPath = rtJarPath.concat(resourcePath); 224 225 final String rtJarUserInfo = rtJarURI.getUserInfo(); 226 final String rtJarHost = rtJarURI.getHost(); 227 final int rtJarPort = rtJarURI.getPort(); 228 229 // 230 // Put together a new URI from the pieces of rtJarURI. We cannot use resolve here since 231 // the scheme and path may have been munged. 232 // 233 URI resolved = new URI(scheme, rtJarUserInfo, rtJarHost, rtJarPort, rtJarPath, null, null); 234 return resolved.toURL(); 235 236 } catch (URISyntaxException | MalformedURLException | PrivilegedActionException ignored) { 237 // Allow this method to return null so the caller will try to further resolve the path. 238 // If nothing else, an error message will result when the converted URL is consumed. 239 } 240 } 241 return null; 242 } 243 244 @Override 245 public String toString() { 246 return "URLType"; 247 } 248 249 public static final class SequenceConverter extends StyleConverter<ParsedValue<ParsedValue[], String>[], String[]> { 250 251 public static SequenceConverter getInstance() { 252 return Holder.SEQUENCE_INSTANCE; 253 } 254 255 private SequenceConverter() { 256 super(); 257 } 258 259 @Override 260 public String[] convert(ParsedValue<ParsedValue<ParsedValue[], String>[], String[]> value, Font font) { 261 ParsedValue<ParsedValue[], String>[] layers = value.getValue(); 262 String[] urls = new String[layers.length]; 263 for (int layer = 0; layer < layers.length; layer++) { 264 urls[layer] = URLConverter.getInstance().convert(layers[layer], font); 265 } 266 return urls; 267 } 268 269 @Override 270 public String toString() { 271 return "URLSeqType"; 272 } 273 } 274 275 }