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 }