| /* |
| * |
| * 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; |
| } |
| } |