All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.myfaces.trinidadinternal.skin.SkinCSSDocumentHandler Maven / Gradle / Ivy

The newest version!
/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 * 
 *  http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.apache.myfaces.trinidadinternal.skin;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;

import java.text.ParseException;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Set;
import java.util.HashSet;
import java.util.HashMap;
import java.util.LinkedList;

import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.share.io.InputStreamProvider;
import org.apache.myfaces.trinidad.share.io.NameResolver;
import org.apache.myfaces.trinidadinternal.agent.TrinidadAgent;
import org.apache.myfaces.trinidadinternal.share.io.CachingInputStreamProvider;
import org.apache.myfaces.trinidadinternal.share.xml.ParseContext;
import org.apache.myfaces.trinidadinternal.share.xml.ParseErrorUtils;
import org.apache.myfaces.trinidadinternal.share.xml.XMLUtils;
import org.apache.myfaces.trinidadinternal.style.util.ModeUtils;
import org.apache.myfaces.trinidadinternal.style.util.NameUtils;
import org.apache.myfaces.trinidadinternal.style.util.StyleUtils;
import org.apache.myfaces.trinidadinternal.style.xml.parse.PropertyNode;
import org.apache.myfaces.trinidadinternal.util.nls.LocaleUtils;


/** As the Skin css file is parsed, methods in this class are called to
 * build up a SkinStyleSheetNode.
 * TODO figure out if this is thread-safe
 *
 */
public class SkinCSSDocumentHandler
{
  /* ParseContext is useful for parsing CSS files only because we use it to set/get properties,
   * like the current NameResolver and InputStreamProvider.
   */
   public SkinCSSDocumentHandler(ParseContext pContext)  
   {
    _parseContext = pContext;     
   }

  /**
   * Return the List of SkinStyleSheetNodes that was created
   *  at the end of parsing the skin css file (endDocument).
   */
  public List  getSkinStyleSheetNodes()
  {
    // We now have a list of CompleteSelectorNodes.
    // We need to group this list into stylesheet nodes by matching
    // the additional information, like direction.
    // Then we create a list of SkinStyleSheetNodes.
    List  skinStyleSheetNodes = 
      _createSkinStyleSheetNodes(_completeSelectorNodeList, _namespaceMap);
     
    List allSkinStyleSheetNodes = new ArrayList();

    if (_imports != null && !_imports.isEmpty())
    {
      // _imports is a List>();;
      for (List  nodeList : _imports)
      {
        for (SkinStyleSheetNode node : nodeList)
        {
          allSkinStyleSheetNodes.add(node);
        }
      }

    }
    
    allSkinStyleSheetNodes.addAll(skinStyleSheetNodes);

    return
      allSkinStyleSheetNodes;
  }
  
   
  /**
  * Call this at the start of parsing the skin css file.
  */
  public void startDocument()
  {
    // do nothing
  }
   
  /**
  * Call this at the end of parsing the skin css file.
  */
  public void endDocument()
  {
    // do nothing
  }

  public void comment(String text)
  {
     // ignore comments
  }
   
  /**
  * Call this at the beginning of parsing one set of selectors/properties.
  * e.g., .AFDefaultFont, af|breadCrumbs::font
  * {font-family:Arial,Helvetica; font-size:small}
  */
  public void startSelector()
  {
    _inStyleRule = true;
    // CSS spec says to ignore all @import rules after any other rules are processed.
    _ignoreImports = true;
    _propertyNodeList = new ArrayList();
  }
   
