import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.apache.poi.poifs.filesystem.Entry;
/**
return _calendar;
}
+ /**
+ * Returns a SimpleDateFormat for the given format string which is
+ * configured with a compatible Calendar instance (see
+ * {@link DatabaseBuilder#toCompatibleCalendar}) and this database's
+ * {@link TimeZone}.
+ */
+ public SimpleDateFormat createDateFormat(String formatStr) {
+ SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr);
+ sdf.setTimeZone(getTimeZone());
+ return sdf;
+ }
+
/**
* @returns the current handler for reading/writing properties, creating if
* necessary
--- /dev/null
+/*
+Copyright (c) 2016 James Ahlborn
+
+Licensed 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 com.healthmarketscience.jackcess.util;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.healthmarketscience.jackcess.DatabaseBuilder;
+import com.healthmarketscience.jackcess.impl.DatabaseImpl;
+import static com.healthmarketscience.jackcess.util.Expressionator.*;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+class ExpressionTokenizer
+{
+ private static final int EOF = -1;
+ private static final char QUOTED_STR_CHAR = '"';
+ private static final char OBJ_NAME_START_CHAR = '[';
+ private static final char OBJ_NAME_END_CHAR = ']';
+ private static final char DATE_LIT_DELIM_CHAR = '#';
+ private static final char EQUALS_CHAR = '=';
+
+ private static final String DATE_FORMAT = "M/d/yyyy";
+ private static final String TIME_FORMAT = "HH:mm:ss";
+ private static final String DATE_TIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT;
+
+ private static final byte IS_OP_FLAG = 0x01;
+ private static final byte IS_COMP_FLAG = 0x02;
+ private static final byte IS_DELIM_FLAG = 0x04;
+ private static final byte IS_SPACE_FLAG = 0x08;
+ private static final byte IS_QUOTE_FLAG = 0x10;
+
+ enum TokenType {
+ OBJ_NAME, LITERAL, OP, STRING, SPACE;
+ }
+
+ private static final byte[] CHAR_FLAGS = new byte[128];
+ private static final Set<String> TWO_CHAR_COMP_OPS = new HashSet<String>(
+ Arrays.asList("<=", ">=", "<>"));
+
+ static {
+ setCharFlag(IS_OP_FLAG, '+', '-', '*', '/', '\\', '^', '&');
+ setCharFlag(IS_COMP_FLAG, '<', '>', '=');
+ setCharFlag(IS_DELIM_FLAG, '.', '!', ',', '(', ')');
+ setCharFlag(IS_SPACE_FLAG, ' ', '\n', '\r', '\t');
+ setCharFlag(IS_QUOTE_FLAG, '"', '#', '[', ']');
+ }
+
+ /**
+ * Tokenizes an expression string of the given type and (optionally) in the
+ * context of the relevant database.
+ */
+ static List<Token> tokenize(Type exprType, String exprStr, DatabaseImpl db) {
+
+ if(exprStr != null) {
+ exprStr = exprStr.trim();
+ }
+
+ if((exprStr == null) || (exprStr.length() == 0)) {
+ return null;
+ }
+
+ List<Token> tokens = new ArrayList<Token>();
+
+ ExprBuf buf = new ExprBuf(exprStr);
+
+ while(buf.hasNext()) {
+ char c = buf.next();
+
+ byte charFlag = getCharFlag(c);
+ if(charFlag != 0) {
+
+ // what could it be?
+ switch(charFlag) {
+ case IS_OP_FLAG:
+ // special case '-' for negative number
+ Object numLit = maybeParseNumberLiteral(c, buf);
+ if(numLit != null) {
+ tokens.add(new Token(TokenType.LITERAL, numLit));
+ continue;
+ }
+
+ // all simple operator chars are single character operators
+ tokens.add(new Token(TokenType.OP, String.valueOf(c)));
+ break;
+ case IS_COMP_FLAG:
+
+ switch(exprType) {
+ case DEFAULT_VALUE:
+
+ // special case
+ if((c == EQUALS_CHAR) && (buf.prevPos() == 0)) {
+ // a leading equals sign indicates how a default value should be
+ // evaluated
+ tokens.add(new Token(TokenType.OP, String.valueOf(c)));
+ continue;
+ }
+ // def values can't have cond at top level
+ throw new IllegalArgumentException(
+ exprType + " cannot have top-level conditional");
+ case FIELD_VALIDATOR:
+ tokens.add(new Token(TokenType.OP, parseCompOp(c, buf)));
+ break;
+ }
+
+ break;
+
+ case IS_DELIM_FLAG:
+
+ // all delimiter chars are single character operators
+ tokens.add(new Token(TokenType.OP, String.valueOf(c)));
+ break;
+
+ case IS_SPACE_FLAG:
+
+ // normalize whitespace into single space
+ consumeWhitespace(buf);
+ tokens.add(new Token(TokenType.SPACE, " "));
+ break;
+
+ case IS_QUOTE_FLAG:
+
+ switch(c) {
+ case QUOTED_STR_CHAR:
+ tokens.add(new Token(TokenType.LITERAL, parseQuotedString(buf)));
+ break;
+ case DATE_LIT_DELIM_CHAR:
+ tokens.add(new Token(TokenType.LITERAL,
+ parseDateLiteralString(buf, db)));
+ break;
+ case OBJ_NAME_START_CHAR:
+ tokens.add(new Token(TokenType.OBJ_NAME, parseObjNameString(buf)));
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid leading quote character " + c);
+ }
+
+ break;
+
+ default:
+ throw new RuntimeException("unknown char flag " + charFlag);
+ }
+
+ } else {
+
+ if(isDigit(c)) {
+ Object numLit = maybeParseNumberLiteral(c, buf);
+ if(numLit != null) {
+ tokens.add(new Token(TokenType.LITERAL, numLit));
+ continue;
+ }
+ }
+
+ // standalone word of some sort
+ String str = parseBareString(c, buf, exprType);
+ tokens.add(new Token(TokenType.STRING, str));
+ }
+
+ }
+
+ return tokens;
+ }
+
+ private static byte getCharFlag(char c) {
+ return ((c < 128) ? CHAR_FLAGS[c] : 0);
+ }
+
+ private static boolean isSpecialChar(char c) {
+ return (getCharFlag(c) != 0);
+ }
+
+ private static String parseCompOp(char firstChar, ExprBuf buf) {
+ String opStr = String.valueOf(firstChar);
+
+ int c = buf.peekNext();
+ if((c != EOF) && hasFlag(getCharFlag((char)c), IS_COMP_FLAG)) {
+
+ // is the combo a valid comparison operator?
+ String tmpStr = opStr + (char)c;
+ if(TWO_CHAR_COMP_OPS.contains(tmpStr)) {
+ opStr = tmpStr;
+ buf.next();
+ }
+ }
+
+ return opStr;
+ }
+
+ private static void consumeWhitespace(ExprBuf buf) {
+ int c = EOF;
+ while(((c = buf.peekNext()) != EOF) &&
+ hasFlag(getCharFlag((char)c), IS_SPACE_FLAG)) {
+ buf.next();
+ }
+ }
+
+ private static String parseBareString(char firstChar, ExprBuf buf,
+ Type exprType) {
+ StringBuilder sb = buf.getScratchBuffer().append(firstChar);
+
+ byte stopFlags = (IS_OP_FLAG | IS_DELIM_FLAG | IS_SPACE_FLAG);
+ if(exprType == Type.FIELD_VALIDATOR) {
+ stopFlags |= IS_COMP_FLAG;
+ }
+
+ while(buf.hasNext()) {
+ char c = buf.next();
+ byte charFlag = getCharFlag(c);
+ if(hasFlag(charFlag, stopFlags)) {
+ buf.popPrev();
+ break;
+ }
+ sb.append(c);
+ }
+
+ return sb.toString();
+ }
+
+ private static String parseQuotedString(ExprBuf buf) {
+ StringBuilder sb = buf.getScratchBuffer();
+
+ boolean complete = false;
+ while(buf.hasNext()) {
+ char c = buf.next();
+ if(c == QUOTED_STR_CHAR) {
+ int nc = buf.peekNext();
+ if(nc == QUOTED_STR_CHAR) {
+ sb.append(QUOTED_STR_CHAR);
+ buf.next();
+ } else {
+ complete = true;
+ break;
+ }
+ }
+
+ sb.append(c);
+ }
+
+ if(!complete) {
+ throw new IllegalArgumentException("Missing closing '" + QUOTED_STR_CHAR +
+ "' for quoted string");
+ }
+
+ return sb.toString();
+ }
+
+ private static String parseObjNameString(ExprBuf buf) {
+ return parseStringUntil(buf, OBJ_NAME_END_CHAR);
+ }
+
+ private static String parseStringUntil(ExprBuf buf, char endChar) {
+ StringBuilder sb = buf.getScratchBuffer();
+
+ boolean complete = false;
+ while(buf.hasNext()) {
+ char c = buf.next();
+ if(c == endChar) {
+ complete = true;
+ break;
+ }
+
+ sb.append(c);
+ }
+
+ if(!complete) {
+ throw new IllegalArgumentException("Missing closing '" + endChar +
+ "' for quoted string");
+ }
+
+ return sb.toString();
+ }
+
+ private static Date parseDateLiteralString(ExprBuf buf, DatabaseImpl db) {
+ String dateStr = parseStringUntil(buf, DATE_LIT_DELIM_CHAR);
+
+ boolean hasDate = (dateStr.indexOf('/') >= 0);
+ boolean hasTime = (dateStr.indexOf(':') >= 0);
+
+ SimpleDateFormat sdf = null;
+ if(hasDate && hasTime) {
+ sdf = buf.getDateTimeFormat(db);
+ } else if(hasDate) {
+ sdf = buf.getDateFormat(db);
+ } else if(hasTime) {
+ sdf = buf.getTimeFormat(db);
+ } else {
+ throw new IllegalArgumentException("Invalid date time literal " + dateStr);
+ }
+
+ // FIXME, do we need to know which "type" it was?
+ try {
+ return sdf.parse(dateStr);
+ } catch(ParseException pe) {
+ throw new IllegalArgumentException(
+ "Invalid date time literal " + dateStr, pe);
+ }
+ }
+
+ private static Object maybeParseNumberLiteral(char firstChar, ExprBuf buf) {
+ StringBuilder sb = buf.getScratchBuffer().append(firstChar);
+ boolean hasDigit = isDigit(firstChar);
+
+ int startPos = buf.curPos();
+ boolean foundNum = false;
+
+ try {
+
+ int c = EOF;
+ while((c = buf.peekNext()) != EOF) {
+ if(isDigit(c)) {
+ hasDigit = true;
+ sb.append((char)c);
+ buf.next();
+ } else if(c == '.') {
+ sb.append((char)c);
+ buf.next();
+ } else if(isSpecialChar((char)c)) {
+ break;
+ } else {
+ // found a non-number, non-special string
+ return null;
+ }
+ }
+
+ if(!hasDigit) {
+ // no digits, no number
+ return null;
+ }
+
+ String numStr = sb.toString();
+ try {
+ // what number type to use here?
+ BigDecimal num = new BigDecimal(numStr);
+ foundNum = true;
+ return num;
+ } catch(NumberFormatException ne) {
+ throw new IllegalArgumentException(
+ "Invalid number literal " + numStr, ne);
+ }
+
+ } finally {
+ if(!foundNum) {
+ buf.reset(startPos);
+ }
+ }
+ }
+
+ private static boolean hasFlag(byte charFlag, byte flag) {
+ return ((charFlag & flag) != 0);
+ }
+
+ private static void setCharFlag(byte flag, char... chars) {
+ for(char c : chars) {
+ CHAR_FLAGS[c] |= flag;
+ }
+ }
+
+ private static boolean isDigit(int c) {
+ return ((c >= '0') && (c <= '9'));
+ }
+
+ private static final class ExprBuf
+ {
+ private final String _str;
+ private int _pos;
+ private SimpleDateFormat _dateFmt;
+ private SimpleDateFormat _timeFmt;
+ private SimpleDateFormat _dateTimeFmt;
+ private final StringBuilder _scratch = new StringBuilder();
+
+ private ExprBuf(String str) {
+ _str = str;
+ }
+
+ private int len() {
+ return _str.length();
+ }
+
+ public int curPos() {
+ return _pos;
+ }
+
+ public int prevPos() {
+ return _pos - 1;
+ }
+
+ public boolean hasNext() {
+ return _pos < len();
+ }
+
+ public char next() {
+ return _str.charAt(_pos++);
+ }
+
+ public void popPrev() {
+ --_pos;
+ }
+
+ public int peekNext() {
+ if(!hasNext()) {
+ return EOF;
+ }
+ return _str.charAt(_pos);
+ }
+
+ public void reset(int pos) {
+ _pos = pos;
+ }
+
+ public StringBuilder getScratchBuffer() {
+ _scratch.setLength(0);
+ return _scratch;
+ }
+
+ public SimpleDateFormat getDateFormat(DatabaseImpl db) {
+ if(_dateFmt == null) {
+ _dateFmt = newFormat(DATE_FORMAT, db);
+ }
+ return _dateFmt;
+ }
+
+ public SimpleDateFormat getTimeFormat(DatabaseImpl db) {
+ if(_timeFmt == null) {
+ _timeFmt = newFormat(TIME_FORMAT, db);
+ }
+ return _timeFmt;
+ }
+
+ public SimpleDateFormat getDateTimeFormat(DatabaseImpl db) {
+ if(_dateTimeFmt == null) {
+ _dateTimeFmt = newFormat(DATE_TIME_FORMAT, db);
+ }
+ return _dateTimeFmt;
+ }
+
+ private static SimpleDateFormat newFormat(String str, DatabaseImpl db) {
+ return ((db != null) ? db.createDateFormat(str) :
+ DatabaseBuilder.createDateFormat(str));
+ }
+ }
+
+
+ static final class Token
+ {
+ private final TokenType _type;
+ private final Object _val;
+
+ private Token(TokenType type, Object val) {
+ _type = type;
+ _val = val;
+ }
+
+ public TokenType getType() {
+ return _type;
+ }
+
+ public Object getValue() {
+ return _val;
+ }
+
+ @Override
+ public String toString() {
+ if(_type == TokenType.SPACE) {
+ return "' '";
+ }
+ String str = "[" + _type + "] '" + _val + "'";
+ if(_type == TokenType.LITERAL) {
+ str += " (" + _val.getClass() + ")";
+ }
+ return str;
+ }
+ }
+
+}
--- /dev/null
+/*
+Copyright (c) 2016 James Ahlborn
+
+Licensed 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 com.healthmarketscience.jackcess.util;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.healthmarketscience.jackcess.Database;
+import com.healthmarketscience.jackcess.impl.DatabaseImpl;
+import static com.healthmarketscience.jackcess.util.ExpressionTokenizer.Token;
+import static com.healthmarketscience.jackcess.util.ExpressionTokenizer.TokenType;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class Expressionator
+{
+
+ public enum Type {
+ DEFAULT_VALUE, FIELD_VALIDATOR;
+ }
+
+ private enum WordType {
+ OP, COMP, LOG_OP, CONST, SPEC_OP_PREFIX;
+ }
+
+ private static final String FUNC_START_DELIM = "(";
+ private static final String FUNC_END_DELIM = ")";
+ private static final String FUNC_PARAM_SEP = ",";
+
+ private static final Map<String,WordType> WORD_TYPES = new HashMap<String,WordType>();
+
+ static {
+ setWordType(WordType.OP, "+", "-", "*", "/", "\\", "^", "&", "mod");
+ setWordType(WordType.COMP, "<", "<=", ">", ">=", "=", "<>");
+ setWordType(WordType.LOG_OP, "and", "or", "eqv", "not", "xor");
+ setWordType(WordType.CONST, "true", "false", "null");
+ setWordType(WordType.SPEC_OP_PREFIX, "is", "like", "between", "in");
+ }
+
+
+ private static final Expr THIS_COL_VALUE = new Expr() {
+ @Override protected Object eval(RowContext ctx) {
+ return ctx.getThisColumnValue();
+ }
+ };
+ private static final Expr NULL_VALUE = new Expr() {
+ @Override protected Object eval(RowContext ctx) {
+ return null;
+ }
+ };
+ private static final Expr TRUE_VALUE = new Expr() {
+ @Override protected Object eval(RowContext ctx) {
+ return Boolean.TRUE;
+ }
+ };
+ private static final Expr FALSE_VALUE = new Expr() {
+ @Override protected Object eval(RowContext ctx) {
+ return Boolean.FALSE;
+ }
+ };
+
+ private Expressionator()
+ {
+ }
+
+ public static String testTokenize(Type exprType, String exprStr, Database db) {
+
+ List<Token> tokens = trimSpaces(
+ ExpressionTokenizer.tokenize(exprType, exprStr, (DatabaseImpl)db));
+
+ if(tokens == null) {
+ // FIXME, NULL_EXPR?
+ return null;
+ }
+
+ return tokens.toString();
+ }
+
+ public static Expr parse(Type exprType, String exprStr, Database db) {
+
+ List<Token> tokens = trimSpaces(
+ ExpressionTokenizer.tokenize(exprType, exprStr, (DatabaseImpl)db));
+
+ if(tokens == null) {
+ // FIXME, NULL_EXPR?
+ return null;
+ }
+
+ TokBuf buf = new TokBuf(tokens);
+ parseExpression(exprType, buf, isSimpleExpression(buf, exprType));
+
+ // FIXME
+ return null;
+ }
+
+ private static List<Token> trimSpaces(List<Token> tokens) {
+ if(tokens == null) {
+ return null;
+ }
+
+ // for the most part, spaces are superfluous except for one situation(?).
+ // when they appear between a string literal and '(' they help distinguish
+ // a function call from another expression form
+ for(int i = 1; i < (tokens.size() - 1); ++i) {
+ Token t = tokens.get(i);
+ if(t.getType() == TokenType.SPACE) {
+ if((tokens.get(i - 1).getType() == TokenType.STRING) &&
+ isOp(tokens.get(i + 1), FUNC_START_DELIM)) {
+ // we want to keep this space
+ } else {
+ tokens.remove(i);
+ --i;
+ }
+ }
+ }
+ return tokens;
+ }
+
+ private static Expr parseExpression(Type exprType, TokBuf buf,
+ boolean isSimpleExpr)
+ {
+
+ // FIXME, how do we handle order of ops when no parens?
+
+ while(buf.hasNext()) {
+ Token t = buf.next();
+
+ switch(t.getType()) {
+ case OBJ_NAME:
+ break;
+ case LITERAL:
+ break;
+ case OP:
+ break;
+ case STRING:
+ WordType wordType = getWordType(t);
+ if(wordType == null) {
+ // literal string? or possibly function?
+ Expr funcExpr = maybeParseFuncCall(t, buf, exprType, isSimpleExpr);
+ if(funcExpr != null) {
+ buf.setPendingExpr(funcExpr);
+ continue;
+ }
+ // FIXME
+ }
+ break;
+ case SPACE:
+ // top-level space is irrelevant
+ break;
+ default:
+ throw new RuntimeException("unknown token type " + t.getType());
+ }
+ }
+
+ Expr expr = buf.takePendingExpr();
+ if(expr == null) {
+ throw new IllegalArgumentException("No expression found?");
+ }
+
+ return expr;
+ }
+
+ private static Expr maybeParseFuncCall(Token firstTok, TokBuf buf,
+ Type exprType, boolean isSimpleExpr) {
+
+ int startPos = buf.curPos();
+ boolean foundFunc = false;
+
+ try {
+ Token t = buf.peekNext();
+ if((t == null) || !isOp(t, FUNC_START_DELIM)) {
+ // not a function call
+ return null;
+ }
+
+ buf.next();
+ List<TokBuf> paramBufs = findFuncCallParams(buf);
+
+ List<Expr> params = Collections.emptyList();
+ if(!paramBufs.isEmpty()) {
+ params = new ArrayList<Expr>(paramBufs.size());
+ for(TokBuf paramBuf : paramBufs) {
+ params.add(parseExpression(exprType, paramBuf, isSimpleExpr));
+ }
+ }
+
+ return new EFunc((String)firstTok.getValue(), params);
+
+ } finally {
+ if(!foundFunc) {
+ buf.reset(startPos);
+ }
+ }
+ }
+
+ private static List<TokBuf> findFuncCallParams(TokBuf buf) {
+
+ // simple case, no params
+ Token t = buf.peekNext();
+ if((t != null) && isOp(t, FUNC_END_DELIM)) {
+ buf.next();
+ return Collections.emptyList();
+ }
+
+ // find closing ")", handle nested parens
+ List<TokBuf> params = new ArrayList<TokBuf>(3);
+ int level = 1;
+ int startPos = buf.curPos();
+ while(buf.hasNext()) {
+
+ t = buf.next();
+
+ if(isOp(t, FUNC_START_DELIM)) {
+
+ ++level;
+
+ } else if(isOp(t, FUNC_END_DELIM)) {
+
+ --level;
+ if(level == 0) {
+ params.add(buf.subBuf(startPos, buf.prevPos()));
+
+ if(params.size() > 1) {
+ // if there is more than one param and one of them is empty, then
+ // something is messed up (note, it should not be possible to have
+ // an empty param if there is only one since we trim superfluous
+ // spaces)
+ for(TokBuf paramBuf : params) {
+ if(!paramBuf.hasNext()) {
+ throw new IllegalArgumentException(
+ "Invalid empty parameter for function");
+ }
+ }
+ }
+
+ return params;
+ }
+
+ } else if((level == 1) && isOp(t, FUNC_PARAM_SEP)) {
+
+ params.add(buf.subBuf(startPos, buf.prevPos()));
+ startPos = buf.curPos();
+ }
+ }
+
+ throw new IllegalArgumentException("Missing closing '" + FUNC_END_DELIM +
+ "' for function call");
+ }
+
+ private static boolean isSimpleExpression(TokBuf buf, Type exprType) {
+ if(exprType != Type.DEFAULT_VALUE) {
+ return false;
+ }
+
+ // a leading "=" indicates "full" expression handling for a DEFAULT_VALUE
+ Token t = buf.peekNext();
+ if((t != null) && isOp(t, "=")) {
+ buf.next();
+ return false;
+ }
+
+ // this is a "simple" DEFAULT_VALUE
+ return true;
+ }
+
+ private static boolean isOp(Token t, String opStr) {
+ return ((t.getType() == TokenType.OP) &&
+ opStr.equalsIgnoreCase((String)t.getValue()));
+ }
+
+ private static WordType getWordType(Token t) {
+ return WORD_TYPES.get(((String)t.getValue()).toLowerCase());
+ }
+
+ private static void setWordType(WordType type, String... words) {
+ for(String w : words) {
+ WORD_TYPES.put(w, type);
+ }
+ }
+
+ private static final class TokBuf
+ {
+ private final List<Token> _tokens;
+ private final boolean _topLevel;
+ private int _pos;
+ private Expr _pendingExpr;
+
+ private TokBuf(List<Token> tokens) {
+ this(tokens, true);
+ }
+
+ private TokBuf(List<Token> tokens, boolean topLevel) {
+ _tokens = tokens;
+ _topLevel = topLevel;
+ }
+
+ public boolean isTopLevel() {
+ return _topLevel;
+ }
+
+ public int curPos() {
+ return _pos;
+ }
+
+ public int prevPos() {
+ return _pos - 1;
+ }
+
+ public boolean hasNext() {
+ return (_pos < _tokens.size());
+ }
+
+ public Token peekNext() {
+ if(!hasNext()) {
+ return null;
+ }
+ return _tokens.get(_pos);
+ }
+
+ public Token next() {
+ return _tokens.get(_pos++);
+ }
+
+ public void reset(int pos) {
+ _pos = pos;
+ }
+
+ public TokBuf subBuf(int start, int end) {
+ return new TokBuf(_tokens.subList(start, end), false);
+ }
+
+ public void setPendingExpr(Expr expr) {
+ if(_pendingExpr == null) {
+ throw new IllegalArgumentException("Found multiple expressions with no operator");
+ }
+ _pendingExpr = expr;
+ }
+
+ public Expr takePendingExpr() {
+ Expr expr = _pendingExpr;
+ _pendingExpr = null;
+ return expr;
+ }
+
+ public boolean hasPendingExpr() {
+ return (_pendingExpr != null);
+ }
+ }
+
+ public static abstract class Expr
+ {
+ public Object evalDefault() {
+ return eval(null);
+ }
+
+ public boolean evalCondition(RowContext ctx) {
+ Object val = eval(ctx);
+
+ if(val instanceof Boolean) {
+ return (Boolean)val;
+ }
+
+ // a single value as a conditional expression seems to act like an
+ // implicit "="
+ return val.equals(ctx.getThisColumnValue());
+ }
+
+ protected abstract Object eval(RowContext ctx);
+ }
+
+ public interface RowContext
+ {
+ public Object getThisColumnValue();
+
+ public Object getRowValue(String colName);
+ }
+
+ private static final class ELiteralValue extends Expr
+ {
+ private final Object _value;
+
+ private ELiteralValue(Object value) {
+ _value = value;
+ }
+
+ @Override
+ public Object eval(RowContext ctx) {
+ return _value;
+ }
+ }
+
+ private static final class EColumnValue extends Expr
+ {
+ private final String _colName;
+
+ private EColumnValue(String colName) {
+ _colName = colName;
+ }
+
+ @Override
+ public Object eval(RowContext ctx) {
+ return ctx.getRowValue(_colName);
+ }
+ }
+
+ private static abstract class EOp
+ {
+
+ }
+
+ private static abstract class ECond
+ {
+
+ }
+
+ private static class EParen extends Expr
+ {
+ private final Expr _expr;
+
+ private EParen(Expr expr) {
+ _expr = expr;
+ }
+
+ @Override
+ protected Object eval(RowContext ctx) {
+ return _expr.eval(ctx);
+ }
+ }
+
+ private static class EFunc extends Expr
+ {
+ private final String _name;
+ private final List<Expr> _params;
+
+ private EFunc(String name, List<Expr> params) {
+ _name = name;
+ _params = params;
+ }
+
+ @Override
+ protected Object eval(RowContext ctx) {
+ // FIXME how do func results act for conditional values?
+
+ return false;
+ }
+ }
+
+}