blob: 148a95caf736fbaa1f42b0a030e97628fbcda0aa [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 flash.css;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import flash.fonts.FontManager;
import flash.localization.LocalizationManager;
import flash.util.FileUtils;
import flash.util.Trace;
import flex2.compiler.Logger;
import flex2.compiler.util.CompilerMessage;
import flex2.compiler.util.ThreadLocalToolkit;
import org.apache.flex.forks.batik.css.parser.Parser;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.ErrorHandler;
import org.w3c.css.sac.InputSource;
/**
* The class is a wrapper around the Batik CSS Parser. It uses a
* StyleDocumentHandler to handle the Batik CSS Parser's callbacks.
* The Rule instances that StyleDocumentHandler creates and populates
* are handed back to this class and stored in the <code>rules</code>.
* Batik CSS Parser errors and warnings are reported via the passed in
* Logger.
*/
public class StyleParser
{
private int ruleIndex = 0;
private List<Rule> rules = new ArrayList<Rule>();
private String cssPath;
private boolean errorsExist = false;
private String mxmlPath;
private int mxmlLineNumberOffset;
private Logger messageHandler;
private FontManager fontManager;
private Parser parser;
private boolean checkDeprecation;
public StyleParser(String cssPath, InputStream inputStream,
final Logger handler, FontManager fontManager,
boolean checkDeprecation)
{
this.cssPath = cssPath;
this.fontManager = fontManager;
this.checkDeprecation = checkDeprecation;
try
{
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
String charsetName = null;
try
{
charsetName = StyleParser.readCSSCharset(bufferedInputStream);
}
catch (StyleSheetInvalidCharset e)
{
// add filename to exception and log warning.
LocalizationManager l10n = ThreadLocalToolkit.getLocalizationManager();
String message = l10n.getLocalizedTextString(
new StyleSheetInvalidCharset(cssPath, e.charsetName));
handler.logError(message);
return;
}
FileUtils.consumeBOM(bufferedInputStream, charsetName);
init(new InputStreamReader(bufferedInputStream, charsetName), handler);
}
catch (IOException ioException)
{
handler.logError(cssPath, -1, ioException.getLocalizedMessage());
if (Trace.css || Trace.error)
{
ioException.printStackTrace();
}
}
}
public StyleParser(String mxmlPath, int mxmlLineNumber, Reader reader,
final Logger handler, FontManager fontManager, boolean checkDeprecation)
{
this.mxmlPath = mxmlPath;
// preilly: We subtract one here, because if we have the following:
//
// 1 <mx:Application>
// 2 <mx:Style>
// 3 Application { backgroundColor: red }
// 4 </mx:Style>
// 5 </mx:Application>
//
// the mxmlLineNumber passed in will be 2 and the "Application" Selector's line
// number will be 2 as far as the CSS Parser is concerned, so if we want to report
// the "Application" Selector's line number in the mxml file, we need to subtract
// 1 after adding the two line numbers, ie 2 + 2 - 1 = 3.
this.mxmlLineNumberOffset = mxmlLineNumber - 1;
this.fontManager = fontManager;
this.checkDeprecation = checkDeprecation;
init(reader, handler);
}
public StyleParser(String cssPath, Reader reader, final Logger handler,
FontManager fontManager, boolean checkDeprecation)
{
this.cssPath = cssPath;
this.fontManager = fontManager;
this.checkDeprecation = checkDeprecation;
init(reader, handler);
}
private void init(Reader reader, final Logger handler)
{
try
{
messageHandler = handler; //PJF: Batik SAC needs a better error interface for DocumentHandlers!
ErrorHandler errorHandler = new ErrorHandler() {
public void error(CSSParseException exception) throws CSSException
{
if (mxmlPath != null)
{
handler.logError(mxmlPath, exception.getLineNumber(),
StyleParserErrorTranslator.getUserFriendlyErrror(exception.getMessage()));
errorsExist = true;
}
else
{
handler.logError(cssPath, exception.getLineNumber(), StyleParserErrorTranslator.getUserFriendlyErrror(exception.getMessage()));
errorsExist = true;
}
}
public void fatalError(CSSParseException exception) throws CSSException
{
if (mxmlPath != null)
{
handler.logError(mxmlPath, exception.getLineNumber(),
StyleParserErrorTranslator.getUserFriendlyErrror(exception.getMessage()));
errorsExist = true;
}
else
{
handler.logError(cssPath, exception.getLineNumber(), StyleParserErrorTranslator.getUserFriendlyErrror(exception.getMessage()));
errorsExist = true;
}
}
public void warning(CSSParseException exception) throws CSSException
{
if (mxmlPath != null)
{
handler.logWarning(mxmlPath, exception.getLineNumber(),
StyleParserErrorTranslator.getUserFriendlyErrror(exception.getMessage()));
}
else
{
handler.logWarning(cssPath, exception.getLineNumber(), StyleParserErrorTranslator.getUserFriendlyErrror(exception.getMessage()));
}
}
};
parser = new Parser();
parser.setLineNumberOffset(mxmlLineNumberOffset);
parser.setDocumentHandler( new StyleDocumentHandler(this) );
parser.setErrorHandler(errorHandler);
parser.parseStyleSheet(new InputSource(reader));
}
catch (Exception exception)
{
String path;
if (mxmlPath != null)
{
path = mxmlPath;
}
else
{
path = cssPath;
}
handler.logError(path, -1, StyleParserErrorTranslator.getUserFriendlyErrror(exception.getLocalizedMessage()));
if (Trace.css || Trace.error)
{
exception.printStackTrace();
}
}
}
public boolean errorsExist()
{
return errorsExist;
}
// preilly: Be careful using this method, because in some cases,
// org.apache.flex.forks.batik.css.parser.Parser calls nextIgnoreSpaces() before calling handler
// functions, so the line number could have been advanced one or more times. We store
// line numbers in CSSLexicalUnit's and DefaultElementSelector's when they are
// created, so if possible get the line number from there instead of here.
public int getLineNumber()
{
return parser.getLineNumber();
}
int getMxmlLineNumber()
{
return mxmlLineNumberOffset;
}
public String getPath()
{
String path = mxmlPath;
if (path == null)
{
path = cssPath;
}
return path;
}
public void addRule(Rule rule)
{
rules.add(rule);
}
public List<Rule> getRules()
{
return rules;
}
public Rule parseRule()
{
return rules.get(ruleIndex++);
}
public FontManager getFontManager()
{
return fontManager;
}
public void warnDeprecation(String deprecated, String replacement, int lineNumber)
{
if (checkDeprecation)
{
LocalizationManager l10n = ThreadLocalToolkit.getLocalizationManager();
String message = l10n.getLocalizedTextString(new DeprecatedWarning(deprecated, replacement, "3.0"));
if (mxmlPath != null)
{
messageHandler.logWarning(mxmlPath, lineNumber, message);
}
else
{
messageHandler.logWarning(cssPath, lineNumber, message);
}
}
}
public void warning(CSSException cssException)
{
if ((Trace.css || Trace.error) && cssException.getException() != null)
{
cssException.getException().printStackTrace();
}
if (mxmlPath != null)
{
int lineNumber = parser.getLineNumber();
messageHandler.logWarning(mxmlPath, lineNumber,
StyleParserErrorTranslator.getUserFriendlyErrror(cssException.getMessage()));
}
else
{
int lineNumber = parser.getLineNumber();
messageHandler.logWarning(cssPath, lineNumber,
StyleParserErrorTranslator.getUserFriendlyErrror(cssException.getMessage()));
}
}
public void error(CSSException cssException)
{
if ((Trace.css || Trace.error) && cssException.getException() != null)
{
cssException.getException().printStackTrace();
}
if (mxmlPath != null)
{
int lineNumber = parser.getLineNumber();
messageHandler.logError(mxmlPath, lineNumber,
StyleParserErrorTranslator.getUserFriendlyErrror(cssException.getMessage()));
}
else
{
int lineNumber = parser.getLineNumber();
messageHandler.logError(cssPath, lineNumber,
StyleParserErrorTranslator.getUserFriendlyErrror(cssException.getMessage()));
}
}
/**
* Discover the file encoding of a CSS file by reading the first few bytes of the file.
* If a charset rule or a BOM is not present then assume the file is UTF-8.
*
* see http://www.w3.org/TR/CSS21/syndata.html#charset.
*
* Limitations: UTF-16 encoding is only detect by the presence of a BOM.
* Charset rule not read if a UTF-16 BOM is found.
*
*
* @param in - must be positioned at the beginning of the file.
* @return charset encoding of the file
* @throws IOException
* @throws StyleSheetInvalidCharset - if the encoding specified in the charset rule
* is invalid or not supported.
*
*/
public static String readCSSCharset(BufferedInputStream in) throws IOException, StyleSheetInvalidCharset
{
// The leading bytes of the file can tell us the BOM and if a @charset rule
// is present.
byte[][] leadingBytes = { {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF, // utf-8 bom, as specified
0x40, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65,
0x74, 0x20, 0x22},
{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}, // utf-8
{0x40, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // ascii, as specified
0x74, 0x20, 0x22},
{(byte) 0xFE, (byte) 0xFF}, // UTF-16BE
{(byte) 0xFF, (byte) 0xFE} // UTF-16LE
};
// matching array of charsets for leadingBytes array.
// "specified" means an @charset rule was found as we need to read the charset
// encoding.
// NOTE: We don't attempt to read charset rules for UTF-16 formats. This is a
// extra work and we know the charset must be UTF-16 if we were able
// to read the @charset rule. Does not seem worth the effort since if we read
// it we could only throw out the css file if the encoding was not valid.
String[] charsetTable = { "specified", "UTF-8", "specified",
"UTF-16BE", "UTF-16LE"};
String charset = "UTF-8"; // default return value
int maxCharsetName = 40;
int maxBytesToRead = 2 + ((12 + maxCharsetName)); // calculated for 8 bit and 16 bit cases only
in.mark(maxBytesToRead + 1);
int found = -1; // index into leadingBytes if there is a match
byte[] buffer = new byte[maxBytesToRead];
// read bytes
int results = in.read(buffer); // max number of bytes read to determine charset
if (results == -1) {
return charset;
}
// find a match
for (int i = 0; i < leadingBytes.length; i++) {
byte[] bytes = leadingBytes[i];
found = i;
for (int j = 0; j < bytes.length; j++) {
if (bytes[j] != buffer[j]) {
found = -1;
break;
}
}
if (found != -1) {
break;
}
}
// if found a match, then look more closely
if (found != -1)
{
if ("specified".equals(charsetTable[found]))
{
// read specified charset followed by the bytes 22 (quote), 3B
// (semicolon)
int charsetIndex = leadingBytes[found].length;
int charsetLength = 0;
int quotePosition = -1;
for (int i = leadingBytes[found].length; i < buffer.length; i++)
{
if (buffer[i] == 0x22)
{ // end quotes of
quotePosition = i;
break;
}
charsetLength++;
}
// if found a quote followed by a semicolon
if (quotePosition > 0 && (quotePosition + 1) < buffer.length
&& buffer[quotePosition + 1] == 0x3B)
{
// get specified name and test it.
String testCharset = new String(buffer, charsetIndex,
charsetLength, "US-ASCII");
try
{
// must be able to read in @charset rule using encoding
in.reset();
InputStreamReader reader = new InputStreamReader(in,
testCharset);
char[] chBuf = new char[2 + 12 + charsetLength]; // bom + @charset ""; + charset
reader.read(chBuf);
String specifiedCharaset = new String(chBuf);
if (specifiedCharaset.indexOf(testCharset) == -1)
{
in.reset();
throw new StyleSheetInvalidCharset("", testCharset);
} else
{
charset = testCharset; // found valid charset
}
} catch (UnsupportedEncodingException e)
{
in.reset();
throw new StyleSheetInvalidCharset("", testCharset);
}
}
} else
{
charset = charsetTable[found];
}
}
in.reset();
return charset;
}
public static class DeprecatedWarning extends CompilerMessage.CompilerWarning
{
private static final long serialVersionUID = 3832979911761729776L;
public DeprecatedWarning(String var, String replacement, String since)
{
this.var = var;
this.replacement = replacement;
this.since = since;
}
public final String var, replacement, since;
}
public static class StyleSheetInvalidCharset extends CompilerMessage.CompilerWarning
{
private static final long serialVersionUID = -4363998590309962624L;
public StyleSheetInvalidCharset(String stylePath, String charsetName)
{
super();
this.stylePath = stylePath;
this.charsetName = charsetName;
}
public final String stylePath;
public final String charsetName;
}
}