  /**
   * Call this at the end of parsing one set of selectors/properties.
   * @param selectors A List of Strings, each String is a selector.
   * e.g., given the selectors/properties:
   * .AFDefaultFont, af|breadCrumbs::font
   * {font-family:Arial,Helvetica; font-size:small}
   * The selectors in the List are
   * ".AFDefaultFont" and "af|breadCrumbs::font"
  */
  public void endSelector(List selectors)
  {
    if (selectors == null)
      return;
      
    int selectorNum = selectors.size();
    
    for (int i = 0; i < selectorNum; i++)
    {
       String selector = selectors.get(i);
       CompleteSelectorNode node =
         _createCompleteSelectorNode(selector,
                                     _propertyNodeList,
                                     _locales,
                                     _agentAtRuleMatcher,
                                     _selectorPlatforms,
                                     _getSelectorAccProperties(),
                                     _mode);
       _completeSelectorNodeList.add(node);
    }
    // reset flags
    _inStyleRule = false;
    _propertyNodeList = null;
  }

   /**
    * Call this when a property name/value is found.
    * e.g., given the selectors/properties:
    * .AFDefaultFont, af|breadCrumbs::font
    * {font-family:Arial,Helvetica; font-size:small}
    * One property name/value pair is "font-family"/"Arial,Helvetica"
    * If the name and value are both non-null and we are in a style rule,
    * then a PropertyNode will be created and added to the _propertyNodeList.
    * @param name
    * @param value
    * 
    */
  public void property(String name, String value)
  {

    if (_inStyleRule && (_propertyNodeList != null))
    {
      if (name == null || "".equals(name))
      {
        _LOG.severe("ERR_PARSING_SKIN_CSS_FILE", new Object[] {name, value});
      }
      else
        _propertyNodeList.add(new PropertyNode(name, value));
    }

  }

 /**
  * Call when you have an atRule. This will do further processing.
  * @param atRule The @rule string
  * e.g., @namespace af url(http:\\www.xxx.com);
  * e.g., @agent gecko { .foo {color:red}}
  */
  public void atRule(String atRule)
  {
    // parse the atRule further here.
   boolean importRule = atRule.startsWith(_AT_IMPORT);
   boolean charsetRule = atRule.startsWith(_AT_CHARSET);
   
    if (atRule != null)
    {
      if (importRule)
      {
        if (_ignoreImports)
        {
          // according to the css spec, @imports must come before all other rules (except @charset).
          if (_LOG.isWarning())
             _LOG.warning("AT_IMPORT_NOT_FIRST", atRule);
        }
        else
          _parseImport(atRule);
      }
      else if (atRule.startsWith(_AT_NAMESPACE))
      {
        _parseNamespace(_namespaceMap, atRule);
      }
      else if (atRule.startsWith(_AT_AGENT))
      {
        _parseCustomAtRule(_AT_AGENT, atRule);
      }
      else if (atRule.startsWith(_AT_PLATFORM))
      {
        _parseCustomAtRule(_AT_PLATFORM, atRule);
      }
      else if (atRule.startsWith(_AT_LOCALE))
      {
        _parseCustomAtRule(_AT_LOCALE, atRule);
      }
      else if (atRule.startsWith(_AT_ACC_PROFILE))
      {
        _parseCustomAtRule(_AT_ACC_PROFILE, atRule);
      }
      else if (atRule.startsWith(_AT_MODE))
      {
        _parseCustomAtRule(_AT_MODE, atRule);
      }
      // for now, ignore other atRules in a skinning css file
      
      // CSS spec says you ignore all @import rules after any other rules are processed
      // (except for @charset).
      if(!importRule && !charsetRule)
      {
        _ignoreImports = true;
      }
      
    }
  }

  private void _parseNamespace(Map namespaceMap, String atRule)
  {
   // TODO deal with default namespaces that don't have prefixes??
   String[] namespaceParts = atRule.split("\\s+");
   if (namespaceParts.length > 2)
   {
     String url = _extractUrl(namespaceParts[2]);
     namespaceMap.put(namespaceParts[1], url);
   }
  }

