       1 /*
       2  * Copyright (c) 2010, 2012, 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.logging.PlatformLogger;
      29 import java.lang.ref.Reference;
      30 import java.util.*;
      31 import javafx.scene.Scene;
      32
      33 import javafx.collections.ListChangeListener.Change;
      34 import javafx.scene.Node;
      35 import javafx.scene.Parent;
      36
      37 /**
      38  * Contains the stylesheet state for a single parent.
      39  */
      40 final class ParentStyleManager extends StyleManager<Parent> {
      41
      42     /**
      43      *
      44      * @param scene
      45      */
      46     public ParentStyleManager(Parent parent, StyleManager ancestorStyleManager) {
      47
      48         super(parent);
      49         this.ancestorStyleManager = ancestorStyleManager;
      50         this.key = new ParentKey();
      51         this.indices = getIndicesOfParentsWithStylesheets(parent, 0);
      52         updateStylesheets();
      53     }
      54
      55     /** Either the Scene styleManager or the styleManager of a Parent */
      56     private final StyleManager ancestorStyleManager;
      57
      58     /**
      59      * This instance is reused for each lookup in the cache so as to avoid
      60      * creating temporary objects as much as reasonable.
      61      */
      62     private final ParentKey key;
      63
      64     private final int[] indices;
      65
      66     /**
      67      * Another map from String => Stylesheet for a Parent. If a stylesheet for the
      68      * given URL has already been loaded then we'll simply reuse the stylesheet
      69      * rather than loading a duplicate.
      70      */
      71     private static final Map<String,StylesheetContainer<Parent>> parentStylesheetMap
      72         = new HashMap<String,StylesheetContainer<Parent>>();
      73
      74     ////////////////////////////////////////////////////////////////////////////
      75     //
      76     // abstract method implementation
      77     //
      78     ////////////////////////////////////////////////////////////////////////////
      79
      80     @Override
      81     public void updateStylesheets() {
      82         // The list of referenced stylesheets is about to be recalculated,
      83         // so clear the current list. The style maps are no longer valid, so
      84         // annihilate the cache
      85         referencedStylesheets.clear();
      86         clearCache();
      87
      88         if (defaultUserAgentStylesheet != null) {
      89             referencedStylesheets.add(defaultUserAgentStylesheet);
      90         }
      91
      92         if (userAgentStylesheetMap.isEmpty() == false) {
      93             // TODO: This isn't right. They should be in the same order that
      94             // they were first added.
      95             referencedStylesheets.addAll(userAgentStylesheetMap.values());
      96         }
      97
      98         referencedStylesheets.addAll(ancestorStyleManager.referencedStylesheets);
      99
     100         // RT-20643
     101         CssError.setCurrentScene(owner.getScene());
     102
     103         // create the stylesheets, one per URL supplied
     104         List<String> stylesheetURLs = owner.getStylesheets();
     105
     106         for (int i = 0; i < stylesheetURLs.size(); i++) {
     107
     108             String url = stylesheetURLs.get(i);
     109             url = url != null ? url.trim() : null;
     110             if (url == null || url.isEmpty()) continue;
     111
     112             Stylesheet stylesheet = null;
     113             try {
     114
     115                 stylesheet = loadParentStylesheet(url);
     116                 if (stylesheet != null) {
     117                     referencedStylesheets.add(stylesheet);
     118                 }
     119
     120             } catch (Exception e) {
     121                 // If an exception occurred while loading one of the stylesheets
     122                 // then we will simply print warning into system.err and skip the
     123                 // stylesheet, allowing other stylesheets to attempt to load
     124                 if (LOGGER.isLoggable(PlatformLogger.WARNING)) {
     125                     LOGGER.warning("Cannot add stylesheet. %s\n", e.getLocalizedMessage());
     126                 }
     127
     128             }
     129         }
     130
     131         // RT-20643
     132         CssError.setCurrentScene(null);
     133
     134     }
     135
     136     @Override
     137     protected Key getKey(Node node) {
     138
     139         // Populate our helper key with the class name, id, and style class
     140         // of the node and lookup the associated Cache in the cacheMap
     141         key.className = node.getClass().getName();
     142         key.id = node.getId();
     143         key.styleClass = node.getStyleClass();
     144
     145         key.indices = indices;
     146
     147         return key;
     148
     149     }
     150
     151     ////////////////////////////////////////////////////////////////////////////
     152     //
     153     // Parent stylesheet handling
     154     //
     155     ////////////////////////////////////////////////////////////////////////////
     156
     157     /**
     158      * called from Parent's stylesheets property's onChanged method
     159      */
     160     @Override
     161     public void stylesheetsChanged(Change<String> c) {
     162
     163         final Scene scene = owner.getScene();
     164         if (scene == null) return;
     165
     166         boolean wasRemoved = false;
     167
     168         while (c.next()) {
     169
     170             // RT-22565
     171             // If the String was removed, remove it from parentStylesheetMap
     172             // and remove all Caches that got styles from the stylesheet.
     173             // The stylesheet may still be referenced by some other parent,
     174             // in which case the stylesheet will be reparsed and added
     175             // back to parentStylesheetMap when the StyleHelper is recreated.
     176             // This incurs a some overhead since the stylesheet has to be
     177             // reparsed, but this keeps the other Parents that use this
     178             // in sync. For example, SceneBuilder will remove and then add
     179             // a stylesheet after it has been edited and all Parents that use
     180             // that stylesheet should get the new values.
     181             //
     182             if (c.wasRemoved()) {
     183
     184                 final List<String> list = c.getRemoved();
     185                 int nMax = list != null ? list.size() : 0;
     186                 for (int n=0; n<nMax; n++) {
     187                     final String fname = list.get(n);
     188
     189                     // remove this parent from the container and clear
     190                     // all caches used by this stylesheet
     191                     StylesheetContainer<Parent> psc = parentStylesheetMap.remove(fname);
     192                     if (psc != null) {
     193                         clearParentCache(psc);
     194                     }
     195                 }
     196             }
     197
     198             // RT-22565: only wasRemoved matters. If the logic was applied to
     199             // wasAdded, then the stylesheet would be reparsed each time a
     200             // a Parent added it.
     201
     202         }
     203         // parent uses change also
     204         c.reset();
     205     }
     206
     207     // RT-22565: Called from parentStylesheetsChanged to clear the cache entries
     208     // for parents that use the same stylesheet
     209     private void clearParentCache(StylesheetContainer<Parent> psc) {
     210
     211         final List<Reference<Parent>> parentList = psc.users.list;
     212         final List<Reference<StyleManager.Key>>    keyList    = psc.keys.list;
     213         for (int n=parentList.size()-1; 0<=n; --n) {
     214
     215             final Reference<Parent> ref = parentList.get(n);
     216             final Parent parent = ref.get();
     217             if (parent == null) continue;
     218
     219             Reference<StyleManager> styleManagerRef = managerReferenceMap.get(parent);
     220             if (styleManagerRef == null) continue;
     221
     222             StyleManager styleManager = styleManagerRef.get();
     223             if (styleManager == null) continue;
     224
     225             ((ParentStyleManager)styleManager).clearParentCache(keyList);
     226
     227             // tell parent it needs to reapply css
     228             parent.impl_reapplyCSS();
     229         }
     230     }
     231
     232     // RT-22565: Called from clearParentCache to clear the cache entries.
     233     private void clearParentCache(List<Reference<StyleManager.Key>> keyList) {
     234
     235         if (cacheMap.isEmpty()) return;
     236
     237         for (int n=keyList.size()-1; 0<=n; --n) {
     238
     239             final Reference<StyleManager.Key> ref = keyList.get(n);
     240             final StyleManager.Key key = ref.get();
     241             if (key == null) continue;
     242
     243             final StyleManager.Cache cache = cacheMap.remove(key);
     244         }
     245     }
     246
     247
     248     //
     249     // This int array represents the set of Parent's that have stylesheets.
     250     // The indice is that of the Parent's hash code or -1 if
     251     // the Parent has no stylesheet. See also the comments in the Key class.
     252     // Note that the array is ordered from root at index zero to the
     253     // leaf Node's parent at index length-1.
     254     //
     255
     256     private static int[] getIndicesOfParentsWithStylesheets(Parent parent, int count) {
     257         if (parent == null) {
     258             return new int[count];
     259         }
     260         final int[] indices = getIndicesOfParentsWithStylesheets(parent.getParent(), ++count);
     261         // logic elsewhere depends on indices of parents
     262         // with no stylesheets being -1
     263         if (parent.getStylesheets().isEmpty() == false) {
     264             indices[indices.length - count] = parent.hashCode();
     265         } else {
     266             indices[indices.length - count] = -1;
     267         }
     268         return indices;
     269     }
     270
     271     //
     272     // recurse so that stylesheets of Parents closest to the root are
     273     // added to the list first. The ensures that declarations for
     274     // stylesheets further down the tree (closer to the leaf) have
     275     // a higer ordinal in the cascade.
     276     //
     277     private Stylesheet loadParentStylesheet(String url) {
     278
     279         // RT-20643
     280         CssError.setCurrentScene(owner.getScene());
     281
     282         Stylesheet stylesheet = null;
     283
     284         StyleManager.StylesheetContainer<Parent> container = null;
     285         if (parentStylesheetMap.containsKey(url)) {
     286             container = parentStylesheetMap.get(url);
     287             // RT-22565: remember that this parent uses this stylesheet.
     288             // Later, if the cache is cleared, the parent is told to
     289             // reapply css.
     290             container.users.add(owner);
     291             stylesheet = container.stylesheet;
     292         } else {
     293             stylesheet = loadStylesheet(url);
     294             // stylesheet may be null which would mean that some IOException
     295             // was thrown while trying to load it. Add it to the
     296             // parentStylesheetMap anyway as this will prevent further
     297             // attempts to parse the file
     298             container =
     299                     new StyleManager.StylesheetContainer<Parent>(url, stylesheet);
     300             // RT-22565: remember that this parent uses this stylesheet.
     301             // Later, if the cache is cleared, the parent is told to
     302             // reapply css.
     303             container.users.add(owner);
     304             parentStylesheetMap.put(url, container);
     305         }
     306
     307         // RT-20643
     308         CssError.setCurrentScene(null);
     309
     310         return stylesheet;
     311     }
     312
     313
     314     /**
     315      * The key used in the cacheMap of the StylesheetContainer
     316      */
     317     private static class ParentKey extends StyleManager.Key {
     318
     319         ParentKey() {
     320             super();
     321             indices = null;
     322         }
     323
     324         protected Key dup() {
     325
     326             ParentKey key = new ParentKey();
     327             key.className  = className;
     328             key.id = id;
     329             final int nElements = styleClass.size();
     330             key.styleClass = new ArrayList<String>(nElements);
     331             for (int n = 0; n < nElements; n++) {
     332                 key.styleClass.add(styleClass.get(n));
     333             }
     334             key.indices = indices;
     335
     336             return key;
     337         }
     338
     339         // this will be initialized if a Parent has a stylesheet and will
     340         // hold the indices of those Parents with stylesheets (the Parent's
     341         // hash code is used). If the parent does not have a stylesheet, the
     342         // indice will be -1. When finding the equals Key in the cache lookup,
     343         // we only need to check up to the last parent that has a stylesheet.
     344         // In other words, if one node is at level n and nearest parent with
     345         // a stylesheet is at level m, then the indices match provided
     346         // indices[x] == other.indices[x] for 0 <= x <= m and all other
     347         // indices are -1. Thus, [1, -1, 2] and [1, -1, 2, -1, -1] are equivalent
     348         // whereas [1, -1, 2] and [1, -1, 2, -1, 3] are not.
     349         private int[] indices;
     350
     351         public int[] getIndices() {
     352             return indices;
     353         }
     354
     355         public void setIndices(int[] indices) {
     356             this.indices = indices;
     357         }
     358
     359         @Override
     360         public boolean equals(Object o) {
     361             if (o instanceof ParentKey && super.equals(o)) {
     362                 ParentKey other = (ParentKey)o;
     363                 if(indices != null && other.indices != null) {
     364                     boolean eq = true;
     365                     final int max = Math.min(indices.length, other.indices.length);
     366                     for (int x = 0; eq && (x < max); x++) {
     367                         eq = indices[x] == other.indices[x];
     368                     }
     369                     if (eq) {
     370                         // ensure the remainder are -1
     371                         for(int x=max; eq && x<indices.length; x++) {
     372                             eq = indices[x] == -1;
     373                         }
     374                         for(int x=max; eq && x<other.indices.length; x++) {
     375                             eq = other.indices[x] == -1;
     376                         }
     377                     }
     378                     return eq;
     379                 }
     380             }
     381             return false;
     382         }
     383
     384         @Override
     385         public int hashCode() {
     386             int hash = super.hashCode();
     387             if (indices != null) hash = 31 * (hash + Arrays.hashCode(indices));
     388             return hash;
     389         }
     390
     391         @Override
     392         public String toString() {
     393             final String istr = indices != null ? Arrays.toString(indices) : "[]";
     394             return "ParentKey {"+className+", "+id+", "+String.valueOf(styleClass)+", "+istr+"}";
     395         }
     396
     397     }
     398
     399 }