blob: 95d5ef542ce8cd05c15be9bf52e42007fc37e868 [file] [log] [blame]
/*
* 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.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
/**
* This parses a skin css file into namespace map and
* selector/properties.
*/
public class SkinCSSParser
{
public SkinCSSParser()
{
}
public void parseCSSDocument(
Reader in,
SkinCSSDocumentHandler documentHandler)
{
try
{
CSSScanner scanner = new CSSScanner(in);
documentHandler.startDocument();
List<String> selectorList = null;
// start scanning the document
// return comments /* xxx */
// return @rules, which end with ';'
// return selectors, which end with a {
// return properties, which end with a }
int currentType = _nextIgnoreSpaces(scanner);
while (currentType != CSSLexicalUnits.EOF)
{
if (currentType == CSSLexicalUnits.COMMENT)
documentHandler.comment(scanner.getStringValue());
else if (currentType == CSSLexicalUnits.AT_KEYWORD)
{
// Remove any comments that are inside of the @rule
String atRule = scanner.getStringValue();
atRule = _COMMENT_PATTERN.matcher(atRule).replaceAll("");
documentHandler.atRule(atRule);
}
else if (currentType == CSSLexicalUnits.LEFT_CURLY_BRACE)
{
documentHandler.startSelector();
selectorList = _parseSelectorString(scanner.getStringValue());
}
else if (currentType == CSSLexicalUnits.RIGHT_CURLY_BRACE)
{
String properties = scanner.getStringValue();
_handlePropertiesString(documentHandler, properties);
if (selectorList == null)
{
if (_LOG.isWarning())
{
_LOG.warning("IGNORING_PROPERTIES_WITHOUT_SELECTOR", properties);
}
}
documentHandler.endSelector(selectorList);
}
currentType = _nextIgnoreSpaces(scanner);
}
}
finally
{
documentHandler.endDocument();
}
}
/**
* given a string that denotes the selectors in a css file, parse this
* further into a list of selectors.
* (the selectors are deliminated by commas)
*/
private List<String> _parseSelectorString(
String selectors)
{
// give a list of selectors, deliminated by commas, parse into a List.
// loop thru each character until I get to a left bracket.
if (selectors == null)
return null;
List<String> selectorList = new ArrayList<String>();
// pull apart by commas
// don't skip whitespace since whitespace means descendant selectors in css
String[] selector = _splitString(selectors, ',', false);
String trimmedSelector;
for (int i=0; i < selector.length; i++)
{
// the first selector might have extra }
// this is a common typo, to have extra }s.
if (i == 0)
{
trimmedSelector = _trimChar(selector[i].trim(), '}');
}
else
{
trimmedSelector = selector[i].trim();
}
// skip the selector if it is empty
if ("".equals(trimmedSelector))
{
if (_LOG.isWarning())
_LOG.warning("ERR_PARSING_SKIN_SELECTOR", selectors);
}
else
selectorList.add(trimmedSelector);
}
return selectorList;
}
/**
* given a string that denotes the properties of one or more selectors,
* parse further into name/value pairs and call documentHandler's property
* callback.
*/
private void _handlePropertiesString(
SkinCSSDocumentHandler documentHandler,
String properties)
{
if (properties == null)
return;
// first, parse out any comments
Matcher matcher = _COMMENT_PATTERN.matcher(properties);
properties = matcher.replaceAll("");
// handle with direct base 64 encodings, like encoded images (see Bug 13361138)
boolean containsEncodedImage = properties.indexOf(_BASE_64) != -1;
// replace with token, so we avoid incorrect splitting and unnecessary reconstruction
if (containsEncodedImage)
{
properties = properties.replaceAll(_BASE_64, _BASE_64_REPLACEMENT_TOKEN);
}
// split into name and value (don't skip whitespace since properties like padding: 0px 5px
// need the spaces)
String[] property = _splitString(properties, ';', false);
for (int i=0; i < property.length; i++)
{
int indexOfColon = property[i].indexOf(':');
if ((indexOfColon > -1) && (indexOfColon < property[i].length()))
{
String name = property[i].substring(0, indexOfColon);
String value = property[i].substring(indexOfColon+1);
// if there is possibility of a base 64 encoded value, adjust the replacement token that we added earlier
if (containsEncodedImage && value.indexOf(_BASE_64_REPLACEMENT_TOKEN) != -1)
{
value = value.replace(_BASE_64_REPLACEMENT_TOKEN, _BASE_64);
}
documentHandler.property(name.trim(), value.trim());
}
}
}
/**
* return the array of strings computed by splitting this string
* around matches of the given character
* @param in
* @param charDelimiter
* @param skipWhitespace if true, whitespace is skipped and not included in the return Strings
* in the String array.
* @return String[] The array of Strings computed by splitting the input String
* around matches of the charDelimiter
*/
private static String[] _splitString (
String in,
char charDelimiter,
boolean skipWhitespace)
{
// return a String[] with each piece that is deliminated by the inChar.
int length = in.length();
StringBuffer buffer = new StringBuffer(length);
List<String> splitList = new ArrayList<String>();
for (int i=0; i < length; i++)
{
char c = in.charAt(i);
if (c == charDelimiter)
{
// we hit the delimiter, so put it in the splitList and start a new buffer.
splitList.add(buffer.toString());
buffer = new StringBuffer(length);
}
else
{
// it's ok to put the character in the buffer if we don't want to skip whitespace
// or if it isn't whitespace to begin with.
if (!skipWhitespace || !(Character.isWhitespace(c)))
buffer.append(c);
}
}
// we are done with all the characters
String lastString = buffer.toString();
if (lastString.length() > 0)
splitList.add(lastString);
return splitList.toArray(_EMPTY_STRING_ARRAY);
}
private static String _trimChar (
String in,
char c)
{
int len = in.length();
char currentChar = in.charAt(0);
if (currentChar != c)
return in;
for (int i=1; i < len; i++)
{
currentChar = in.charAt(i);
if (currentChar != c)
{
return in.substring(i);
}
}
return in;
}
// ignores spaces.
private int _nextIgnoreSpaces(CSSScanner scanner)
{
int currentType = scanner.getNextToken();
while (currentType == CSSLexicalUnits.SPACE)
{
currentType = scanner.getNextToken();
}
return currentType;
}
// This class builds up tokens for SPACE, COMMENT, AT_RULE,
// LEFT_CURLY_BRACE (selectorList), and RIGHT_CURLY_BRACE (properties)
// A token is stored in the _buffer object.
private static class CSSScanner
{
public CSSScanner(Reader reader)
{
_reader = reader;
}
public String getStringValue()
{
if (_end <= 0)
return null;
else
return new String(_buffer, 0, _end);
}
// get the next token in the buffer and return the type
public int getNextToken()
{
_position = 0;
_fillToken();
_end = _position;
// strip off the final brace if needed
if (_type == CSSLexicalUnits.RIGHT_CURLY_BRACE ||
_type == CSSLexicalUnits.LEFT_CURLY_BRACE)
_end--;
if (_currentChar == -1)
return CSSLexicalUnits.EOF;
return _type;
}
private void _fillToken()
{
while (true)
{
_nextChar();
switch (_currentChar)
{
case -1:
_type = CSSLexicalUnits.EOF;
break;
case ' ':
case '\t':
case '\n':
case '\f':
case '\r':
if (_type != CSSLexicalUnits.LEFT_CURLY_BRACE)
{
_type = CSSLexicalUnits.SPACE;
return;
}
// fall through to LEFT_CURLY_BRACE
case '/':
if (_type != CSSLexicalUnits.LEFT_CURLY_BRACE)
{
// check for comment. If it is a comment, set the type and return
// if it isn't a comment, keep looping to get more characters.
_nextChar();
if (_currentChar == '*')
{
// WE ARE IN A COMMENT
// loop and get characters into buffer until we get '*/'
_nextChar();
int prevChar;
while (_currentChar != -1)
{
prevChar = _currentChar;
_nextChar();
if ((prevChar == '*') && (_currentChar == '/'))
break;
}
_type = CSSLexicalUnits.COMMENT;
return;
}
// wasn't a comment, so keep going on, filling the buffer with
// each _nextChar call.
break;
}
case '@':
if (_type != CSSLexicalUnits.LEFT_CURLY_BRACE)
{
// found @.
// @namespace is treated differently than other @rules.
// These are the formats:
// @namespace foo url(http://www.foo.com);
// @agent {
// af|inputText::content{color:red; background-color:blue;}
// }
// @platform {...}
// If @namespace, go 'til the semi-colon
// Else, go until the start/end brace match.
// found @. keep getting characters until we get a ; or end of file.
/*
_nextChar();
while ((_currentChar != -1) && (_currentChar != ';'))
{
_nextChar();
}
*/
_nextChar();
// go until ; or until {} match
int openBraceCount = 0;
boolean openBraceCountStarted = false;
while ((_currentChar != -1))
{
if (_currentChar == '{')
openBraceCount++;
if (openBraceCount == 1)
openBraceCountStarted = true;
if (_currentChar == '}' && openBraceCountStarted)
{
openBraceCount--;
// make sure openBraceCount never goes negative
// if it does, then there was an extra right curly brace
if (openBraceCount < 0)
{
_handleBraceMismatch();
}
if (openBraceCountStarted && openBraceCount == 0)
{
break;
}
}
if (_currentChar == ';' && openBraceCount==0)
{
break;
}
_nextChar();
}
// There should not be any closing braces pending at this point
if (openBraceCountStarted && openBraceCount != 0)
{
_handleBraceMismatch();
}
_type = CSSLexicalUnits.AT_KEYWORD;
return;
}
default:
if (_type == CSSLexicalUnits.LEFT_CURLY_BRACE)
{
// these are the properties,
// keep going until we have all the properties
while ((_currentChar != -1) && (_currentChar != '}'))
{
if (_currentChar == '{')
{
// this is not expected. There is a right curly braces missing
_handleBraceMismatch();
}
_nextChar();
}
_type = CSSLexicalUnits.RIGHT_CURLY_BRACE;
}
else
{
while ((_currentChar != -1) && (_currentChar != '{'))
{
// here we navigate to the opening curly braces
// there cannot be a closing curly brace here
if (_currentChar == '}')
_handleBraceMismatch();
_nextChar();
}
_type = CSSLexicalUnits.LEFT_CURLY_BRACE;
}
return;
} // end switch
if (_currentChar == -1)
{
_type = CSSLexicalUnits.EOF;
return;
}
}
}
private void _handleBraceMismatch()
{
// This log is dependent on LOG in StyleSheetEntry._createSkinStyleSheetFromCSS
// The skin file name is logged there.
_LOG.warning("CSS_SYNTAX_ERROR");
}
// fill buffer with one more character
private void _nextChar()
{
try
{
_currentChar = _reader.read();
}
catch (IOException e)
{
if (_LOG.isSevere())
{
_LOG.severe("ERR_READING_SKIN_CSS_FILE", e);
}
_currentChar = -1;
return;
}
// need to make sure buffer doesn't go over its size
if (_buffer.length <= _position)
{
// increase buffer size by 50%
char[] tmp = new char[_buffer.length + (_buffer.length/2)];
// copy over buffer to new buffer
for (int i=0; i < _buffer.length; i++)
tmp[i] = _buffer[i];
// pt _buffer to bigger buffer.
_buffer = tmp;
}
_buffer[_position++] = (char)_currentChar;
}
private Reader _reader;
// buffer parameters
private char[] _buffer = new char[1024];
private int _position;
private int _end;
// type of token (it will be a CSSLexicalUnits constant)
private int _type;
// current character. -1 means EOF
private int _currentChar;
}
/**
* constants that we use to keep track of what type of token of the css file
* we have parsed.
*/
private static class CSSLexicalUnits
{
public static final int EOF = 0;
public static final int LEFT_CURLY_BRACE = 1;
public static final int RIGHT_CURLY_BRACE = 2;
public static final int SPACE = 3;
public static final int COMMENT = 4;
public static final int AT_KEYWORD = 5;
}
// this is the pattern for finding comments. We want to strip out
// comments from the properties, and we use this pattern to do it.
private static final Pattern _COMMENT_PATTERN =
Pattern.compile("(?s)/\\*.*?\\*/");
private static final String[] _EMPTY_STRING_ARRAY = new String[0];
private static final TrinidadLogger _LOG =
TrinidadLogger.createTrinidadLogger(SkinCSSParser.class);
private static final String _BASE_64 = ";base64";
private static final String _BASE_64_REPLACEMENT_TOKEN = "#base64";
}