  private String _extractUrl(String urlString)
  {

    urlString = urlString.trim();
     
    // first, strip off the url( and );
    if (urlString.startsWith("url("))
      urlString = urlString.substring(4);
    if (urlString.endsWith(");"))
      urlString = urlString.substring(0, urlString.length() - 2);
    else if (urlString.endsWith(";"))
      urlString = urlString.substring(0, urlString.length() - 1);
       
    // second, strip off the starting/ending quotes if there are any
    urlString = SkinStyleSheetParserUtils.trimQuotes(urlString);
    return urlString;
  }
  
  
  private void _parseImport(String type)
  {

    // parse any of these
    //@import "mystyle.css";
    //@import url("mystyle.css");
    //@import url(mystyle.css);

    // strip out @import any spaces, then get the url
    String styleSheetName = _extractUrl(type.substring(8));

    try
    {
      if (_imports == null)
        _imports = new ArrayList>();
      _imports.add(_parseImportStyleSheetFile(_parseContext, styleSheetName, List.class));
    }
    catch (IOException e)
    {
      if (_LOG.isSevere())
        _LOG.severe("CANNOT_LOAD_STYLESHEET", styleSheetName);
        _LOG.severe(e);
    }
    catch (ParseException e)
    {
      _LOG.severe(e);
    }

  }
  
  private List _parseImportStyleSheetFile(
    ParseContext  context,
    String        sourceName,
    Class      expectedType)
    throws IOException, ParseException
  {
    // Step 1. Find the name resolver  
    NameResolver resolver = XMLUtils.getResolver(context);
    if (resolver == null)
    {
      if (_LOG.isWarning())
         _LOG.warning("Internal error: couldn't find NameResolver");
      
      return Collections.emptyList();
    }
    
    // Step 2. Find an InputStreamProvider. Mark a dependency on the base provider (if necessary)
    InputStreamProvider importProvider = resolver.getProvider(sourceName);
    InputStreamProvider baseProvider = XMLUtils.getInputStreamProvider(context);
    if (baseProvider instanceof CachingInputStreamProvider)
    {
      // important: hasSourceChanged takes into account this dependency
      ((CachingInputStreamProvider)baseProvider).addCacheDependency(importProvider);
    }
    
    // Step 3. Detect if this will be a circular include
    ArrayList list = (ArrayList) context.getProperty(_SHARE_NAMESPACE, 
                                                                     _INCLUDE_STACK);
    Object identifier = importProvider.getIdentifier();
    
    if ((list != null) && (list.contains(identifier)))
    {
      // Just logging an error isn't really enough - the include
      // will fail, but parsing continues and you'll get a stack overflow.  So, instead, we throw
      // an exception...
     throw new ParseException(_LOG.getMessage(
      "CIRCULAR_INCLUDE_DETECTED", sourceName), 0);
    }

    // Step 4. Try to get a cached version
    // =-=jmw  I don't know when the cached returns non-null other than when
    // the same import is included twice. This step (and Step 7) might not be worth it.
    // comment out caching code
    //Object cached = importProvider.getCachedResult();
    //if ((cached != null) && expectedType.isInstance(cached))
    //{
     //return (List>)cached;
    //}
    
    // Step 5. Set up the new context; first, clone the original
    ParseContext newContext = (ParseContext)context.clone();
    
    // Add the current identifier to the stack (used for detecting circular includes) 
    // placed on the ParseContext
    // cloning ParseContext does a shallow copy. It doesn't copy this list.
    if (list == null)
      list = new ArrayList();
    else
      list = new ArrayList(list);
    list.add(identifier);
    newContext.setProperty(_SHARE_NAMESPACE, _INCLUDE_STACK, list);
    
    InputStream stream = importProvider.openInputStream();
    try
    {

      // Store a resolver relative to the file we're about to parse. This will be used for imports.
      // Store the inputStreamProvider on the context;
      // this will be used to get the document's timestamp later on
      XMLUtils.setResolver(newContext, resolver.getResolver(sourceName));
      XMLUtils.setInputStreamProvider(newContext, importProvider);

      // PARSE!
      // create a SkinStyleSheetNode
      // (contains a namespaceMap and a List of SkinSelectorPropertiesNodes
      // and additional information like direction, locale, etc.)
      // (selectorName + a css propertyList))
      BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
      SkinCSSParser parser = new SkinCSSParser();
      // Send over the ParseContext so that we can get the resolver from it in case we encounter 
      // an @import in the CSS file.
      SkinCSSDocumentHandler documentHandler = new SkinCSSDocumentHandler(newContext);
      parser.parseCSSDocument(reader, documentHandler);
      // If the imported css has an import of its own, this list will contain all the
      // imported nodes.
      List  importSkinSSNodeList = documentHandler.getSkinStyleSheetNodes();      

      reader.close();
      
      // Step 7. Store the cached result (if successful)
      //if (!_imports.isEmpty())
      //{
      //  importProvider.setCachedResult(_imports);
      //}
      return importSkinSSNodeList;

    }
    finally
    {
      stream.close();
    }
  }
  
