Mercurial > hg > blitz_condensed
diff src/com/go/trove/util/PropertyParser.java @ 0:3dc0c5604566
Initial checkin of blitz 2.0 fcs - no installer yet.
author | Dan Creswell <dan.creswell@gmail.com> |
---|---|
date | Sat, 21 Mar 2009 11:00:06 +0000 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/com/go/trove/util/PropertyParser.java Sat Mar 21 11:00:06 2009 +0000 @@ -0,0 +1,862 @@ +/* ==================================================================== + * Trove - Copyright (c) 1997-2000 Walt Disney Internet Group + * ==================================================================== + * The Tea Software License, Version 1.1 + * + * Copyright (c) 2000 Walt Disney Internet Group. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Walt Disney Internet Group (http://opensource.go.com/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Tea", "TeaServlet", "Kettle", "Trove" and "BeanDoc" must + * not be used to endorse or promote products derived from this + * software without prior written permission. For written + * permission, please contact opensource@dig.com. + * + * 5. Products derived from this software may not be called "Tea", + * "TeaServlet", "Kettle" or "Trove", nor may "Tea", "TeaServlet", + * "Kettle", "Trove" or "BeanDoc" appear in their name, without prior + * written permission of the Walt Disney Internet Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE WALT DISNEY INTERNET GROUP OR ITS + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * For more information about Tea, please see http://opensource.go.com/. + */ + +package com.go.trove.util; + +import java.util.*; +import java.io.InputStream; +import java.io.Reader; +import java.io.IOException; +import java.io.Serializable; +import com.go.trove.io.SourceInfo; +import com.go.trove.io.SourceReader; + +/****************************************************************************** + * Parses a properties file similar to how {@link java.util.Properties} does, + * except: + * + * <ul> + * <li>Values have trailing whitespace trimmed. + * <li>Quotation marks ( " or ' ) can be used to define keys and values that + * have embedded spaces. + * <li>Quotation marks can also be used to define multi-line keys and values + * without having to use continuation characters. + * <li>Properties may be nested using braces '{' and '}'. + * </ul> + * + * Just like Properties, comment lines start with optional whitespace followed + * by a '#' or '!'. Keys and values may have an optional '=' or ':' as a + * separator, unicode escapes are supported as well as other common escapes. + * A line may end in a backslash so that it continues to the next line. + * Escapes for brace characters '{' and '}' are also supported. + * + * Example: + * + * <pre> + * # Properties file + * + * foo = bar + * foo.sub = blink + * empty + * + * block { + * inner { + * foo = bar + * item + * } + * next.item = "true" + * } + * + * section = test { + * level = 4 + * message = "Message: " + * } + * </pre> + * + * is equivalent to + * + * <pre> + * # Properties file + * + * foo = bar + * foo.sub = blink + * empty + * + * block.inner.foo = bar + * block.inner.item + * block.next.item = true + * + * section = test + * section.level = 4 + * section.message = Message: + * </pre> + * + * @author Brian S O'Neill + * @version + * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $--> + */ +public class PropertyParser { + // Parsed grammer (EBNF) is: + // + // Properties ::= { PropertyList } + // PropertyList ::= { Property | COMMENT } + // Property ::= KEY [ VALUE ] [ Block ] + // Block ::= LBRACE PropertyList RBRACE + + private Map mMap; + + private Vector mListeners = new Vector(1); + private int mErrorCount = 0; + + private Scanner mScanner; + + /** + * @param map Map to receive properties + */ + public PropertyParser(Map map) { + mMap = map; + } + + public void addErrorListener(ErrorListener listener) { + mListeners.addElement(listener); + } + + public void removeErrorListener(ErrorListener listener) { + mListeners.removeElement(listener); + } + + private void dispatchParseError(ErrorEvent e) { + mErrorCount++; + + synchronized (mListeners) { + for (int i = 0; i < mListeners.size(); i++) { + ((ErrorListener)mListeners.elementAt(i)).parseError(e); + } + } + } + + private void error(String str, SourceInfo info) { + dispatchParseError(new ErrorEvent(this, str, info)); + } + + private void error(String str, Token token) { + error(str, token.getSourceInfo()); + } + + /** + * Parses properties from the given reader and stores them in the Map. To + * capture any parsing errors, call addErrorListener prior to parsing. + */ + public void parse(Reader reader) throws IOException { + mScanner = new Scanner(reader); + + mScanner.addErrorListener(new ErrorListener() { + public void parseError(ErrorEvent e) { + dispatchParseError(e); + } + }); + + try { + parseProperties(); + } + finally { + mScanner.close(); + } + } + + private void parseProperties() throws IOException { + Token token; + while ((token = peek()).getId() != Token.EOF) { + switch (token.getId()) { + + case Token.KEY: + case Token.LBRACE: + case Token.COMMENT: + parsePropertyList(null); + break; + + case Token.RBRACE: + token = read(); + error("No matching left brace", token); + break; + + default: + token = read(); + error("Unexpected token: " + token.getValue(), token); + break; + } + } + } + + private void parsePropertyList(String keyPrefix) throws IOException { + Token token; + + loop: + while ((token = peek()).getId() != Token.EOF) { + switch (token.getId()) { + + case Token.KEY: + token = read(); + parseProperty(keyPrefix, token); + break; + + case Token.COMMENT: + read(); + break; + + case Token.LBRACE: + read(); + error("Nested properties must have a base name", token); + parseBlock(keyPrefix); + break; + + default: + break loop; + } + } + } + + private void parseProperty(String keyPrefix, Token token) + throws IOException { + + String key = token.getValue(); + if (keyPrefix != null) { + key = keyPrefix + key; + } + + String value = null; + + if (peek().getId() == Token.VALUE) { + token = read(); + value = token.getValue(); + } + + if (peek().getId() == Token.LBRACE) { + read(); + parseBlock(key + '.'); + } + else if (value == null) { + value = ""; + } + + if (value != null) { + putProperty(key, value, token); + } + } + + // When this is called, the LBRACE token has already been read. + private void parseBlock(String keyPrefix) throws IOException { + parsePropertyList(keyPrefix); + + Token token; + if ((token = peek()).getId() == Token.RBRACE) { + read(); + } + else { + error("Right brace expected", token); + } + } + + private void putProperty(String key, String value, Token token) { + if (mMap.containsKey(key)) { + error("Property \"" + key + "\" already defined", token); + } + mMap.put(key, value); + } + + /** + * Total number of errors accumulated by this PropertyParser instance. + */ + public int getErrorCount() { + return mErrorCount; + } + + private Token read() throws IOException { + return mScanner.readToken(); + } + + private Token peek() throws IOException { + return mScanner.peekToken(); + } + + private void unread(Token token) throws IOException { + mScanner.unreadToken(token); + } + + /************************************************************************** + * + * @author Brian S O'Neill + * @version + * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $--> + */ + public static interface ErrorListener extends java.util.EventListener { + public void parseError(ErrorEvent e); + } + + /************************************************************************** + * + * @author Brian S O'Neill + * @version + * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $--> + */ + public static class ErrorEvent extends java.util.EventObject { + private String mErrorMsg; + private SourceInfo mInfo; + + ErrorEvent(Object source, String errorMsg, SourceInfo info) { + super(source); + mErrorMsg = errorMsg; + mInfo = info; + } + + public String getErrorMessage() { + return mErrorMsg; + } + + /** + * Returns the error message prepended with source file information. + */ + public String getDetailedErrorMessage() { + String prepend = getSourceInfoMessage(); + if (prepend == null || prepend.length() == 0) { + return mErrorMsg; + } + else { + return prepend + ": " + mErrorMsg; + } + } + + public String getSourceInfoMessage() { + if (mInfo == null) { + return ""; + } + else { + return String.valueOf(mInfo.getLine()); + } + } + + /** + * This method reports on where in the source code an error was found. + * + * @return Source information on this error or null if not known. + */ + public SourceInfo getSourceInfo() { + return mInfo; + } + } + + /************************************************************************** + * + * @author Brian S O'Neill + * @version + * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $--> + */ + private static class Token implements java.io.Serializable { + public final static int UNKNOWN = 0; + public final static int EOF = 1; + + public final static int COMMENT = 2; + public final static int KEY = 3; + public final static int VALUE = 4; + + public final static int LBRACE = 5; + public final static int RBRACE = 6; + + private final static int LAST_ID = 6; + + private int mTokenId; + private String mValue; + private SourceInfo mInfo; + + Token(int sourceLine, + int sourceStartPos, + int sourceEndPos, + int tokenId, + String value) { + + mTokenId = tokenId; + mValue = value; + + if (tokenId > LAST_ID) { + throw new IllegalArgumentException("Token Id out of range: " + + tokenId); + } + + mInfo = new SourceInfo(sourceLine, sourceStartPos, sourceEndPos); + + if (sourceStartPos > sourceEndPos) { + // This is an internal error. + throw new IllegalArgumentException + ("Token start position greater than " + + "end position at line: " + sourceLine); + } + } + + public Token(SourceInfo info, int tokenId, String value) { + mTokenId = tokenId; + + if (tokenId > LAST_ID) { + throw new IllegalArgumentException("Token Id out of range: " + + tokenId); + } + + mInfo = info; + } + + public final int getId() { + return mTokenId; + } + + /** + * Token code is non-null, and is exactly the same as the name for + * its Id. + */ + public String getCode() { + return Code.TOKEN_CODES[mTokenId]; + } + + public final SourceInfo getSourceInfo() { + return mInfo; + } + + public String getValue() { + return mValue; + } + + public String toString() { + StringBuffer buf = new StringBuffer(10); + + String image = getCode(); + + if (image != null) { + buf.append(image); + } + + String str = getValue(); + + if (str != null) { + if (image != null) { + buf.append(' '); + } + buf.append('"'); + buf.append(str); + buf.append('"'); + } + + return buf.toString(); + } + + private static class Code { + public static final String[] TOKEN_CODES = + { + "UNKNOWN", + "EOF", + + "COMMENT", + "KEY", + "VALUE", + + "LBRACE", + "RBRACE", + }; + } + } + + /************************************************************************** + * + * @author Brian S O'Neill + * @version + * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $--> + */ + private static class Scanner { + private SourceReader mSource; + + /** The scanner supports any amount of lookahead. */ + private Stack mLookahead = new Stack(); + + private boolean mScanKey = true; + private Token mEOFToken; + + private Vector mListeners = new Vector(1); + private int mErrorCount = 0; + + public Scanner(Reader in) { + mSource = new SourceReader(in, null, null); + } + + public void addErrorListener(ErrorListener listener) { + mListeners.addElement(listener); + } + + public void removeErrorListener(ErrorListener listener) { + mListeners.removeElement(listener); + } + + private void dispatchParseError(ErrorEvent e) { + mErrorCount++; + + synchronized (mListeners) { + for (int i = 0; i < mListeners.size(); i++) { + ((ErrorListener)mListeners.elementAt(i)).parseError(e); + } + } + } + + private void error(String str, SourceInfo info) { + dispatchParseError(new ErrorEvent(this, str, info)); + } + + private void error(String str) { + error(str, new SourceInfo(mSource.getLineNumber(), + mSource.getStartPosition(), + mSource.getEndPosition())); + } + + /** + * Returns EOF as the last token. + */ + public synchronized Token readToken() throws IOException { + if (mLookahead.empty()) { + return scanToken(); + } + else { + return (Token)mLookahead.pop(); + } + } + + /** + * Returns EOF as the last token. + */ + public synchronized Token peekToken() throws IOException { + if (mLookahead.empty()) { + return (Token)mLookahead.push(scanToken()); + } + else { + return (Token)mLookahead.peek(); + } + } + + public synchronized void unreadToken(Token token) throws IOException { + mLookahead.push(token); + } + + public void close() throws IOException { + mSource.close(); + } + + public int getErrorCount() { + return mErrorCount; + } + + private Token scanToken() throws IOException { + if (mSource.isClosed()) { + if (mEOFToken == null) { + mEOFToken = makeToken(Token.EOF, null); + } + + return mEOFToken; + } + + int c; + int peek; + + int startPos; + + while ( (c = mSource.read()) != -1 ) { + switch (c) { + + case SourceReader.ENTER_CODE: + case SourceReader.ENTER_TEXT: + continue; + + case '#': + case '!': + mScanKey = true; + return scanComment(); + + case '{': + mScanKey = true; + return makeToken(Token.LBRACE, "{"); + case '}': + mScanKey = true; + return makeToken(Token.RBRACE, "}"); + + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': + case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': + case 'z': case '.': + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': + case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': + case 'Z': case '_': + mSource.unread(); + return scanKeyOrValue(); + + case '\n': + mScanKey = true; + // fall through + case ' ': + case '\0': + case '\t': + continue; + + default: + if (Character.isWhitespace((char)c)) { + continue; + } + else { + mSource.unread(); + return scanKeyOrValue(); + } + } + } + + if (mEOFToken == null) { + mEOFToken = makeToken(Token.EOF, null); + } + + return mEOFToken; + } + + private Token scanKeyOrValue() throws IOException { + StringBuffer buf = new StringBuffer(40); + boolean trim = true; + + int startLine = mSource.getLineNumber(); + int startPos = mSource.getStartPosition(); + int endPos = mSource.getEndPosition(); + + boolean skipWhitespace = true; + boolean skipSeparator = true; + + int c; + loop: + while ( (c = mSource.read()) != -1 ) { + switch (c) { + + case '\n': + mSource.unread(); + break loop; + + case '\\': + int next = mSource.read(); + if (next == -1 || next == '\n') { + // line continuation + skipWhitespace = true; + continue; + } + + c = processEscape(c, next); + skipWhitespace = false; + break; + + case '{': + case '}': + mSource.unread(); + break loop; + + case '=': + case ':': + if (mScanKey) { + mSource.unread(); + break loop; + } + else if (skipSeparator) { + skipSeparator = false; + continue; + } + skipWhitespace = false; + break; + + case '\'': + case '"': + if (buf.length() == 0) { + scanStringLiteral(c, buf); + endPos = mSource.getEndPosition(); + trim = false; + break loop; + } + // fall through + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': + case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': + case 'z': case '.': + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': + case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': + case 'Z': case '_': + skipWhitespace = false; + break; + + case ' ': + case '\0': + case '\t': + if (skipWhitespace) { + continue; + } + if (mScanKey) { + break loop; + } + break; + + default: + if (Character.isWhitespace((char)c)) { + if (skipWhitespace) { + continue; + } + if (mScanKey) { + break loop; + } + } + else { + skipWhitespace = false; + } + break; + } + + buf.append((char)c); + endPos = mSource.getEndPosition(); + skipSeparator = false; + } + + int tokenId; + if (mScanKey) { + tokenId = Token.KEY; + mScanKey = false; + } + else { + tokenId = Token.VALUE; + mScanKey = true; + } + + String value = buf.toString(); + + if (trim) { + value = value.trim(); + } + + return new Token(startLine, startPos, endPos, tokenId, value); + } + + private Token scanComment() throws IOException { + StringBuffer buf = new StringBuffer(40); + + int startLine = mSource.getLineNumber(); + int startPos = mSource.getStartPosition(); + int endPos = mSource.getEndPosition(); + + int c; + while ( (c = mSource.peek()) != -1 ) { + if (c == '\n') { + break; + } + + mSource.read(); + buf.append((char)c); + + endPos = mSource.getEndPosition(); + } + + return new Token(startLine, startPos, endPos, + Token.COMMENT, buf.toString()); + } + + private void scanStringLiteral(int quote, StringBuffer buf) + throws IOException { + + int c; + while ( (c = mSource.read()) != -1 ) { + if (c == quote) { + return; + } + + if (c == '\\') { + int next = mSource.read(); + if (next == -1 || next == '\n') { + // line continuation + continue; + } + c = processEscape(c, next); + } + + buf.append((char)c); + } + } + + private int processEscape(int c, int next) { + switch (next) { + case '0': + return '\0'; + case 't': + return '\t'; + case 'n': + return '\n'; + case 'f': + return '\f'; + case 'r': + return '\r'; + + case '\\': + case '\'': + case '\"': + case '=': + case ':': + case '{': + case '}': + return next; + + default: + error("Invalid escape code: \\" + (char)next); + return next; + } + } + + private Token makeToken(int Id, String value) { + return new Token(mSource.getLineNumber(), + mSource.getStartPosition(), + mSource.getEndPosition(), + Id, value); + } + } +}