1 /*
   2  * Copyright (c) 2008, 2014, 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.geometry.NodeOrientation;
  29 import javafx.scene.Node;
  30 
  31 import java.io.DataInputStream;
  32 import java.io.DataOutputStream;
  33 import java.io.IOException;
  34 import java.util.ArrayList;
  35 import java.util.Collections;
  36 import java.util.Iterator;
  37 import java.util.List;
  38 import java.util.Set;
  39 
  40 import com.sun.javafx.css.PseudoClassState;
  41 import com.sun.javafx.css.StyleClassSet;
  42 
  43 import static javafx.geometry.NodeOrientation.INHERIT;
  44 import static javafx.geometry.NodeOrientation.LEFT_TO_RIGHT;
  45 import static javafx.geometry.NodeOrientation.RIGHT_TO_LEFT;
  46 
  47 /**
  48  * A simple selector which behaves according to the CSS standard.
  49  *
  50  * @since 9
  51  */
  52 final public class SimpleSelector extends Selector {
  53     
  54     /**
  55      * If specified in the CSS file, the name of the java class to which
  56      * this selector is applied. For example, if the CSS file had:
  57      * <code><pre>
  58      *   Rectangle { }
  59      * </pre></code>
  60      * then name would be "Rectangle".
  61      */
  62     final private String name;
  63     /**
  64      * @return The name of the java class to which this selector is applied, or *.
  65      */
  66     public String getName() {
  67         return name;
  68     }
  69     
  70     /**
  71      * @return Immutable List&lt;String&gt; of style-classes of the selector
  72      */
  73     public List<String> getStyleClasses() {
  74         
  75         final List<String> names = new ArrayList<String>();
  76         
  77         Iterator<StyleClass> iter = styleClassSet.iterator();
  78         while (iter.hasNext()) {
  79             names.add(iter.next().getStyleClassName());
  80         }
  81         
  82         return Collections.unmodifiableList(names);
  83     }
  84 
  85     public Set<StyleClass> getStyleClassSet() {
  86         return styleClassSet;
  87     }
  88     
  89     /** styleClasses converted to a set of bit masks */
  90     final private StyleClassSet styleClassSet;
  91     
  92     final private String id;
  93     /*
  94      * @return The value of the selector id, which may be an empty string.
  95      */
  96     public String getId() {
  97         return id;
  98     }
  99     
 100     // a mask of bits corresponding to the pseudoclasses
 101     final private PseudoClassState pseudoClassState;
 102     
 103     Set<PseudoClass> getPseudoClassStates() {
 104         return pseudoClassState;
 105     }
 106 
 107     /**
 108      * @return Immutable List&lt;String&gt; of pseudo-classes of the selector
 109      */
 110     List<String> getPseudoclasses() {
 111         
 112         final List<String> names = new ArrayList<String>();
 113         
 114         Iterator<PseudoClass> iter = pseudoClassState.iterator();
 115         while (iter.hasNext()) {
 116             names.add(iter.next().getPseudoClassName());
 117         }
 118         
 119         if (nodeOrientation == RIGHT_TO_LEFT) {
 120             names.add("dir(rtl)");
 121         } else if (nodeOrientation == LEFT_TO_RIGHT) {
 122             names.add("dir(ltr)");
 123         }
 124                     
 125         return Collections.unmodifiableList(names);
 126     }
 127         
 128     // true if name is not a wildcard
 129     final private boolean matchOnName;
 130 
 131     // true if id given
 132     final private boolean matchOnId;
 133 
 134     // true if style class given
 135     final private boolean matchOnStyleClass;
 136 
 137     // dir(ltr) or dir(rtl), otherwise inherit
 138     final private NodeOrientation nodeOrientation;
 139     
 140     // Used in Match. If nodeOrientation is ltr or rtl, 
 141     // then count it as a pseudoclass
 142     public NodeOrientation getNodeOrientation() {
 143         return nodeOrientation;
 144     }
 145     
 146     // TODO: The parser passes styleClasses as a List. Should be array?
 147     SimpleSelector(final String name, final List<String> styleClasses,
 148             final List<String> pseudoClasses, final String id)
 149     {
 150         this.name = name == null ? "*" : name;
 151         // if name is not null and not empty or wildcard, 
 152         // then match needs to check name
 153         this.matchOnName = (name != null && !("".equals(name)) && !("*".equals(name)));
 154 
 155         this.styleClassSet = new StyleClassSet();
 156         
 157         int nMax = styleClasses != null ? styleClasses.size() : 0;
 158         for(int n=0; n<nMax; n++) { 
 159             
 160             final String styleClassName = styleClasses.get(n);
 161             if (styleClassName == null || styleClassName.isEmpty()) continue;
 162             
 163             final StyleClass styleClass = StyleClassSet.getStyleClass(styleClassName);
 164             this.styleClassSet.add(styleClass);
 165         }
 166         
 167         this.matchOnStyleClass = (this.styleClassSet.size() > 0);
 168 
 169         this.pseudoClassState = new PseudoClassState();
 170         
 171         nMax = pseudoClasses != null ? pseudoClasses.size() : 0;
 172 
 173         NodeOrientation dir = NodeOrientation.INHERIT;
 174         for(int n=0; n<nMax; n++) { 
 175             
 176             final String pclass = pseudoClasses.get(n);
 177             if (pclass == null || pclass.isEmpty()) continue;
 178             
 179             // TODO: This is not how we should handle functional pseudo-classes in the long-run!
 180             if ("dir(".regionMatches(true, 0, pclass, 0, 4)) {
 181                 final boolean rtl = "dir(rtl)".equalsIgnoreCase(pclass);
 182                 dir = rtl ? RIGHT_TO_LEFT : LEFT_TO_RIGHT;
 183                 continue;
 184             }
 185             
 186             final PseudoClass pseudoClass = PseudoClassState.getPseudoClass(pclass);
 187             this.pseudoClassState.add(pseudoClass);
 188         }
 189         
 190         this.nodeOrientation = dir;
 191         this.id = id == null ? "" : id;
 192         // if id is not null and not empty, then match needs to check id
 193         this.matchOnId = (id != null && !("".equals(id)));
 194 
 195     }
 196 
 197     @Override public Match createMatch() {
 198         final int idCount = (matchOnId) ? 1 : 0;
 199         int styleClassCount = styleClassSet.size();
 200         return new Match(this, pseudoClassState, idCount, styleClassCount);
 201     }
 202 
 203     @Override public boolean applies(Styleable styleable) {
 204         
 205         // handle functional pseudo-class :dir()
 206         // INHERIT applies to both :dir(rtl) and :dir(ltr)
 207         if (nodeOrientation != INHERIT && styleable instanceof Node) {
 208             final Node node = (Node)styleable;
 209             final NodeOrientation orientation = node.getNodeOrientation();
 210 
 211             if (orientation == INHERIT
 212                     ? node.getEffectiveNodeOrientation() != nodeOrientation
 213                     : orientation != nodeOrientation)
 214             {
 215                 return false;
 216             }
 217         }
 218 
 219         // if the selector has an id,
 220         // then bail if it doesn't match the node's id
 221         // (do this first since it is potentially the cheapest check)
 222         if (matchOnId) {
 223             final String otherId = styleable.getId();
 224             final boolean idMatch = id.equals(otherId);
 225             if (!idMatch) return false;
 226         }
 227 
 228         // If name is not a wildcard,
 229         // then bail if it doesn't match the node's class name
 230         // if not wildcard, then match name with node's class name
 231         if (matchOnName) {
 232             final String otherName = styleable.getTypeSelector();
 233             final boolean classMatch = this.name.equals(otherName);
 234             if (!classMatch) return false;
 235         }
 236 
 237         if (matchOnStyleClass) {
 238             
 239             final StyleClassSet otherStyleClassSet = new StyleClassSet();
 240             final List<String> styleClasses = styleable.getStyleClass();
 241             for(int n=0, nMax = styleClasses.size(); n<nMax; n++) { 
 242 
 243                 final String styleClassName = styleClasses.get(n);
 244                 if (styleClassName == null || styleClassName.isEmpty()) continue;
 245 
 246                 final StyleClass styleClass = StyleClassSet.getStyleClass(styleClassName);
 247                 otherStyleClassSet.add(styleClass);
 248             }
 249             
 250             boolean styleClassMatch = matchStyleClasses(otherStyleClassSet); 
 251             if (!styleClassMatch) return false;
 252         }
 253         
 254         return true;
 255     }
 256     
 257     @Override public boolean applies(Styleable styleable, Set<PseudoClass>[] pseudoClasses, int depth) {
 258 
 259         
 260         final boolean applies = applies(styleable);
 261         
 262         //
 263         // We only need the pseudo-classes if the selector applies to the node.
 264         // 
 265         if (applies && pseudoClasses != null && depth < pseudoClasses.length) {
 266 
 267             if (pseudoClasses[depth] == null) {
 268                 pseudoClasses[depth] = new PseudoClassState();
 269             }
 270             
 271             pseudoClasses[depth].addAll(pseudoClassState);
 272             
 273         }
 274         return applies;
 275     }
 276 
 277     @Override public boolean stateMatches(final Styleable styleable, Set<PseudoClass> states) {
 278         // [foo bar] matches [foo bar bang], 
 279         // but [foo bar bang] doesn't match [foo bar]
 280         return states != null ? states.containsAll(pseudoClassState) : false;
 281     }
 282 
 283     // Are the Selector's style classes a subset of the Node's style classes?
 284     //
 285     // http://www.w3.org/TR/css3-selectors/#class-html
 286     // The following selector matches any P element whose class attribute has been
 287     // assigned a list of whitespace-separated values that includes both
 288     // pastoral and marine:
 289     //
 290     //     p.pastoral.marine { color: green }
 291     //
 292     // This selector matches when class="pastoral blue aqua marine" but does not
 293     // match for class="pastoral blue".
 294     private boolean matchStyleClasses(StyleClassSet otherStyleClasses) {
 295         return otherStyleClasses.containsAll(styleClassSet);
 296     }
 297 
 298     @Override public boolean equals(Object obj) {
 299         if (obj == null) {
 300             return false;
 301         }
 302         if (getClass() != obj.getClass()) {
 303             return false;
 304         }
 305         final SimpleSelector other = (SimpleSelector) obj;
 306         if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
 307             return false;
 308         }
 309         if ((this.id == null) ? (other.id != null) : !this.id.equals(other.id)) {
 310             return false;
 311         }
 312         if (this.styleClassSet.equals(other.styleClassSet) == false) {
 313             return false;
 314         }
 315         if (this.pseudoClassState.equals(other.pseudoClassState) == false) {
 316             return false;
 317         } 
 318         
 319         return true;
 320     }
 321 
 322     /* Hash code is used in Style's hash code and Style's hash 
 323        code is used by StyleHelper */
 324     @Override public int hashCode() {
 325         int hash = 7;
 326         hash = 31 * (hash + name.hashCode());
 327         hash = 31 * (hash + styleClassSet.hashCode());
 328         hash = 31 * (hash + styleClassSet.hashCode());
 329         hash = (id != null) ? 31 * (hash + id.hashCode()) : 0;
 330         hash = 31 * (hash + pseudoClassState.hashCode());
 331         return hash;
 332     }
 333 
 334     /** Converts this object to a string. */
 335     @Override public String toString() {
 336         
 337         StringBuilder sbuf = new StringBuilder();
 338         if (name != null && name.isEmpty() == false) sbuf.append(name);
 339         else sbuf.append("*");
 340         Iterator<StyleClass> iter1 = styleClassSet.iterator();
 341         while(iter1.hasNext()) {
 342             final StyleClass styleClass = iter1.next();
 343             sbuf.append('.').append(styleClass.getStyleClassName());
 344         }
 345         if (id != null && id.isEmpty() == false) {
 346             sbuf.append('#');
 347             sbuf.append(id);
 348         }
 349         Iterator<PseudoClass> iter2 = pseudoClassState.iterator();
 350         while(iter2.hasNext()) {
 351             final PseudoClass pseudoClass = iter2.next();
 352             sbuf.append(':').append(pseudoClass.getPseudoClassName());
 353         }
 354             
 355         return sbuf.toString();
 356     }
 357 
 358     @Override protected final void writeBinary(final DataOutputStream os, final StyleConverter.StringStore stringStore)
 359         throws IOException
 360     {
 361         super.writeBinary(os, stringStore);
 362         os.writeShort(stringStore.addString(name));
 363         os.writeShort(styleClassSet.size());
 364         Iterator<StyleClass> iter1 = styleClassSet.iterator();
 365         while(iter1.hasNext()) {
 366             final StyleClass sc = iter1.next();
 367             os.writeShort(stringStore.addString(sc.getStyleClassName()));
 368         }
 369         os.writeShort(stringStore.addString(id));
 370         int pclassSize = pseudoClassState.size()
 371                 + (nodeOrientation == RIGHT_TO_LEFT || nodeOrientation == LEFT_TO_RIGHT ? 1 : 0);
 372         os.writeShort(pclassSize);
 373         Iterator<PseudoClass> iter2 = pseudoClassState.iterator();
 374         while(iter2.hasNext()) {
 375             final PseudoClass pc = iter2.next();
 376             os.writeShort(stringStore.addString(pc.getPseudoClassName()));
 377         }
 378         if (nodeOrientation == RIGHT_TO_LEFT) {
 379             os.writeShort(stringStore.addString("dir(rtl)"));
 380         } else if (nodeOrientation == LEFT_TO_RIGHT) {
 381             os.writeShort(stringStore.addString("dir(ltr)"));
 382         }
 383     }
 384 
 385     static SimpleSelector readBinary(int bssVersion, final DataInputStream is, final String[] strings)
 386         throws IOException
 387     {
 388         final String name = strings[is.readShort()];
 389         final int nStyleClasses = is.readShort();
 390         final List<String> styleClasses = new ArrayList<String>();
 391         for (int n=0; n < nStyleClasses; n++) {
 392             styleClasses.add(strings[is.readShort()]);
 393         }
 394         final String id = strings[is.readShort()];
 395         final int nPseudoclasses = is.readShort();
 396         final List<String> pseudoclasses = new ArrayList<String>();
 397         for(int n=0; n < nPseudoclasses; n++) {
 398             pseudoclasses.add(strings[is.readShort()]);
 399         }
 400         return new SimpleSelector(name, styleClasses, pseudoclasses, id);
 401     }
 402 }