  /** Get the atRule, and send its contents through the SkinCSSParser
   * again, using the current DocumentHandler object. The start/end
   * callbacks will be called again, but in the context of the atRule.
   */
  private void _parseCustomAtRule(String type, String atRule)
  {
    // get the @agent agents, @platform platforms or the @locale locales 
    // they are deliminated by commas parse out the content
    // save the atRule type, so the document handler code can get to it.
    // run this through parser again
    String content = _getAtRuleContent(atRule);
    _initAtRuleTargetTypes(type, atRule);
    
    // use this current DocumentHandler. This way we can add to the
    // CompleteSelectorNode list with agent information.
    SkinCSSParser parser = new SkinCSSParser();
    parser.parseCSSDocument(new StringReader(content), this);
    
    // reset
    _resetAtRuleTargetTypes(type);

  }
  
  private void _resetAtRuleTargetTypes(
    String type)
  {
    if (_AT_AGENT.equals(type))
      _agentAtRuleMatcher = null;
    else if (_AT_PLATFORM.equals(type))
      _selectorPlatforms = null;
    else if (_AT_LOCALE.equals(type))
      _locales = null;
    else if (_AT_ACC_PROFILE.equals(type))
    {
      assert(!_selectorAccPropertiesStack.isEmpty());
      
      if (!_selectorAccPropertiesStack.isEmpty())
        _selectorAccPropertiesStack.removeLast();
    }
    else if (_AT_MODE.equals(type))
    {
      _mode = ModeUtils.MODE_DEFAULT;
    }
  }
  
   // create a CompleteSelectorNode (this is the selector, properties, and
   // additional info, like 'rtl' direction
  private CompleteSelectorNode _createCompleteSelectorNode(
    String                     selector,
    List         propertyNodeList,
    Set                locales,
    AgentAtRuleMatcher         agentMatcher,
    int[]                      selectorPlatforms,
    Set                selectorAccProperties,
    int                        mode)
  {
    // parse the selector to see if there is a :rtl or :ltr ending.
    // if so, then set the reading direction.
    int direction = LocaleUtils.DIRECTION_DEFAULT;
    if (selector.endsWith(StyleUtils.RTL_CSS_SUFFIX))
    {
      int length = StyleUtils.RTL_CSS_SUFFIX.length();
      // strip off the SUFFIX
      selector = selector.substring(0, selector.length()-length);
      direction = LocaleUtils.DIRECTION_RIGHTTOLEFT;
    }
    else if (selector.endsWith(StyleUtils.LTR_CSS_SUFFIX))
    {
       int length = StyleUtils.LTR_CSS_SUFFIX.length();
       // strip off the SUFFIX
       selector = selector.substring(0, selector.length()-length);
       direction = LocaleUtils.DIRECTION_LEFTTORIGHT;
    }

    return
      new CompleteSelectorNode(
        selector,
        propertyNodeList,
        locales,
        direction,
        agentMatcher,
        selectorPlatforms,
        selectorAccProperties,
        mode);
  }

