1 /* 2 * Copyright (c) 2010, 2015, 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; 27 28 import javafx.css.StyleConverter.StringStore; 29 30 import javafx.collections.ListChangeListener.Change; 31 import javafx.collections.ObservableList; 32 33 import com.sun.javafx.collections.TrackableObservableList; 34 import com.sun.javafx.css.FontFaceImpl; 35 36 import java.io.BufferedInputStream; 37 import java.io.ByteArrayOutputStream; 38 import java.io.DataInputStream; 39 import java.io.DataOutputStream; 40 import java.io.File; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.net.URI; 45 import java.net.URL; 46 import java.util.ArrayList; 47 import java.util.List; 48 49 /** 50 * A stylesheet which can apply properties to a tree of objects. A stylesheet 51 * is a collection of zero or more {@link Rule Rules}, each of which is applied 52 * to each object in the tree. Typically the selector will examine the object to 53 * determine whether or not it is applicable, and if so it will apply certain 54 * property values to the object. 55 * 56 * @since 9 57 */ 58 public class Stylesheet { 59 60 /** 61 * Version number of binary CSS format. The value is incremented whenever the format of the 62 * binary stream changes. This number does not correlate with JavaFX versions. 63 * Version 5: persist @font-face 64 * Version 6: converter classes moved to public package 65 */ 66 final static int BINARY_CSS_VERSION = 6; 67 68 private final String url; 69 /** The URL from which the stylesheet was loaded. 70 * @return The URL from which the stylesheet was loaded, or null if 71 * the stylesheet was created from an inline style. 72 */ 73 public String getUrl() { 74 return url; 75 } 76 77 /** 78 * True if this style came from user stylesheet, we need to know this so 79 * that we can make user important styles have higher priority than 80 * author styles 81 */ 82 private StyleOrigin origin = StyleOrigin.AUTHOR; 83 public StyleOrigin getOrigin() { 84 return origin; 85 } 86 public void setOrigin(StyleOrigin origin) { 87 this.origin = origin; 88 } 89 90 /** All the rules contained in the stylesheet in the order they are in the file */ 91 private final ObservableList<Rule> rules = new TrackableObservableList<Rule>() { 92 93 @Override protected void onChanged(Change<Rule> c) { 94 c.reset(); 95 while (c.next()) { 96 if (c.wasAdded()) { 97 for(Rule rule : c.getAddedSubList()) { 98 rule.setStylesheet(Stylesheet.this); 99 } 100 } else if (c.wasRemoved()) { 101 for (Rule rule : c.getRemoved()) { 102 if (rule.getStylesheet() == Stylesheet.this) rule.setStylesheet(null); 103 } 104 } 105 } 106 } 107 }; 108 109 /** List of all font faces */ 110 private final List<FontFace> fontFaces = new ArrayList<FontFace>(); 111 112 /** 113 * Constructs a stylesheet with the base URI defaulting to the root 114 * path of the application. 115 */ 116 Stylesheet() { 117 118 // ClassLoader cl = Thread.currentThread().getContextClassLoader(); 119 // this.url = (cl != null) ? cl.getResource("") : null; 120 // 121 // RT-17344 122 // The above code is unreliable. The getResource call is intended 123 // to return the root path of the Application instance, but it sometimes 124 // returns null. Here, we'll set url to null and then when a url is 125 // resolved, the url path can be used in the getResource call. For 126 // example, if the css is -fx-image: url("images/duke.png"), we can 127 // do cl.getResouce("images/duke.png") in URLConverter 128 // 129 130 this(null); 131 } 132 133 /** 134 * Constructs a Stylesheet using the given URL as the base URI. The 135 * parameter may not be null. 136 * @param url 137 */ 138 Stylesheet(String url) { 139 140 this.url = url; 141 142 } 143 144 public List<Rule> getRules() { 145 return rules; 146 } 147 148 public List<FontFace> getFontFaces() { 149 return fontFaces; 150 } 151 152 @Override public boolean equals(Object obj) { 153 if (this == obj) return true; 154 if (obj instanceof Stylesheet) { 155 Stylesheet other = (Stylesheet)obj; 156 157 if (this.url == null && other.url == null) { 158 return true; 159 } else if (this.url == null || other.url == null) { 160 return false; 161 } else { 162 return this.url.equals(other.url); 163 } 164 } 165 return false; 166 } 167 168 @Override public int hashCode() { 169 int hash = 7; 170 hash = 13 * hash + (this.url != null ? this.url.hashCode() : 0); 171 return hash; 172 } 173 174 /** Returns a string representation of this object. */ 175 @Override public String toString() { 176 StringBuilder sbuf = new StringBuilder(); 177 sbuf.append("/* "); 178 if (url != null) sbuf.append(url); 179 if (rules.isEmpty()) { 180 sbuf.append(" */"); 181 } else { 182 sbuf.append(" */\n"); 183 for(int r=0; r<rules.size(); r++) { 184 sbuf.append(rules.get(r)); 185 sbuf.append('\n'); 186 } 187 } 188 return sbuf.toString(); 189 } 190 191 // protected for unit testing 192 final void writeBinary(final DataOutputStream os, final StringStore stringStore) 193 throws IOException 194 { 195 // Note: url is not written since it depends on runtime environment. 196 int index = stringStore.addString(origin.name()); 197 os.writeShort(index); 198 os.writeShort(rules.size()); 199 for (Rule r : rules) r.writeBinary(os,stringStore); 200 201 // Version 5 adds persistence of FontFace 202 List<FontFace> fontFaceList = getFontFaces(); 203 int nFontFaces = fontFaceList != null ? fontFaceList.size() : 0; 204 os.writeShort(nFontFaces); 205 206 for(int n=0; n<nFontFaces; n++) { 207 FontFace fontFace = fontFaceList.get(n); 208 if (fontFace instanceof FontFaceImpl) { 209 ((FontFaceImpl)fontFace).writeBinary(os, stringStore); 210 } 211 } 212 } 213 214 // protected for unit testing 215 final void readBinary(int bssVersion, DataInputStream is, String[] strings) 216 throws IOException 217 { 218 this.stringStore = strings; 219 final int index = is.readShort(); 220 this.setOrigin(StyleOrigin.valueOf(strings[index])); 221 final int nRules = is.readShort(); 222 List<Rule> persistedRules = new ArrayList<Rule>(nRules); 223 for (int n=0; n<nRules; n++) { 224 persistedRules.add(Rule.readBinary(bssVersion,is,strings)); 225 } 226 this.rules.addAll(persistedRules); 227 228 if (bssVersion >= 5) { 229 List<FontFace> fontFaceList = this.getFontFaces(); 230 int nFontFaces = is.readShort(); 231 for (int n=0; n<nFontFaces; n++) { 232 FontFace fontFace = FontFaceImpl.readBinary(bssVersion, is, strings); 233 fontFaceList.add(fontFace); 234 } 235 } 236 } 237 238 private String[] stringStore; 239 final String[] getStringStore() { return stringStore; } 240 241 /** Load a binary stylesheet file from a input stream */ 242 public static Stylesheet loadBinary(URL url) throws IOException { 243 244 if (url == null) return null; 245 246 Stylesheet stylesheet = null; 247 248 try (DataInputStream dataInputStream = 249 new DataInputStream(new BufferedInputStream(url.openStream(), 40 * 1024))) { 250 251 // read file version 252 final int bssVersion = dataInputStream.readShort(); 253 if (bssVersion > Stylesheet.BINARY_CSS_VERSION) { 254 throw new IOException(url.toString() + " wrong binary CSS version: " 255 + bssVersion + ". Expected version less than or equal to" + 256 Stylesheet.BINARY_CSS_VERSION); 257 } 258 // read strings 259 final String[] strings = StringStore.readBinary(dataInputStream); 260 // read binary data 261 stylesheet = new Stylesheet(url.toExternalForm()); 262 263 try { 264 265 dataInputStream.mark(Integer.MAX_VALUE); 266 stylesheet.readBinary(bssVersion, dataInputStream, strings); 267 268 } catch (Exception e) { 269 270 stylesheet = new Stylesheet(url.toExternalForm()); 271 272 dataInputStream.reset(); 273 274 if (bssVersion == 2) { 275 // RT-31022 276 stylesheet.readBinary(3, dataInputStream, strings); 277 } else { 278 stylesheet.readBinary(Stylesheet.BINARY_CSS_VERSION, dataInputStream, strings); 279 } 280 } 281 282 } catch (FileNotFoundException fnfe) { 283 // This comes from url.openStream() and is expected. 284 // It just means that the .bss file doesn't exist. 285 } 286 287 // return stylesheet 288 return stylesheet; 289 } 290 291 /** 292 * Convert the .css file referenced by urlIn to binary format and write to urlOut. 293 * @param source is the JavaFX .css file to convert 294 * @param destination is the file to which the binary conversion is written 295 * @throws IOException 296 * @throws IllegalArgumentException if either parameter is null, if source and destination are the same, 297 * if source cannot be read, or if destination cannot be written. 298 */ 299 public static void convertToBinary(File source, File destination) throws IOException { 300 301 if (source == null || destination == null) { 302 throw new IllegalArgumentException("parameters may not be null"); 303 } 304 305 if (source.getAbsolutePath().equals(destination.getAbsolutePath())) { 306 throw new IllegalArgumentException("source and destination may not be the same"); 307 } 308 309 if (source.canRead() == false) { 310 throw new IllegalArgumentException("cannot read source file"); 311 } 312 313 if (destination.exists() ? (destination.canWrite() == false) : (destination.createNewFile() == false)) { 314 throw new IllegalArgumentException("cannot write destination file"); 315 } 316 317 URI sourceURI = source.toURI(); 318 Stylesheet stylesheet = new CssParser().parse(sourceURI.toURL()); 319 320 // first write all the css binary data into the buffer and collect strings on way 321 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 322 DataOutputStream dos = new DataOutputStream(baos); 323 StringStore stringStore = new StringStore(); 324 stylesheet.writeBinary(dos, stringStore); 325 dos.flush(); 326 dos.close(); 327 328 FileOutputStream fos = new FileOutputStream(destination); 329 DataOutputStream os = new DataOutputStream(fos); 330 331 // write file version 332 os.writeShort(BINARY_CSS_VERSION); 333 334 // write strings 335 stringStore.writeBinary(os); 336 337 // write binary css 338 os.write(baos.toByteArray()); 339 os.flush(); 340 os.close(); 341 } 342 343 // Add the rules from the other stylesheet to this one 344 void importStylesheet(Stylesheet importedStylesheet) { 345 if (importedStylesheet == null) return; 346 347 List<Rule> rulesToImport = importedStylesheet.getRules(); 348 if (rulesToImport == null || rulesToImport.isEmpty()) return; 349 350 List<Rule> importedRules = new ArrayList<>(rulesToImport.size()); 351 for (Rule rule : rulesToImport) { 352 List<Selector> selectors = rule.getSelectors(); 353 List<Declaration> declarations = rule.getUnobservedDeclarationList(); 354 importedRules.add(new Rule(selectors, declarations)); 355 } 356 357 rules.addAll(importedRules); 358 } 359 }