import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import com.healthmarketscience.jackcess.DatabaseBuilder;
// what could it be?
switch(charFlag) {
case IS_OP_FLAG:
+
// special case '-' for negative number
- Object numLit = maybeParseNumberLiteral(c, buf);
+ Map.Entry<?,String> numLit = maybeParseNumberLiteral(c, buf);
if(numLit != null) {
- tokens.add(new Token(TokenType.LITERAL, numLit));
+ tokens.add(new Token(TokenType.LITERAL, numLit.getKey(),
+ numLit.getValue()));
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) {
}
// def values can't have cond at top level
throw new IllegalArgumentException(
- exprType + " cannot have top-level conditional");
+ exprType + " cannot have top-level conditional " + buf);
+
case FIELD_VALIDATOR:
+ case RECORD_VALIDATOR:
+
tokens.add(new Token(TokenType.OP, parseCompOp(c, buf)));
break;
}
tokens.add(new Token(TokenType.LITERAL, parseQuotedString(buf)));
break;
case DATE_LIT_DELIM_CHAR:
- tokens.add(new Token(TokenType.LITERAL,
- parseDateLiteralString(buf, db)));
+ Map.Entry<?,String> dateLit = parseDateLiteralString(buf, db);
+ tokens.add(new Token(TokenType.LITERAL, dateLit.getKey(),
+ dateLit.getValue()));
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);
+ "Invalid leading quote character " + c + " " + buf);
}
break;
} else {
if(isDigit(c)) {
- Object numLit = maybeParseNumberLiteral(c, buf);
+ Map.Entry<?,String> numLit = maybeParseNumberLiteral(c, buf);
if(numLit != null) {
- tokens.add(new Token(TokenType.LITERAL, numLit));
+ tokens.add(new Token(TokenType.LITERAL, numLit.getKey(),
+ numLit.getValue()));
continue;
}
}
if(!complete) {
throw new IllegalArgumentException("Missing closing '" + QUOTED_STR_CHAR +
- "' for quoted string");
+ "' for quoted string " + buf);
}
return sb.toString();
}
private static String parseObjNameString(ExprBuf buf) {
- return parseStringUntil(buf, OBJ_NAME_END_CHAR);
+ return parseStringUntil(buf, OBJ_NAME_END_CHAR, OBJ_NAME_START_CHAR);
}
- private static String parseStringUntil(ExprBuf buf, char endChar) {
+ private static String parseStringUntil(ExprBuf buf, char endChar,
+ Character startChar)
+ {
StringBuilder sb = buf.getScratchBuffer();
boolean complete = false;
if(c == endChar) {
complete = true;
break;
+ } else if((startChar != null) &&
+ (startChar == c)) {
+ throw new IllegalArgumentException("Missing closing '" + endChar +
+ "' for quoted string " + buf);
}
sb.append(c);
if(!complete) {
throw new IllegalArgumentException("Missing closing '" + endChar +
- "' for quoted string");
+ "' for quoted string " + buf);
}
return sb.toString();
}
- private static Date parseDateLiteralString(ExprBuf buf, DatabaseImpl db) {
- String dateStr = parseStringUntil(buf, DATE_LIT_DELIM_CHAR);
+ private static Map.Entry<?,String> parseDateLiteralString(
+ ExprBuf buf, DatabaseImpl db)
+ {
+ String dateStr = parseStringUntil(buf, DATE_LIT_DELIM_CHAR, null);
boolean hasDate = (dateStr.indexOf('/') >= 0);
boolean hasTime = (dateStr.indexOf(':') >= 0);
} else if(hasTime) {
sdf = buf.getTimeFormat(db);
} else {
- throw new IllegalArgumentException("Invalid date time literal " + dateStr);
+ throw new IllegalArgumentException("Invalid date time literal " + dateStr +
+ " " + buf);
}
// FIXME, do we need to know which "type" it was?
try {
- return sdf.parse(dateStr);
+ return newEntry(sdf.parse(dateStr), dateStr);
} catch(ParseException pe) {
throw new IllegalArgumentException(
- "Invalid date time literal " + dateStr, pe);
+ "Invalid date time literal " + dateStr + " " + buf, pe);
}
}
- private static Object maybeParseNumberLiteral(char firstChar, ExprBuf buf) {
+ private static Map.Entry<?,String> maybeParseNumberLiteral(char firstChar, ExprBuf buf) {
StringBuilder sb = buf.getScratchBuffer().append(firstChar);
boolean hasDigit = isDigit(firstChar);
// what number type to use here?
BigDecimal num = new BigDecimal(numStr);
foundNum = true;
- return num;
+ return newEntry(num, numStr);
} catch(NumberFormatException ne) {
throw new IllegalArgumentException(
- "Invalid number literal " + numStr, ne);
+ "Invalid number literal " + numStr + " " + buf, ne);
}
} finally {
return ((c >= '0') && (c <= '9'));
}
+ static <K,V> Map.Entry<K,V> newEntry(K a, V b) {
+ return new AbstractMap.SimpleImmutableEntry<K,V>(a, b);
+ }
+
private static final class ExprBuf
{
private final String _str;
return ((db != null) ? db.createDateFormat(str) :
DatabaseBuilder.createDateFormat(str));
}
+
+ @Override
+ public String toString() {
+ return "[char " + _pos + "] '" + _str + "'";
+ }
}
{
private final TokenType _type;
private final Object _val;
+ private final String _valStr;
+
+ private Token(TokenType type, String val) {
+ this(type, val, val);
+ }
- private Token(TokenType type, Object val) {
+ private Token(TokenType type, Object val, String valStr) {
_type = type;
_val = val;
+ _valStr = valStr;
}
public TokenType getType() {
}
public String getValueStr() {
- return (String)_val;
+ return _valStr;
}
@Override
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
+import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
public class Expressionator
{
+ // Useful links:
+ // - syntax: https://support.office.com/en-us/article/Guide-to-expression-syntax-ebc770bc-8486-4adc-a9ec-7427cce39a90
+ // - examples: https://support.office.com/en-us/article/Examples-of-expressions-d3901e11-c04e-4649-b40b-8b6ec5aed41f
+ // - validation rule usage: https://support.office.com/en-us/article/Restrict-data-input-by-using-a-validation-rule-6c0b2ce1-76fa-4be0-8ae9-038b52652320
+
public enum Type {
- DEFAULT_VALUE, FIELD_VALIDATOR;
+ DEFAULT_VALUE, FIELD_VALIDATOR, RECORD_VALIDATOR;
}
private enum WordType {
public static Expr parse(Type exprType, String exprStr, Database db) {
+ // FIXME,restrictions:
+ // - default value only accepts simple exprs, otherwise becomes literal text
+ // - def val cannot refer to any columns
+ // - field validation cannot refer to other columns
+ // - record validation cannot refer to outside columns
+
List<Token> tokens = trimSpaces(
ExpressionTokenizer.tokenize(exprType, exprStr, (DatabaseImpl)db));
private static Expr parseExpression(Type exprType, TokBuf buf,
boolean isSimpleExpr)
{
+ // FIXME pass exprType and isSimple expr in TokBuf?
// FIXME, how do we handle order of ops when no parens?
throw new RuntimeException("Invalid operator " + t);
}
+ // this can old be an OP or a COMP (those are the only words that the
+ // tokenizer would define as TokenType.OP)
+ switch(wordType) {
+ case OP:
+
+ // most ops are two argument except that '-' could be negation
+ if(buf.hasPendingExpr()) {
+ buf.setPendingExpr(parseBinaryOperator(t, buf, exprType,
+ isSimpleExpr));
+ } else if(isOp(t, "-")) {
+ buf.setPendingExpr(parseUnaryOperator(t, buf, exprType,
+ isSimpleExpr));
+ } else {
+ throw new IllegalArgumentException(
+ "Missing left expression for binary operator " + t.getValue() +
+ " " + buf);
+ }
+ break;
+
+ case COMP:
+
+ if(!buf.hasPendingExpr() && (exprType == Type.FIELD_VALIDATOR)) {
+ // comparison operators for field validators can implicitly use
+ // the current field value for the left value
+ buf.setPendingExpr(THIS_COL_VALUE);
+ }
+ if(buf.hasPendingExpr()) {
+ buf.setPendingExpr(parseCompOperator(t, buf, exprType,
+ isSimpleExpr));
+ } else {
+ throw new IllegalArgumentException(
+ "Missing left expression for comparison operator " +
+ t.getValue() + " " + buf);
+ }
+ break;
+
+ default:
+ throw new RuntimeException("Unexpected OP word type " + wordType);
+ }
break;
// see if it's a special word?
wordType = getWordType(t);
if(wordType == null) {
- // literal string? or possibly function?
+
+ // is it a function call?
Expr funcExpr = maybeParseFuncCall(t, buf, exprType, isSimpleExpr);
if(funcExpr != null) {
buf.setPendingExpr(funcExpr);
continue;
}
+ // is it an object name?
+ Token next = buf.peekNext();
+ if((next != null) && isObjNameSep(next)) {
+ buf.setPendingExpr(parseObjectReference(t, buf));
+ continue;
+ }
+
// FIXME maybe obj name, maybe string?
} else {
private static Expr parseObjectReference(Token firstTok, TokBuf buf) {
- // object references may be joined by '.' or '!';
- List<String> objNames = new ArrayList<String>();
+ // object references may be joined by '.' or '!'. access syntac docs claim
+ // object identifiers can be formatted like:
+ // "[Collection name]![Object name].[Property name]"
+ // However, in practice, they only ever seem to be (at most) two levels
+ // and only use '.'.
+ Deque<String> objNames = new LinkedList<String>();
objNames.add(firstTok.getValueStr());
Token t = null;
boolean atSep = false;
while((t = buf.peekNext()) != null) {
if(!atSep) {
- if(isOp(t, ".") || isOp(t, "!")) {
+ if(isObjNameSep(t)) {
buf.next();
atSep = true;
continue;
if((t.getType() == TokenType.OBJ_NAME) ||
(t.getType() == TokenType.STRING)) {
buf.next();
- objNames.add(t.getValueStr());
+ // always insert at beginning of list so names are in reverse order
+ objNames.addFirst(t.getValueStr());
atSep = false;
continue;
}
break;
}
- if(atSep) {
+ if(atSep || (objNames.size() > 3)) {
throw new IllegalArgumentException("Invalid object reference " + buf);
}
-
- return new EObjValue(objNames);
+
+ // names are in reverse order
+ String fieldName = objNames.poll();
+ String objName = objNames.poll();
+ String collectionName = objNames.poll();
+
+ return new EObjValue(collectionName, objName, fieldName);
}
private static Expr maybeParseFuncCall(Token firstTok, TokBuf buf,
"' for function call " + buf);
}
+ private static Expr parseBinaryOperator(Token firstTok, TokBuf buf,
+ Type exprType, boolean isSimpleExpr) {
+ String op = firstTok.getValueStr();
+ Expr leftExpr = buf.takePendingExpr();
+ Expr rightExpr = parseExpression(exprType, buf, isSimpleExpr);
+
+ return new EBinaryOp(op, leftExpr, rightExpr);
+ }
+
+ private static Expr parseUnaryOperator(Token firstTok, TokBuf buf,
+ Type exprType, boolean isSimpleExpr) {
+ String op = firstTok.getValueStr();
+ Expr val = parseExpression(exprType, buf, isSimpleExpr);
+
+ return new EUnaryOp(op, val);
+ }
+
+ private static Expr parseCompOperator(Token firstTok, TokBuf buf,
+ Type exprType, boolean isSimpleExpr) {
+ String op = firstTok.getValueStr();
+ Expr leftExpr = buf.takePendingExpr();
+ Expr rightExpr = parseExpression(exprType, buf, isSimpleExpr);
+
+ return new ECompOp(op, leftExpr, rightExpr);
+ }
+
private static boolean isSimpleExpression(TokBuf buf, Type exprType) {
if(exprType != Type.DEFAULT_VALUE) {
return false;
return true;
}
+ private static boolean isObjNameSep(Token t) {
+ return (isOp(t, ".") || isOp(t, "!"));
+ }
+
private static boolean isOp(Token t, String opStr) {
- return ((t.getType() == TokenType.OP) && opStr.equalsIgnoreCase(t.getValueStr()));
+ return ((t.getType() == TokenType.OP) &&
+ opStr.equalsIgnoreCase(t.getValueStr()));
}
private static WordType getWordType(Token t) {
{
private final List<Token> _tokens;
private final TokBuf _parent;
+ private final int _parentOff;
private int _pos;
private Expr _pendingExpr;
private TokBuf(List<Token> tokens) {
- this(tokens, null);
+ this(tokens, null, 0);
}
- private TokBuf(List<Token> tokens, TokBuf parent) {
+ private TokBuf(List<Token> tokens, TokBuf parent, int parentOff) {
_tokens = tokens;
_parent = parent;
+ _parentOff = parentOff;
}
public boolean isTopLevel() {
}
public TokBuf subBuf(int start, int end) {
- return new TokBuf(_tokens.subList(start, end), this);
+ return new TokBuf(_tokens.subList(start, end), this, start);
}
public void setPendingExpr(Expr expr) {
return (_pendingExpr != null);
}
+ private Map.Entry<Integer,List<Token>> getTopPos() {
+ int pos = _pos;
+ List<Token> toks = _tokens;
+ TokBuf cur = this;
+ while(cur._parent != null) {
+ pos += cur._parentOff;
+ cur = cur._parent;
+ toks = cur._tokens;
+ }
+ return ExpressionTokenizer.newEntry(pos, toks);
+ }
+
@Override
public String toString() {
- // FIXME show current pos
- return null;
+
+ Map.Entry<Integer,List<Token>> e = getTopPos();
+
+ // TODO actually format expression?
+ StringBuilder sb = new StringBuilder()
+ .append("[token ").append(e.getKey()).append("] (");
+
+ for(Iterator<Token> iter = e.getValue().iterator(); iter.hasNext(); ) {
+ Token t = iter.next();
+ sb.append("'").append(t.getValueStr()).append("'");
+ if(iter.hasNext()) {
+ sb.append(",");
+ }
+ }
+
+ sb.append(")");
+
+ return sb.toString();
}
}
{
public Object getThisColumnValue();
- public Object getRowValue(String colName);
+ public Object getRowValue(String collectionName, String objName,
+ String colName);
}
private static final class ELiteralValue extends Expr
private static final class EObjValue extends Expr
{
- private final List<String> _objNames;
+ private final String _collectionName;
+ private final String _objName;
+ private final String _fieldName;
+
- private EObjValue(List<String> objNames) {
- _objNames = objNames;
+ private EObjValue(String collectionName, String objName, String fieldName) {
+ _collectionName = collectionName;
+ _objName = objName;
+ _fieldName = fieldName;
}
@Override
public Object eval(RowContext ctx) {
- // FIXME
- return null;
- // return ctx.getRowValue(_colName);
+ return ctx.getRowValue(_collectionName, _objName, _fieldName);
}
}
}
}
+ private static class EBinaryOp extends Expr
+ {
+ private final String _op;
+ private final Expr _left;
+ private final Expr _right;
+
+ private EBinaryOp(String op, Expr left, Expr right) {
+ _op = op;
+ _left = left;
+ _right = right;
+ }
+
+ @Override
+ protected Object eval(RowContext ctx) {
+ // FIXME
+
+ return null;
+ }
+ }
+
+ private static class EUnaryOp extends Expr
+ {
+ private final String _op;
+ private final Expr _val;
+
+ private EUnaryOp(String op, Expr val) {
+ _op = op;
+ _val = val;
+ }
+
+ @Override
+ protected Object eval(RowContext ctx) {
+ // FIXME
+
+ return null;
+ }
+ }
+
+ private static class ECompOp extends Expr
+ {
+ private final String _op;
+ private final Expr _left;
+ private final Expr _right;
+
+ private ECompOp(String op, Expr left, Expr right) {
+ _op = op;
+ _left = left;
+ _right = right;
+ }
+
+ @Override
+ protected Object eval(RowContext ctx) {
+ // FIXME
+
+ return null;
+ }
+ }
+
}