  /**
    * Given a List of CompleteSelectorNodes (selector nodes with
    * infor about selectors, properties, direction, agent), we create a List of
    * SkinStyleSheetNodes.
    * @param selectorList a list of CompleteSelectorNodes.
    * @param namespaceMap the namespace map
    * @return a List of SkinStyleSheetNodes
    */
  private List  _createSkinStyleSheetNodes(
    List selectorList,
    Map        namespaceMap)
  {
    List skinStyleSheetNodes =
      new ArrayList();
     
    for (CompleteSelectorNode completeSelectorNode : selectorList)
    {
      // we add to the ssNodeList in this method.
      int direction = completeSelectorNode.getDirection();
      AgentAtRuleMatcher agentMatcher = completeSelectorNode.getAgentMatcher();
      int[] platforms = completeSelectorNode.getPlatforms();
      Set locales = completeSelectorNode.getLocales();
      Set accProperties = completeSelectorNode.getAccessibilityProperties();
      int mode = completeSelectorNode.getMode();

      // loop through the skinStyleSheetNodeList to find a match
      // of direction, agents, platforms, etc.
      boolean match = false;
         
      // iterate backwards, because the last node is most likely the
      // matching stylesheetnode.
      for (int i = skinStyleSheetNodes.size() - 1; i >= 0 && !match; --i)
      {
        SkinStyleSheetNode ssNode = skinStyleSheetNodes.get(i);
        match = ssNode.matches(direction, agentMatcher, platforms, locales, accProperties, mode);

        if (match)
          ssNode.add(completeSelectorNode.getSkinSelectorPropertiesNode());
      }

      if (!match)
      {
        // no matching stylesheet node found, so create a new one
        SkinStyleSheetNode ssNode =
         new SkinStyleSheetNode(namespaceMap, direction, locales, agentMatcher, platforms, accProperties, mode);
        ssNode.add(completeSelectorNode.getSkinSelectorPropertiesNode());
        skinStyleSheetNodes.add(ssNode);
      }
    }
    return skinStyleSheetNodes;
  }

  /**
   * Initialized at rule target types.
   * 
   * @param type type of the at rule. _AT_AGENT, _AT_PLATFORM, _AT_ACC_PROFILE or _AT_LOCALE
   * @param atRule - the atRule string
   */
  private void _initAtRuleTargetTypes(
    String type,
    String atRule)
  {
    // given the atRule string, get the target types --
    // @agent ie, gecko {...} => target types are the
    // AdfFacesAgent constants for ie and gecko.
    int firstSpace = atRule.indexOf(' ');
    int openBrace = atRule.indexOf('{');
    if (firstSpace != -1 && openBrace != -1)
    {
      String types = atRule.substring(firstSpace, openBrace);
      String[] typeArray = types.split(",");
      
      if (_AT_AGENT.equals(type))
      {
        _agentAtRuleMatcher = new AgentAtRuleMatcher(typeArray);
      }
      else if (_AT_PLATFORM.equals(type))
      {
        List list = new ArrayList();

        for (int i=0; i < typeArray.length; i++)
        {
          int platformInt = NameUtils.getPlatform(typeArray[i].trim());

          if (platformInt != TrinidadAgent.OS_UNKNOWN)
            list.add(platformInt);
        }
        
        _selectorPlatforms = _getIntArray(list);
      }
      else if (_AT_LOCALE.equals(type))
      {
        _locales = new HashSet();
        for (int i = 0; i < typeArray.length; i++)
        {
          Locale locale = LocaleUtils.getLocaleForIANAString(typeArray[i].replace('_', '-').trim());
          _locales.add(locale);
        }
      }
      else if (_AT_ACC_PROFILE.equals(type))
      {
        // The number of profile properties is always going to be
        // very small, so we need to specify some initial capacity -
        // the default is too large.  Make the hash set twice as
        // large as the number of properties so that there is some
        // room to avoid collisions.  This probably isn't especially
        // effective, but probably doesn't matter much given the
        // small size of our sets.
        Set set = new HashSet(typeArray.length * 2);

        for (int i=0; i < typeArray.length; i++)
        {
          String accProp = typeArray[i].trim();

          if (NameUtils.isAccessibilityPropertyName(accProp))
          {
            set.add(accProp);
          }
          else
          {
            _LOG.warning("INVALID_ACC_PROFILE", new Object[]{accProp});
          }
        }
        
        if (!_selectorAccPropertiesStack.isEmpty())
          set = _mergeAccProperties(_selectorAccPropertiesStack.getLast(), set);

        _selectorAccPropertiesStack.add(set);
      }
      else if (_AT_MODE.equals(type))
      {
        _mode = NameUtils.getMode(typeArray[0].trim());
      }
    }
  }
 
