1 /* 2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.javafx.css; 27 28 import com.sun.javafx.collections.TrackableObservableList; 29 import com.sun.javafx.css.parser.CSSParser; 30 import javafx.collections.ListChangeListener.Change; 31 import javafx.collections.ObservableList; 32 import javafx.css.StyleOrigin; 33 34 import java.io.BufferedInputStream; 35 import java.io.ByteArrayOutputStream; 36 import java.io.DataInputStream; 37 import java.io.DataOutputStream; 38 import java.io.File; 39 import java.io.FileNotFoundException; 40 import java.io.FileOutputStream; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.io.OutputStream; 44 import java.net.URI; 45 import java.net.URL; 46 import java.net.URLConnection; 47 import java.util.ArrayList; 48 import java.util.List; 49 50 /** 51 * A stylesheet which can apply properties to a tree of objects. A stylesheet 52 * is a collection of zero or more {@link Rule Rules}, each of which is applied 53 * to each object in the tree. Typically the selector will examine the object to 54 * determine whether or not it is applicable, and if so it will apply certain 55 * property values to the object. 56 * <p> 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 */ 65 final static int BINARY_CSS_VERSION = 5; 66 67 private final String url; 68 /** The URL from which the stylesheet was loaded. 69 * @return The URL from which the stylesheet was loaded, or null if 70 * the stylesheet was created from an inline style. 71 */ 72 public String getUrl() { 73 return url; 74 } 75 76 /** 77 * True if this style came from user stylesheet, we need to know this so 78 * that we can make user important styles have higher priority than 79 * author styles 80 */ 81 private StyleOrigin origin = StyleOrigin.AUTHOR; 82 public StyleOrigin getOrigin() { 83 return origin; 84 } 85 public void setOrigin(StyleOrigin origin) { 86 this.origin = origin; 87 } 88 89 /** All the rules contained in the stylesheet in the order they are in the file */ 90 private final ObservableList<Rule> rules = new TrackableObservableList<Rule>() { 91 92 @Override 93 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 public 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 public 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 fontFace.writeBinary(os, stringStore); 209 } 210 } 211 212 // protected for unit testing 213 final void readBinary(int bssVersion, DataInputStream is, String[] strings) 214 throws IOException 215 { 216 this.stringStore = strings; 217 final int index = is.readShort(); 218 this.setOrigin(StyleOrigin.valueOf(strings[index])); 219 final int nRules = is.readShort(); 220 List<Rule> persistedRules = new ArrayList<Rule>(nRules); 221 for (int n=0; n<nRules; n++) { 222 persistedRules.add(Rule.readBinary(bssVersion,is,strings)); 223 } 224 this.rules.addAll(persistedRules); 225 226 if (bssVersion >= 5) { 227 List<FontFace> fontFaceList = this.getFontFaces(); 228 int nFontFaces = is.readShort(); 229 for (int n=0; n<nFontFaces; n++) { 230 FontFace fontFace = FontFace.readBinary(bssVersion, is, strings); 231 fontFaceList.add(fontFace); 232 } 233 } 234 } 235 236 private String[] stringStore; 237 final String[] getStringStore() { return stringStore; }; 238 239 /** Load a binary stylesheet file from a input stream */ 240 public static Stylesheet loadBinary(URL url) throws IOException { 241 242 if (url == null) return null; 243 244 Stylesheet stylesheet = null; 245 246 try (DataInputStream dataInputStream = 247 new DataInputStream(new BufferedInputStream(url.openStream(), 40 * 1024))) { 248 249 // read file version 250 final int bssVersion = dataInputStream.readShort(); 251 if (bssVersion > Stylesheet.BINARY_CSS_VERSION) { 252 throw new IOException(url.toString() + " wrong binary CSS version: " 253 + bssVersion + ". Expected version less than or equal to" + 254 Stylesheet.BINARY_CSS_VERSION); 255 } 256 // read strings 257 final String[] strings = StringStore.readBinary(dataInputStream); 258 // read binary data 259 stylesheet = new Stylesheet(url.toExternalForm()); 260 261 try { 262 263 dataInputStream.mark(Integer.MAX_VALUE); 264 stylesheet.readBinary(bssVersion, dataInputStream, strings); 265 266 } catch (Exception e) { 267 268 stylesheet = new Stylesheet(url.toExternalForm()); 269 270 dataInputStream.reset(); 271 272 if (bssVersion == 2) { 273 // RT-31022 274 stylesheet.readBinary(3, dataInputStream, strings); 275 } else { 276 stylesheet.readBinary(Stylesheet.BINARY_CSS_VERSION, dataInputStream, strings); 277 } 278 } 279 280 } catch (FileNotFoundException fnfe) { 281 // This comes from url.openStream() and is expected. 282 // It just means that the .bss file doesn't exist. 283 } 284 285 // return stylesheet 286 return stylesheet; 287 } 288 289 /** 290 * Convert the .css file referenced by urlIn to binary format and write to urlOut. 291 * @param source is the JavaFX .css file to convert 292 * @param destination is the file to which the binary conversion is written 293 * @throws IOException 294 * @throws IllegalArgumentException if either parameter is null, if source and destination are the same, 295 * if source cannot be read, or if destination cannot be written. 296 */ 297 public static void convertToBinary(File source, File destination) throws IOException { 298 299 if (source == null || destination == null) { 300 throw new IllegalArgumentException("parameters may not be null"); 301 } 302 303 if (source.getAbsolutePath().equals(destination.getAbsolutePath())) { 304 throw new IllegalArgumentException("source and destination may not be the same"); 305 } 306 307 if (source.canRead() == false) { 308 throw new IllegalArgumentException("cannot read source file"); 309 } 310 311 if (destination.exists() ? (destination.canWrite() == false) : (destination.createNewFile() == false)) { 312 throw new IllegalArgumentException("cannot write destination file"); 313 } 314 315 URI sourceURI = source.toURI(); 316 Stylesheet stylesheet = CSSParser.getInstance().parse(sourceURI.toURL()); 317 318 // first write all the css binary data into the buffer and collect strings on way 319 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 320 DataOutputStream dos = new DataOutputStream(baos); 321 StringStore stringStore = new StringStore(); 322 stylesheet.writeBinary(dos, stringStore); 323 dos.flush(); 324 dos.close(); 325 326 FileOutputStream fos = new FileOutputStream(destination); 327 DataOutputStream os = new DataOutputStream(fos); 328 329 // write file version 330 os.writeShort(BINARY_CSS_VERSION); 331 332 // write strings 333 stringStore.writeBinary(os); 334 335 // write binary css 336 os.write(baos.toByteArray()); 337 os.flush(); 338 os.close(); 339 } 340 341 }