  // Copies Integers from a List of Integers into an int array
  private int[] _getIntArray(List  integerList)
  {
    int count = integerList.size();
  
    if (count == 0)
      return null;
  
    int[] array = new int[count];
  
    for (int i = 0; i < count; i++)
      array[i] = integerList.get(i).intValue();
  
    return array;
  }
   
   /**
    * 
    * @param atRule - the entire @rule's definition, including content.
    * e.g., @agent ie, gecko { af|inputText::content {color:red}}
    * @return the content as a String
    * e.g., "af|inputText::content {color:red}"
    */
  private String _getAtRuleContent(String atRule)
  {
    int openBraceIndex = atRule.indexOf('{');
    int endBraceIndex = atRule.lastIndexOf('}');
    if (endBraceIndex == -1)
      endBraceIndex = atRule.length();
      
    if (openBraceIndex == -1)
      return null;
    else
     return atRule.substring(openBraceIndex+1, endBraceIndex);
   
  }

  // Returns the accessibility properties in effect for the current selector
  private Set _getSelectorAccProperties()
  {
    return _selectorAccPropertiesStack.isEmpty() ?
             null :
             _selectorAccPropertiesStack.getLast();
  }

  // When specifying multiple values in an @accessibility-profile rule, eg:
  //
  // @accessibility-profile high-contrast, large-fonts {
  //   .selector { property: value }
  // }
  //
  // We treat the ',' separator as a logical "or" - ie. we match if either
  // high-contrast or large-fonts is specified by the AccessibilityProfile.
  //
  // In order to "and" accessibility profile properties together, we need
  // to support nested @accessibility-profile rules, eg:
  //
  // @accessibility-profile high-contract {
  //   @accessibility-profile large-fonts {
  //      .selector { property: value}
  //   }
  // }
  //
  // Where we only match the inner rule if both the outer rule *and* the
  // inner rule are matched.  We need some way to represent the fact that
  // we both rules must match.  We can't simply add the individual values
  // into the accessibility properties Set - since we will match either
  // value rather than require that both are present.  Instead we create
  // new compound values, eg. "high-contrast&large-fonts".  This allows the
  // accessibility matching logic in StyleSheetNode to detect cases where
  // multiple properties are required in order to accept the match.
  private Set _mergeAccProperties(
    Set oldProperties,
    Set newProperties)
  {
    // If we don't have any old properties, no merging to do, just use
    // the new properties.
    if ((oldProperties == null) || oldProperties.isEmpty())
      return newProperties;

    // If we don't have any new properties, no merging to do, but we
    // want to inherit the old properties, so make a copy.
    if ((newProperties == null) || newProperties.isEmpty())
      return new HashSet(oldProperties);
    
    // We have both old and new properties.  We need to merge
    // these into a single set.  At the most the merged set contains
    // oldProperties.size() * newProperties.size().  (We double this to
    // avoid collisions/re-allocations.)
    int mergedSize = oldProperties.size() + newProperties.size();
    Set mergedProperties = new HashSet(mergedSize * 2);
    for (String oldProperty : oldProperties)
    {
      for (String newProperty : newProperties)
        mergedProperties.add(oldProperty + "&" + newProperty);
    }

    return mergedProperties;
  }
  
   /**
    * This Class contains a SkinSelectorPropertiesNode and a rtl direction.
    * We will use this information when creating a SkinStyleSheetNode.
    */
  private static class CompleteSelectorNode
  {
    public CompleteSelectorNode(
      String                     selectorName,
      List         propertyNodes,
      Set                locales,
      int                        direction,
      AgentAtRuleMatcher         agentMatcher,
      int[]                      platforms,
      Set                accProperties,
      int                        mode
      )
    {
      _node = new SkinSelectorPropertiesNode(selectorName, propertyNodes);
      _direction = direction;
      
      // copy the agents and platforms because these get nulled out
      // at the end of the @rule parsing.
      _agentMatcher = agentMatcher;
      _platforms = _copyIntArray(platforms);
      _locales = ((locales != null) ? new HashSet(locales)
            :Collections.emptySet());
      
      if (accProperties != null)
      {
        // Copy acc properties just to be safe.  Note that we don't
        // bother wrapping in an unmodifiable set - just following
        // the pattern used for the agents/platforms arrays.
        _accProperties = new HashSet(accProperties);
      }
      else
      {
        _accProperties = null;
      }
      _mode = mode;
    }
    
    public SkinSelectorPropertiesNode getSkinSelectorPropertiesNode()
    {
      return _node;
    }
    
    public int getDirection()
    {
      return _direction;
    }

    /**
     * @return The AgentMatcher if any for this rule
     */
    public AgentAtRuleMatcher getAgentMatcher()
    {
      return _agentMatcher;
    }
    
    public int[] getPlatforms()
    {
      return _platforms;
    }

    public Set getLocales()
    {
      return _locales;
    }

    public Set getAccessibilityProperties()
    {
      return _accProperties;
    }

    public int getMode()
    {
      return _mode;
    }

    // Returns a copy of the int array
    private static int[] _copyIntArray(int[] array)
    {
      if (array == null)
        return null;
    
      int[] copy = new int[array.length];
      System.arraycopy(array, 0, copy, 0, array.length);
    
      return copy;
    }

    private final SkinSelectorPropertiesNode _node;
    private final int _direction;  // the reading direction
    private final AgentAtRuleMatcher _agentMatcher;
    private final int[] _platforms;
    private final Set _locales; 
    private final Set _accProperties;
    private int _mode;
  }

  private static final String _AT_AGENT = "@agent";
  private static final String _AT_PLATFORM = "@platform";
  private static final String _AT_LOCALE = "@locale";
  private static final String _AT_ACC_PROFILE = "@accessibility-profile";
  private static final String _AT_MODE = "@mode";
  private static final String _AT_IMPORT = "@import";
  private static final String _AT_NAMESPACE = "@namespace";
  private static final String _AT_CHARSET = "@charset";


  // below are properties that we set and reset
  // as the methods of this documentHandler get called.
  private boolean _inStyleRule = false;
  private List _propertyNodeList = null;
  // we build this list as we parse the skinning css file. We use this
  // list to create a list of SkinStyleSheetNodes
  private List  _completeSelectorNodeList =
    new ArrayList();
  // these are the selector platform, agents and accessiblity properties of the
  // selectors we are currently parsing in this document.
  private int[] _selectorPlatforms = null;

  // matches the current Agent against the allowed agents
  private AgentAtRuleMatcher _agentAtRuleMatcher = null;

  // the locales of the selectors parsed in this document.
  private Set _locales = null;

  //the mode for which the parsed selectors are valid.
  private int _mode = ModeUtils.MODE_DEFAULT;

  // Stack of accessibility property sets.  While java.util.Stack has the
  // push/pop API that we want, we don't need the synchronization, so we
  // just use a LinkedList instead and pretend its a stack.
  private LinkedList> _selectorAccPropertiesStack =
    new LinkedList>();

  private Map _namespaceMap = new HashMap();
  private ParseContext _parseContext;
  private List> _imports;
  private boolean _ignoreImports = false;

  // Perhaps move to ShareConstants
  static private final String _SHARE_NAMESPACE  =
    "org.apache.myfaces.trinidadinternal.skin.SkinCSSDocumentHandler";
  static private final String _INCLUDE_STACK = "_includeStack";

  private static final TrinidadLogger _LOG =
    TrinidadLogger.createTrinidadLogger(SkinCSSDocumentHandler.class);
}