View Javadoc
1   /*
2   Copyright (c) 2016 James Ahlborn
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl.expr;
18  
19  import java.math.BigDecimal;
20  import java.time.LocalDateTime;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Deque;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.LinkedList;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.Set;
34  import java.util.regex.Pattern;
35  import java.util.regex.PatternSyntaxException;
36  
37  import com.healthmarketscience.jackcess.expr.EvalContext;
38  import com.healthmarketscience.jackcess.expr.EvalException;
39  import com.healthmarketscience.jackcess.expr.Expression;
40  import com.healthmarketscience.jackcess.expr.Function;
41  import com.healthmarketscience.jackcess.expr.FunctionLookup;
42  import com.healthmarketscience.jackcess.expr.Identifier;
43  import com.healthmarketscience.jackcess.expr.LocaleContext;
44  import com.healthmarketscience.jackcess.expr.ParseException;
45  import com.healthmarketscience.jackcess.expr.Value;
46  import com.healthmarketscience.jackcess.impl.StringUtil;
47  import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.Token;
48  import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType;
49  
50  
51  /**
52   *
53   * @author James Ahlborn
54   */
55  public class Expressionator
56  {
57  
58    // Useful links:
59    // - syntax: https://support.office.com/en-us/article/Guide-to-expression-syntax-ebc770bc-8486-4adc-a9ec-7427cce39a90
60    // - examples: https://support.office.com/en-us/article/Examples-of-expressions-d3901e11-c04e-4649-b40b-8b6ec5aed41f
61    // - validation rule usage: https://support.office.com/en-us/article/Restrict-data-input-by-using-a-validation-rule-6c0b2ce1-76fa-4be0-8ae9-038b52652320
62  
63  
64    public enum Type {
65      DEFAULT_VALUE, EXPRESSION, FIELD_VALIDATOR, RECORD_VALIDATOR;
66    }
67  
68    public interface ParseContext extends LocaleContext {
69      public FunctionLookup getFunctionLookup();
70    }
71  
72    private enum WordType {
73      OP, COMP, LOG_OP, CONST, SPEC_OP_PREFIX, DELIM;
74    }
75  
76    private static final String FUNC_START_DELIM = "(";
77    private static final String OPEN_PAREN = "(";
78    private static final String CLOSE_PAREN = ")";
79    private static final String FUNC_PARAM_SEP = ",";
80  
81    private static final Map<String,WordType> WORD_TYPES =
82      new HashMap<>();
83  
84    static {
85      setWordType(WordType.OP, "+", "-", "*", "/", "\\", "^", "&", "mod");
86      setWordType(WordType.COMP, "<", "<=", ">", ">=", "=", "<>");
87      setWordType(WordType.LOG_OP, "and", "or", "eqv", "xor", "imp");
88      setWordType(WordType.CONST, "true", "false", "null", "on", "off",
89                  "yes", "no");
90      setWordType(WordType.SPEC_OP_PREFIX, "is", "like", "between", "in", "not");
91      // "X is null", "X is not null", "X like P", "X between A and B",
92      // "X not between A and B", "X in (A, B, C...)", "X not in (A, B, C...)",
93      // "not X"
94      setWordType(WordType.DELIM, ".", "!", ",", "(", ")");
95    }
96  
97    private static final Collection<String> TRUE_STRS =
98      Arrays.asList("true", "yes", "on");
99    private static final Collection<String> FALSE_STRS =
100     Arrays.asList("false", "no", "off");
101 
102   private interface OpType {}
103 
104   private enum UnaryOp implements OpType {
105     NEG("-", false) {
106       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1) {
107         return BuiltinOperators.negate(ctx, param1);
108       }
109       @Override public UnaryOp getUnaryNumOp() {
110         return UnaryOp.NEG_NUM;
111       }
112     },
113     POS("+", false) {
114       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1) {
115         // basically a no-op
116         return param1;
117       }
118       @Override public UnaryOp getUnaryNumOp() {
119         return UnaryOp.POS_NUM;
120       }
121     },
122     NOT("Not", true) {
123       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1) {
124         return BuiltinOperators.not(ctx, param1);
125       }
126     },
127     // when a '-' immediately precedes a number, it needs "highest" precedence
128     NEG_NUM("-", false) {
129       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1) {
130         return BuiltinOperators.negate(ctx, param1);
131       }
132     },
133     // when a '+' immediately precedes a number, it needs "highest" precedence
134     POS_NUM("+", false) {
135       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1) {
136         // basically a no-op
137         return param1;
138       }
139     };
140 
141     private final String _str;
142     private final boolean _needSpace;
143 
144     private UnaryOp(String str, boolean needSpace) {
145       _str = str;
146       _needSpace = needSpace;
147     }
148 
149     public boolean needsSpace() {
150       return _needSpace;
151     }
152 
153     @Override
154     public String toString() {
155       return _str;
156     }
157 
158     public UnaryOp getUnaryNumOp() {
159       return null;
160     }
161 
162     public abstract Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1);
163   }
164 
165   private enum BinaryOp implements OpType {
166     PLUS("+") {
167       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
168         return BuiltinOperators.add(ctx, param1, param2);
169       }
170     },
171     MINUS("-") {
172       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
173         return BuiltinOperators.subtract(ctx, param1, param2);
174       }
175     },
176     MULT("*") {
177       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
178         return BuiltinOperators.multiply(ctx, param1, param2);
179       }
180     },
181     DIV("/") {
182       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
183         return BuiltinOperators.divide(ctx, param1, param2);
184       }
185     },
186     INT_DIV("\\") {
187       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
188         return BuiltinOperators.intDivide(ctx, param1, param2);
189       }
190     },
191     EXP("^") {
192       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
193         return BuiltinOperators.exp(ctx, param1, param2);
194       }
195     },
196     CONCAT("&") {
197       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
198         return BuiltinOperators.concat(ctx, param1, param2);
199       }
200     },
201     MOD("Mod") {
202       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
203         return BuiltinOperators.mod(ctx, param1, param2);
204       }
205     };
206 
207     private final String _str;
208 
209     private BinaryOp(String str) {
210       _str = str;
211     }
212 
213     @Override
214     public String toString() {
215       return _str;
216     }
217 
218     public abstract Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2);
219   }
220 
221   private enum CompOp implements OpType {
222     LT("<") {
223       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
224         return BuiltinOperators.lessThan(ctx, param1, param2);
225       }
226     },
227     LTE("<=") {
228       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
229         return BuiltinOperators.lessThanEq(ctx, param1, param2);
230       }
231     },
232     GT(">") {
233       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
234         return BuiltinOperators.greaterThan(ctx, param1, param2);
235       }
236     },
237     GTE(">=") {
238       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
239         return BuiltinOperators.greaterThanEq(ctx, param1, param2);
240       }
241     },
242     EQ("=") {
243       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
244         return BuiltinOperators.equals(ctx, param1, param2);
245       }
246     },
247     NE("<>") {
248       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
249         return BuiltinOperators.notEquals(ctx, param1, param2);
250       }
251     };
252 
253     private final String _str;
254 
255     private CompOp(String str) {
256       _str = str;
257     }
258 
259     @Override
260     public String toString() {
261       return _str;
262     }
263 
264     public abstract Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2);
265   }
266 
267   private enum LogOp implements OpType {
268     AND("And") {
269       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
270         return BuiltinOperators.and(ctx, param1, param2);
271       }
272     },
273     OR("Or") {
274       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
275         return BuiltinOperators.or(ctx, param1, param2);
276       }
277     },
278     EQV("Eqv") {
279       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
280         return BuiltinOperators.eqv(ctx, param1, param2);
281       }
282     },
283     XOR("Xor") {
284       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
285         return BuiltinOperators.xor(ctx, param1, param2);
286       }
287     },
288     IMP("Imp") {
289       @Override public Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2) {
290         return BuiltinOperators.imp(ctx, param1, param2);
291       }
292     };
293 
294     private final String _str;
295 
296     private LogOp(String str) {
297       _str = str;
298     }
299 
300     @Override
301     public String toString() {
302       return _str;
303     }
304 
305     public abstract Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Valuef="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value param1, Value param2);
306   }
307 
308   private enum SpecOp implements OpType {
309     // note, "NOT" is not actually used as a special operation, always
310     // replaced with UnaryOp.NOT
311     NOT("Not") {
312       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
313         throw new UnsupportedOperationException();
314       }
315     },
316     IS_NULL("Is Null") {
317       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
318         return BuiltinOperators.isNull(param1);
319       }
320     },
321     IS_NOT_NULL("Is Not Null") {
322       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
323         return BuiltinOperators.isNotNull(param1);
324       }
325     },
326     LIKE("Like") {
327       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
328         return BuiltinOperators.like(ctx, param1, (Pattern)param2);
329       }
330     },
331     NOT_LIKE("Not Like") {
332       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
333         return BuiltinOperators.notLike(ctx, param1, (Pattern)param2);
334       }
335     },
336     BETWEEN("Between") {
337       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
338         return BuiltinOperators.between(ctx, param1, (Value="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value)param2, (Value)param3);
339       }
340     },
341     NOT_BETWEEN("Not Between") {
342       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
343         return BuiltinOperators.notBetween(ctx, param1, (Value="../../../../../com/healthmarketscience/jackcess/expr/Value.html#Value">Value)param2, (Value)param3);
344       }
345     },
346     IN("In") {
347       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
348         return BuiltinOperators.in(ctx, param1, (Value[])param2);
349       }
350     },
351     NOT_IN("Not In") {
352       @Override public Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3) {
353         return BuiltinOperators.notIn(ctx, param1, (Value[])param2);
354       }
355     };
356 
357     private final String _str;
358 
359     private SpecOp(String str) {
360       _str = str;
361     }
362 
363     @Override
364     public String toString() {
365       return _str;
366     }
367 
368     public abstract Value/../com/healthmarketscience/jackcess/expr/Value.html#Value">Value eval(EvalContext ctx, Value param1, Object param2, Object param3);
369   }
370 
371   private static final Map<OpType, Integer> PRECENDENCE =
372     buildPrecedenceMap(
373         new OpType[]{UnaryOp.NEG_NUM, UnaryOp.POS_NUM},
374         new OpType[]{BinaryOp.EXP},
375         new OpType[]{UnaryOp.NEG, UnaryOp.POS},
376         new OpType[]{BinaryOp.MULT, BinaryOp.DIV},
377         new OpType[]{BinaryOp.INT_DIV},
378         new OpType[]{BinaryOp.MOD},
379         new OpType[]{BinaryOp.PLUS, BinaryOp.MINUS},
380         new OpType[]{BinaryOp.CONCAT},
381         new OpType[]{CompOp.LT, CompOp.GT, CompOp.NE, CompOp.LTE, CompOp.GTE,
382                      CompOp.EQ, SpecOp.LIKE, SpecOp.NOT_LIKE,
383                      SpecOp.IS_NULL, SpecOp.IS_NOT_NULL},
384         new OpType[]{UnaryOp.NOT},
385         new OpType[]{LogOp.AND},
386         new OpType[]{LogOp.OR},
387         new OpType[]{LogOp.XOR},
388         new OpType[]{LogOp.EQV},
389         new OpType[]{LogOp.IMP},
390         new OpType[]{SpecOp.IN, SpecOp.NOT_IN, SpecOp.BETWEEN,
391                      SpecOp.NOT_BETWEEN});
392 
393   private static final Set<Character> REGEX_SPEC_CHARS = new HashSet<>(
394       Arrays.asList('\\','.','%','=','+', '$','^','|','(',')','{','}','&',
395                     '[',']','*','?'));
396   // this is a regular expression which will never match any string
397   private static final Pattern UNMATCHABLE_REGEX = Pattern.compile("(?!)");
398 
399   private static final Expr THIS_COL_VALUE = new EThisValue();
400 
401   private static final Expr NULL_VALUE = new EConstValue(
402       ValueSupport.NULL_VAL, "Null");
403   private static final Expr TRUE_VALUE = new EConstValue(
404       ValueSupport.TRUE_VAL, "True");
405   private static final Expr FALSE_VALUE = new EConstValue(
406       ValueSupport.FALSE_VAL, "False");
407 
408 
409   private Expressionator() {}
410 
411   public static Expression parse(Type exprType, String exprStr,
412                                  Value.Type resultType,
413                                  ParseContext context) {
414 
415     List<Token> tokens = trimSpaces(
416         ExpressionTokenizer.tokenize(exprType, exprStr, context));
417 
418     if(tokens == null) {
419       throw new ParseException("null/empty expression");
420     }
421 
422     TokBuf buf = new TokBuf(exprType, tokens, context);
423 
424     if(isLiteralDefaultValue(buf, resultType, exprStr)) {
425 
426       // this is handled as a literal string value, not an expression.  no
427       // need to memo-ize cause it's a simple literal value
428       return new ExprWrapper(exprStr,
429           new ELiteralValue(Value.Type.STRING, exprStr), resultType);
430     }
431 
432     // normal expression handling
433     Expr expr = parseExpression(buf, false);
434 
435     if((exprType == Type.FIELD_VALIDATOR) && !expr.isValidationExpr()) {
436       // a non-validation expression for a FIELD_VALIDATOR treats the result
437       // as an equality comparison with the field in question.  so, transform
438       // the expression accordingly
439       expr = new EImplicitCompOp(expr);
440     }
441 
442     switch(exprType) {
443     case DEFAULT_VALUE:
444     case EXPRESSION:
445       return (expr.isConstant() ?
446               // for now, just cache at top-level for speed (could in theory
447               // cache intermediate values?)
448               new MemoizedExprWrapper(exprStr, expr, resultType) :
449               new ExprWrapper(exprStr, expr, resultType));
450     case FIELD_VALIDATOR:
451     case RECORD_VALIDATOR:
452       return (expr.isConstant() ?
453               // for now, just cache at top-level for speed (could in theory
454               // cache intermediate values?)
455               new MemoizedCondExprWrapper(exprStr, expr) :
456               new CondExprWrapper(exprStr, expr));
457     default:
458       throw new ParseException("unexpected expression type " + exprType);
459     }
460   }
461 
462   private static List<Token> trimSpaces(List<Token> tokens) {
463     if(tokens == null) {
464       return null;
465     }
466 
467     // for the most part, spaces are superfluous except for one situation(?).
468     // when they appear between a string literal and '(' they help distinguish
469     // a function call from another expression form
470     for(int i = 1; i < (tokens.size() - 1); ++i) {
471       Token t = tokens.get(i);
472       if(t.getType() == TokenType.SPACE) {
473         if((tokens.get(i - 1).getType() == TokenType.STRING) &&
474            isDelim(tokens.get(i + 1), FUNC_START_DELIM)) {
475           // we want to keep this space
476         } else {
477           tokens.remove(i);
478           --i;
479         }
480       }
481     }
482     return tokens;
483   }
484 
485   private static Expr parseExpression(TokBuf buf, boolean singleExpr)
486   {
487     while(buf.hasNext()) {
488       Token t = buf.next();
489 
490       switch(t.getType()) {
491       case OBJ_NAME:
492 
493         parseObjectRefExpression(t, buf);
494         break;
495 
496       case LITERAL:
497 
498         buf.setPendingExpr(new ELiteralValue(t.getValueType(), t.getValue()));
499         break;
500 
501       case OP:
502 
503         WordType wordType = getWordType(t);
504         if(wordType == null) {
505           // shouldn't happen
506           throw new ParseException("Invalid operator " + t);
507         }
508 
509         // this can only be an OP or a COMP (those are the only words that the
510         // tokenizer would define as TokenType.OP)
511         switch(wordType) {
512         case OP:
513           parseOperatorExpression(t, buf);
514           break;
515 
516         case COMP:
517 
518           parseCompOpExpression(t, buf);
519           break;
520 
521         default:
522           throw new ParseException("Unexpected OP word type " + wordType);
523         }
524 
525         break;
526 
527       case DELIM:
528 
529         parseDelimExpression(t, buf);
530         break;
531 
532       case STRING:
533 
534         // see if it's a special word?
535         wordType = getWordType(t);
536         if(wordType == null) {
537 
538           // is it a function call?
539           if(!maybeParseFuncCallExpression(t, buf)) {
540 
541             // is it an object name?
542             Token next = buf.peekNext();
543             if((next != null) && isObjNameSep(next)) {
544 
545               parseObjectRefExpression(t, buf);
546 
547             } else {
548 
549               // FIXME maybe bare obj name, maybe string literal?
550               throw new UnsupportedOperationException("FIXME");
551             }
552           }
553 
554         } else {
555 
556           // this could be anything but COMP or DELIM (all COMPs would be
557           // returned as TokenType.OP and all DELIMs would be TokenType.DELIM)
558           switch(wordType) {
559           case OP:
560 
561             parseOperatorExpression(t, buf);
562             break;
563 
564           case LOG_OP:
565 
566             parseLogicalOpExpression(t, buf);
567             break;
568 
569           case CONST:
570 
571             parseConstExpression(t, buf);
572             break;
573 
574           case SPEC_OP_PREFIX:
575 
576             parseSpecOpExpression(t, buf);
577             break;
578 
579           default:
580             throw new ParseException("Unexpected STRING word type "
581                                        + wordType);
582           }
583         }
584 
585         break;
586 
587       case SPACE:
588         // top-level space is irrelevant (and we strip them anyway)
589         break;
590 
591       default:
592         throw new ParseException("unknown token type " + t);
593       }
594 
595       if(singleExpr && buf.hasPendingExpr()) {
596         break;
597       }
598     }
599 
600     Expr expr = buf.takePendingExpr();
601     if(expr == null) {
602       throw new ParseException("No expression found? " + buf);
603     }
604 
605     return expr;
606   }
607 
608   private static void parseObjectRefExpression(Token firstTok, TokBuf buf) {
609 
610     // object references may be joined by '.' or '!'. access syntac docs claim
611     // object identifiers can be formatted like:
612     //     "[Collection name]![Object name].[Property name]"
613     // However, in practice, they only ever seem to be (at most) two levels
614     // and only use '.'.  Apparently '!' is actually a special late-bind
615     // operator (not sure it makes a difference for this code?), see:
616     // http://bytecomb.com/the-bang-exclamation-operator-in-vba/
617     Deque<String> objNames = new LinkedList<>();
618     objNames.add(firstTok.getValueStr());
619 
620     Token t = null;
621     boolean atSep = false;
622     while((t = buf.peekNext()) != null) {
623       if(!atSep) {
624         if(isObjNameSep(t)) {
625           buf.next();
626           atSep = true;
627           continue;
628         }
629       } else {
630         if((t.getType() == TokenType.OBJ_NAME) ||
631            (t.getType() == TokenType.STRING)) {
632           buf.next();
633           // always insert at beginning of list so names are in reverse order
634           objNames.addFirst(t.getValueStr());
635           atSep = false;
636           continue;
637         }
638       }
639       break;
640     }
641 
642     int numNames = objNames.size();
643     if(atSep || (numNames > 3)) {
644       throw new ParseException("Invalid object reference " + buf);
645     }
646 
647     // names are in reverse order
648     String propName = null;
649     if(numNames == 3) {
650       propName = objNames.poll();
651     }
652     String objName = objNames.poll();
653     String collectionName = objNames.poll();
654 
655     buf.setPendingExpr(
656         new EObjValue(new Identifier(collectionName, objName, propName)));
657   }
658 
659   private static void parseDelimExpression(Token firstTok, TokBuf buf) {
660     // the only "top-level" delim we expect to find is open paren, and
661     // there shouldn't be any pending expression
662     if(!isDelim(firstTok, OPEN_PAREN) || buf.hasPendingExpr()) {
663       throw new ParseException("Unexpected delimiter " +
664                                          firstTok.getValue() + " " + buf);
665     }
666 
667     Expr subExpr = findParenExprs(buf, false).get(0);
668     buf.setPendingExpr(new EParen(subExpr));
669   }
670 
671   private static boolean maybeParseFuncCallExpression(
672       Token firstTok, TokBuf buf) {
673 
674     int startPos = buf.curPos();
675     boolean foundFunc = false;
676 
677     try {
678       Token t = buf.peekNext();
679       if(!isDelim(t, FUNC_START_DELIM)) {
680         // not a function call
681         return false;
682       }
683 
684       buf.next();
685       List<Expr> params = findParenExprs(buf, true);
686       String funcName = firstTok.getValueStr();
687       Function func = buf.getFunction(funcName);
688       if(func == null) {
689         throw new ParseException("Could not find function '" +
690                                            funcName + "' " + buf);
691       }
692       buf.setPendingExpr(new EFunc(func, params));
693       foundFunc = true;
694       return true;
695 
696     } finally {
697       if(!foundFunc) {
698         buf.reset(startPos);
699       }
700     }
701   }
702 
703   private static List<Expr> findParenExprs(
704       TokBuf buf, boolean allowMulti) {
705 
706     if(allowMulti) {
707       // simple case, no nested expr
708       Token t = buf.peekNext();
709       if(isDelim(t, CLOSE_PAREN)) {
710         buf.next();
711         return Collections.emptyList();
712       }
713     }
714 
715     // find closing ")", handle nested parens
716     List<Expr> exprs = new ArrayList<>(3);
717     int level = 1;
718     int startPos = buf.curPos();
719     while(buf.hasNext()) {
720 
721       Token t = buf.next();
722 
723       if(isDelim(t, OPEN_PAREN)) {
724 
725         ++level;
726 
727       } else if(isDelim(t, CLOSE_PAREN)) {
728 
729         --level;
730         if(level == 0) {
731           TokBuf subBuf = buf.subBuf(startPos, buf.prevPos());
732           exprs.add(parseExpression(subBuf, false));
733           return exprs;
734         }
735 
736       } else if(allowMulti && (level == 1) && isDelim(t, FUNC_PARAM_SEP)) {
737 
738         TokBuf subBuf = buf.subBuf(startPos, buf.prevPos());
739         exprs.add(parseExpression(subBuf, false));
740         startPos = buf.curPos();
741       }
742     }
743 
744     throw new ParseException("Missing closing '" + CLOSE_PAREN
745                                        + " " + buf);
746   }
747 
748   private static void parseOperatorExpression(Token t, TokBuf buf) {
749 
750     // most ops are two argument except that '-' could be negation, "+" could
751     // be pos-ation
752     if(buf.hasPendingExpr()) {
753       parseBinaryOpExpression(t, buf);
754     } else if(isEitherOp(t, "-", "+")) {
755       parseUnaryOpExpression(t, buf);
756     } else {
757       throw new ParseException(
758           "Missing left expression for binary operator " + t.getValue() +
759           " " + buf);
760     }
761   }
762 
763   private static void parseBinaryOpExpression(Token firstTok, TokBuf buf) {
764     BinaryOp op = getOpType(firstTok, BinaryOp.class);
765     Expr leftExpr = buf.takePendingExpr();
766     Expr rightExpr = parseExpression(buf, true);
767 
768     buf.setPendingExpr(new EBinaryOp(op, leftExpr, rightExpr));
769   }
770 
771   private static void parseUnaryOpExpression(Token firstTok, TokBuf buf) {
772     UnaryOp op = getOpType(firstTok, UnaryOp.class);
773 
774     UnaryOp numOp = op.getUnaryNumOp();
775     if(numOp != null) {
776       // if this operator is immediately preceding a number, it has a higher
777       // precedence
778       Token nextTok = buf.peekNext();
779       if((nextTok != null) && (nextTok.getType() == TokenType.LITERAL) &&
780          nextTok.getValueType().isNumeric()) {
781         op = numOp;
782       }
783     }
784 
785     Expr val = parseExpression(buf, true);
786 
787     buf.setPendingExpr(new EUnaryOp(op, val));
788   }
789 
790   private static void parseCompOpExpression(Token firstTok, TokBuf buf) {
791 
792     if(!buf.hasPendingExpr()) {
793       if(buf.getExprType() == Type.FIELD_VALIDATOR) {
794         // comparison operators for field validators can implicitly use
795         // the current field value for the left value
796         buf.setPendingExpr(THIS_COL_VALUE);
797       } else {
798         throw new ParseException(
799             "Missing left expression for comparison operator " +
800             firstTok.getValue() + " " + buf);
801       }
802     }
803 
804     CompOp op = getOpType(firstTok, CompOp.class);
805     Expr leftExpr = buf.takePendingExpr();
806     Expr rightExpr = parseExpression(buf, true);
807 
808     buf.setPendingExpr(new ECompOp(op, leftExpr, rightExpr));
809   }
810 
811   private static void parseLogicalOpExpression(Token firstTok, TokBuf buf) {
812 
813     if(!buf.hasPendingExpr()) {
814       throw new ParseException(
815           "Missing left expression for logical operator " +
816           firstTok.getValue() + " " + buf);
817     }
818 
819     LogOp op = getOpType(firstTok, LogOp.class);
820     Expr leftExpr = buf.takePendingExpr();
821     Expr rightExpr = parseExpression(buf, true);
822 
823     buf.setPendingExpr(new ELogicalOp(op, leftExpr, rightExpr));
824   }
825 
826   private static void parseSpecOpExpression(Token firstTok, TokBuf buf) {
827 
828     SpecOp specOp = getSpecialOperator(firstTok, buf);
829 
830     if(specOp == SpecOp.NOT) {
831       // this is the unary prefix operator
832       parseUnaryOpExpression(firstTok, buf);
833       return;
834     }
835 
836     if(!buf.hasPendingExpr()) {
837       if(buf.getExprType() == Type.FIELD_VALIDATOR) {
838         // comparison operators for field validators can implicitly use
839         // the current field value for the left value
840         buf.setPendingExpr(THIS_COL_VALUE);
841       } else {
842         throw new ParseException(
843             "Missing left expression for comparison operator " +
844             specOp + " " + buf);
845       }
846     }
847 
848     Expr expr = buf.takePendingExpr();
849 
850     Expr specOpExpr = null;
851     switch(specOp) {
852     case IS_NULL:
853     case IS_NOT_NULL:
854       specOpExpr = new ENullOp(specOp, expr);
855       break;
856 
857     case LIKE:
858     case NOT_LIKE:
859       Token t = buf.next();
860       if((t.getType() != TokenType.LITERAL) ||
861          (t.getValueType() != Value.Type.STRING)) {
862         throw new ParseException("Missing Like pattern " + buf);
863       }
864       String patternStr = t.getValueStr();
865       specOpExpr = new ELikeOp(specOp, expr, patternStr);
866       break;
867 
868     case BETWEEN:
869     case NOT_BETWEEN:
870 
871       // the "rest" of a between expression is of the form "X And Y".  we are
872       // going to speculatively parse forward until we find the "And"
873       // operator.
874       Expr startRangeExpr = null;
875       while(true) {
876 
877         Expr tmpExpr = parseExpression(buf, true);
878         Token tmpT = buf.peekNext();
879 
880         if(tmpT == null) {
881           // ran out of expression?
882           throw new ParseException(
883               "Missing 'And' for 'Between' expression " + buf);
884         }
885 
886         if(isString(tmpT, "and")) {
887           buf.next();
888           startRangeExpr = tmpExpr;
889           break;
890         }
891 
892         // put the pending expression back and try parsing some more
893         buf.restorePendingExpr(tmpExpr);
894       }
895 
896       Expr endRangeExpr = parseExpression(buf, true);
897 
898       specOpExpr = new EBetweenOp(specOp, expr, startRangeExpr, endRangeExpr);
899       break;
900 
901     case IN:
902     case NOT_IN:
903 
904       // there might be a space before open paren
905       t = buf.next();
906       if(t.getType() == TokenType.SPACE) {
907         t = buf.next();
908       }
909       if(!isDelim(t, OPEN_PAREN)) {
910         throw new ParseException("Malformed 'In' expression " + buf);
911       }
912 
913       List<Expr> exprs = findParenExprs(buf, true);
914       specOpExpr = new EInOp(specOp, expr, exprs);
915       break;
916 
917     default:
918       throw new ParseException("Unexpected special op " + specOp);
919     }
920 
921     buf.setPendingExpr(specOpExpr);
922   }
923 
924   private static SpecOp getSpecialOperator(Token firstTok, TokBuf buf) {
925     String opStr = firstTok.getValueStr().toLowerCase(Locale.ROOT);
926 
927     if("is".equals(opStr)) {
928       Token t = buf.peekNext();
929       if(isString(t, "null")) {
930         buf.next();
931         return SpecOp.IS_NULL;
932       } else if(isString(t, "not")) {
933         buf.next();
934         t = buf.peekNext();
935         if(isString(t, "null")) {
936           buf.next();
937           return SpecOp.IS_NOT_NULL;
938         }
939       }
940     } else if("like".equals(opStr)) {
941       return SpecOp.LIKE;
942     } else if("between".equals(opStr)) {
943       return SpecOp.BETWEEN;
944     } else if("in".equals(opStr)) {
945       return SpecOp.IN;
946     } else if("not".equals(opStr)) {
947       Token t = buf.peekNext();
948       if(isString(t, "between")) {
949         buf.next();
950         return SpecOp.NOT_BETWEEN;
951       } else if(isString(t, "in")) {
952         buf.next();
953         return SpecOp.NOT_IN;
954       } else if(isString(t, "like")) {
955         buf.next();
956         return SpecOp.NOT_LIKE;
957       }
958       return SpecOp.NOT;
959     }
960 
961     throw new ParseException(
962         "Malformed special operator " + opStr + " " + buf);
963   }
964 
965   private static void parseConstExpression(Token firstTok, TokBuf buf) {
966     Expr constExpr = null;
967     String tokStr = firstTok.getValueStr().toLowerCase(Locale.ROOT);
968     if(TRUE_STRS.contains(tokStr)) {
969       constExpr = TRUE_VALUE;
970     } else if(FALSE_STRS.contains(tokStr)) {
971       constExpr = FALSE_VALUE;
972     } else if("null".equals(tokStr)) {
973       constExpr = NULL_VALUE;
974     } else {
975       throw new ParseException("Unexpected CONST word "
976                                  + firstTok.getValue());
977     }
978     buf.setPendingExpr(constExpr);
979   }
980 
981   private static boolean isObjNameSep(Token t) {
982     return (isDelim(t, ".") || isDelim(t, "!"));
983   }
984 
985   private static boolean isOp(Token t, String opStr) {
986     return ((t != null) && (t.getType() == TokenType.OP) &&
987             opStr.equalsIgnoreCase(t.getValueStr()));
988   }
989 
990   private static boolean isEitherOp(Token t, String opStr1, String opStr2) {
991     return ((t != null) && (t.getType() == TokenType.OP) &&
992             (opStr1.equalsIgnoreCase(t.getValueStr()) ||
993              opStr2.equalsIgnoreCase(t.getValueStr())));
994   }
995 
996   private static boolean isDelim(Token t, String opStr) {
997     return ((t != null) && (t.getType() == TokenType.DELIM) &&
998             opStr.equalsIgnoreCase(t.getValueStr()));
999   }
1000 
1001   private static boolean isString(Token t, String opStr) {
1002     return ((t != null) && (t.getType() == TokenType.STRING) &&
1003             opStr.equalsIgnoreCase(t.getValueStr()));
1004   }
1005 
1006   private static WordType getWordType(Token t) {
1007     return WORD_TYPES.get(t.getValueStr().toLowerCase(Locale.ROOT));
1008   }
1009 
1010   private static void setWordType(WordType type, String... words) {
1011     for(String w : words) {
1012       WORD_TYPES.put(w, type);
1013     }
1014   }
1015 
1016   private static <T extends Enum<T>> T getOpType(Token t, Class<T> opClazz) {
1017     String str = t.getValueStr();
1018     for(T op : opClazz.getEnumConstants()) {
1019       if(str.equalsIgnoreCase(op.toString())) {
1020         return op;
1021       }
1022     }
1023     throw new ParseException("Unexpected op string " + t.getValueStr());
1024   }
1025 
1026   private static StringBuilder appendLeadingExpr(
1027       Expr expr, LocaleContext ctx, StringBuilder sb, boolean isDebug)
1028   {
1029     int len = sb.length();
1030     expr.toString(ctx, sb, isDebug);
1031     if(sb.length() > len) {
1032       // only add space if the leading expr added some text
1033       sb.append(" ");
1034     }
1035     return sb;
1036   }
1037 
1038   private static final class TokBuf
1039   {
1040     private final Type _exprType;
1041     private final List<Token> _tokens;
1042     private final TokBuf _parent;
1043     private final int _parentOff;
1044     private final ParseContext _ctx;
1045     private int _pos;
1046     private Expr _pendingExpr;
1047 
1048     private TokBuf(Type exprType, List<Token> tokens, ParseContext context) {
1049       this(exprType, tokens, null, 0, context);
1050     }
1051 
1052     private TokBuf(List<Token> tokens, TokBuf parent, int parentOff) {
1053       this(parent._exprType, tokens, parent, parentOff, parent._ctx);
1054     }
1055 
1056     private TokBuf(Type exprType, List<Token> tokens, TokBuf parent,
1057                    int parentOff, ParseContext context) {
1058       _exprType = exprType;
1059       _tokens = tokens;
1060       _parent = parent;
1061       _parentOff = parentOff;
1062       _ctx = context;
1063     }
1064 
1065     public Type getExprType() {
1066       return _exprType;
1067     }
1068 
1069     public int curPos() {
1070       return _pos;
1071     }
1072 
1073     public int prevPos() {
1074       return _pos - 1;
1075     }
1076 
1077     public boolean hasNext() {
1078       return (_pos < _tokens.size());
1079     }
1080 
1081     public Token peekNext() {
1082       if(!hasNext()) {
1083         return null;
1084       }
1085       return _tokens.get(_pos);
1086     }
1087 
1088     public Token next() {
1089       if(!hasNext()) {
1090         throw new ParseException(
1091             "Unexpected end of expression " + this);
1092       }
1093       return _tokens.get(_pos++);
1094     }
1095 
1096     public void reset(int pos) {
1097       _pos = pos;
1098     }
1099 
1100     public TokBuf subBuf(int start, int end) {
1101       return new TokBuf(_tokens.subList(start, end), this, start);
1102     }
1103 
1104     public void setPendingExpr(Expr expr) {
1105       if(_pendingExpr != null) {
1106         throw new ParseException(
1107             "Found multiple expressions with no operator " + this);
1108       }
1109       _pendingExpr = expr.resolveOrderOfOperations();
1110     }
1111 
1112     public void restorePendingExpr(Expr expr) {
1113       // this is an expression which was previously set, so no need to re-resolve
1114       _pendingExpr = expr;
1115     }
1116 
1117     public Expr takePendingExpr() {
1118       Expr expr = _pendingExpr;
1119       _pendingExpr = null;
1120       return expr;
1121     }
1122 
1123     public boolean hasPendingExpr() {
1124       return (_pendingExpr != null);
1125     }
1126 
1127     private Map.Entry<Integer,List<Token>> getTopPos() {
1128       int pos = _pos;
1129       List<Token> toks = _tokens;
1130       TokBuf cur = this;
1131       while(cur._parent != null) {
1132         pos += cur._parentOff;
1133         cur = cur._parent;
1134         toks = cur._tokens;
1135       }
1136       return ExpressionTokenizer.newEntry(pos, toks);
1137     }
1138 
1139     public Function getFunction(String funcName) {
1140       return _ctx.getFunctionLookup().getFunction(funcName);
1141     }
1142 
1143     @Override
1144     public String toString() {
1145 
1146       Map.Entry<Integer,List<Token>> e = getTopPos();
1147 
1148       // TODO actually format expression?
1149       StringBuilder sb = new StringBuilder()
1150         .append("[token ").append(e.getKey()).append("] (");
1151 
1152       for(Iterator<Token> iter = e.getValue().iterator(); iter.hasNext(); ) {
1153         Token t = iter.next();
1154         sb.append("'").append(t.getValueStr()).append("'");
1155         if(iter.hasNext()) {
1156           sb.append(",");
1157         }
1158       }
1159 
1160       sb.append(")");
1161 
1162       if(_pendingExpr != null) {
1163         sb.append(" [pending '").append(_pendingExpr.toDebugString(_ctx))
1164           .append("']");
1165       }
1166 
1167       return sb.toString();
1168     }
1169   }
1170 
1171   private static boolean isHigherPrecendence(OpType op1, OpType op2) {
1172     int prec1 = PRECENDENCE.get(op1);
1173     int prec2 = PRECENDENCE.get(op2);
1174 
1175     // higher preceendence ops have lower numbers
1176     return (prec1 < prec2);
1177   }
1178 
1179   private static final Map<OpType, Integer> buildPrecedenceMap(
1180       OpType[]... opArrs) {
1181     Map<OpType, Integer> prec = new HashMap<>();
1182 
1183     int level = 0;
1184     for(OpType[] ops : opArrs) {
1185       for(OpType op : ops) {
1186         prec.put(op, level);
1187       }
1188       ++level;
1189     }
1190 
1191     return prec;
1192   }
1193 
1194   private static void exprListToString(
1195       List<Expr> exprs, String sep, LocaleContext ctx, StringBuilder sb,
1196       boolean isDebug) {
1197     Iterator<Expr> iter = exprs.iterator();
1198     iter.next().toString(ctx, sb, isDebug);
1199     while(iter.hasNext()) {
1200       sb.append(sep);
1201       iter.next().toString(ctx, sb, isDebug);
1202     }
1203   }
1204 
1205   private static Value[] exprListToValues(
1206       List<Expr> exprs, EvalContext ctx) {
1207     Valueess/expr/Value.html#Value">Value[] paramVals = new Value[exprs.size()];
1208     for(int i = 0; i < exprs.size(); ++i) {
1209       paramVals[i] = exprs.get(i).eval(ctx);
1210     }
1211     return paramVals;
1212   }
1213 
1214   private static Value[] exprListToDelayedValues(
1215       List<Expr> exprs, EvalContext ctx) {
1216     Valueess/expr/Value.html#Value">Value[] paramVals = new Value[exprs.size()];
1217     for(int i = 0; i < exprs.size(); ++i) {
1218       paramVals[i] = new DelayedValue(exprs.get(i), ctx);
1219     }
1220     return paramVals;
1221   }
1222 
1223   private static boolean areConstant(List<Expr> exprs) {
1224     for(Expr expr : exprs) {
1225       if(!expr.isConstant()) {
1226         return false;
1227       }
1228     }
1229     return true;
1230   }
1231 
1232   private static boolean areConstant(Expr... exprs) {
1233     for(Expr expr : exprs) {
1234       if(!expr.isConstant()) {
1235         return false;
1236       }
1237     }
1238     return true;
1239   }
1240 
1241   private static void literalStrToString(String str, StringBuilder sb) {
1242     sb.append("\"")
1243       .append(StringUtil.replace(str, "\"", "\"\""))
1244       .append("\"");
1245   }
1246 
1247   /**
1248    * Converts an ms access like pattern to a java regex, always matching case
1249    * insensitively.
1250    */
1251   public static Pattern likePatternToRegex(String pattern) {
1252 
1253     StringBuilder sb = new StringBuilder(pattern.length());
1254 
1255     // Access LIKE pattern supports (note, matching is case-insensitive):
1256     // - '*' -> 0 or more chars
1257     // - '?' -> single character
1258     // - '#' -> single digit
1259     // - '[...]' -> character class, '[!...]' -> not in char class
1260 
1261     for(int i = 0; i < pattern.length(); ++i) {
1262       char c = pattern.charAt(i);
1263 
1264       if(c == '*') {
1265         sb.append(".*");
1266       } else if(c == '?') {
1267         sb.append('.');
1268       } else if(c == '#') {
1269         sb.append("\\d");
1270       } else if(c == '[') {
1271 
1272         // find closing brace
1273         int startPos = i + 1;
1274         int endPos = -1;
1275         for(int j = startPos; j < pattern.length(); ++j) {
1276           if(pattern.charAt(j) == ']') {
1277             endPos = j;
1278             break;
1279           }
1280         }
1281 
1282         // access treats invalid expression like "unmatchable"
1283         if(endPos == -1) {
1284           return UNMATCHABLE_REGEX;
1285         }
1286 
1287         String charClass = pattern.substring(startPos, endPos);
1288 
1289         if((charClass.length() > 0) && (charClass.charAt(0) == '!')) {
1290           // this is a negated char class
1291           charClass = '^' + charClass.substring(1);
1292         }
1293 
1294         sb.append('[').append(charClass).append(']');
1295         i += (endPos - startPos) + 1;
1296 
1297       } else if(isRegexSpecialChar(c)) {
1298         // this char is special in regexes, so escape it
1299         sb.append('\\').append(c);
1300       } else {
1301         sb.append(c);
1302       }
1303     }
1304 
1305     try {
1306       return Pattern.compile(sb.toString(),
1307                              Pattern.CASE_INSENSITIVE | Pattern.DOTALL |
1308                              Pattern.UNICODE_CASE);
1309     } catch(PatternSyntaxException ignored) {
1310       return UNMATCHABLE_REGEX;
1311     }
1312   }
1313 
1314   public static boolean isRegexSpecialChar(char c) {
1315     return REGEX_SPEC_CHARS.contains(c);
1316   }
1317 
1318   private static Value toLiteralValue(Value.Type valType, Object value) {
1319     switch(valType) {
1320     case STRING:
1321       return ValueSupport.toValue((String)value);
1322     case DATE:
1323     case TIME:
1324     case DATE_TIME:
1325       return ValueSupport.toValue(valType, (LocalDateTime)value);
1326     case LONG:
1327       return ValueSupport.toValue((Integer)value);
1328     case DOUBLE:
1329       return ValueSupport.toValue((Double)value);
1330     case BIG_DEC:
1331       return ValueSupport.toValue((BigDecimal)value);
1332     default:
1333       throw new ParseException("unexpected literal type " + valType);
1334     }
1335   }
1336 
1337   private static boolean isLiteralDefaultValue(
1338       TokBuf buf, Value.Type resultType, String exprStr) {
1339 
1340     // if a default value expression does not start with an '=' and is used in
1341     // a string context, then it is taken as a literal value unless it starts
1342     // with a " char
1343 
1344     if(buf.getExprType() != Type.DEFAULT_VALUE) {
1345       return false;
1346     }
1347 
1348     // a leading "=" indicates "full" expression handling for a DEFAULT_VALUE
1349     // (consume this value once we detect it)
1350     if(isOp(buf.peekNext(), "=")) {
1351       buf.next();
1352       return false;
1353     }
1354 
1355     return((resultType == Value.Type.STRING) &&
1356            ((exprStr.length() == 0) ||
1357             (exprStr.charAt(0) != ExpressionTokenizer.QUOTED_STR_CHAR)));
1358   }
1359 
1360   private interface LeftAssocExpr {
1361     public OpType getOp();
1362     public Expr getLeft();
1363     public void setLeft(Expr left);
1364   }
1365 
1366   private interface RightAssocExpr {
1367     public OpType getOp();
1368     public Expr getRight();
1369     public void setRight(Expr right);
1370   }
1371 
1372   private static final class DelayedValue extends BaseDelayedValue
1373   {
1374     private final Expr _expr;
1375     private final EvalContext _ctx;
1376 
1377     private DelayedValue(Expr expr, EvalContext ctx) {
1378       _expr = expr;
1379       _ctx = ctx;
1380     }
1381 
1382     @Override
1383     public Value eval() {
1384       return _expr.eval(_ctx);
1385     }
1386   }
1387 
1388 
1389   private static abstract class Expr
1390   {
1391     public String toCleanString(LocaleContext ctx) {
1392       return toString(ctx, new StringBuilder(), false).toString();
1393     }
1394 
1395     public String toDebugString(LocaleContext ctx) {
1396       return toString(ctx, new StringBuilder(), true).toString();
1397     }
1398 
1399     protected boolean isValidationExpr() {
1400       return false;
1401     }
1402 
1403     protected StringBuilder toString(
1404         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1405       if(isDebug) {
1406         sb.append("<").append(getClass().getSimpleName()).append(">{");
1407       }
1408       toExprString(ctx, sb, isDebug);
1409       if(isDebug) {
1410         sb.append("}");
1411       }
1412       return sb;
1413     }
1414 
1415     protected Expr resolveOrderOfOperations() {
1416 
1417       if(!(this instanceof LeftAssocExpr)) {
1418         // nothing we can do
1419         return this;
1420       }
1421 
1422       // in order to get the precedence right, we need to first associate this
1423       // expression with the "rightmost" expression preceding it, then adjust
1424       // this expression "down" (lower precedence) as the precedence of the
1425       // operations dictates.  since we parse from left to right, the initial
1426       // "left" value isn't the immediate left expression, instead it's based
1427       // on how the preceding operator precedence worked out.  we need to
1428       // adjust "this" expression to the closest preceding expression before
1429       // we can correctly resolve precedence.
1430 
1431       Expr outerExpr = this;
1432       final LeftAssocExpr thisExpr = (LeftAssocExpr)this;
1433       final Expr thisLeft = thisExpr.getLeft();
1434 
1435       // current: <this>{<left>{A op1 B} op2 <right>{C}}
1436       if(thisLeft instanceof RightAssocExpr) {
1437 
1438         RightAssocExpr leftOp = (RightAssocExpr)thisLeft;
1439 
1440         // target: <left>{A op1 <this>{B op2 <right>{C}}}
1441 
1442         thisExpr.setLeft(leftOp.getRight());
1443 
1444         // give the new version of this expression an opportunity to further
1445         // swap (since the swapped expression may itself be a binary
1446         // expression)
1447         leftOp.setRight(resolveOrderOfOperations());
1448         outerExpr = thisLeft;
1449 
1450         // at this point, this expression has been pushed all the way to the
1451         // rightmost preceding expression (we artifically gave "this" the
1452         // highest precedence).  now, we want to adjust precedence as
1453         // necessary (shift it back down if the operator precedence is
1454         // incorrect).  note, we only need to check precedence against "this",
1455         // as all other precedence has been resolved in previous parsing
1456         // rounds.
1457         if((leftOp.getRight() == this) &&
1458            !isHigherPrecendence(thisExpr.getOp(), leftOp.getOp())) {
1459 
1460           // doh, "this" is lower (or the same) precedence, restore the
1461           // original order of things
1462           leftOp.setRight(thisExpr.getLeft());
1463           thisExpr.setLeft(thisLeft);
1464           outerExpr = this;
1465         }
1466       }
1467 
1468       return outerExpr;
1469     }
1470 
1471     public abstract boolean isConstant();
1472 
1473     public abstract Value eval(EvalContext ctx);
1474 
1475     public abstract void collectIdentifiers(Collection<Identifier> identifiers);
1476 
1477     protected abstract void toExprString(
1478         LocaleContext ctx, StringBuilder sb, boolean isDebug);
1479   }
1480 
1481   private static final class EConstValue extends Expr
1482   {
1483     private final Value _val;
1484     private final String _str;
1485 
1486     private EConstValue(Value val, String str) {
1487       _val = val;
1488       _str = str;
1489     }
1490 
1491     @Override
1492     public boolean isConstant() {
1493       return true;
1494     }
1495 
1496     @Override
1497     public Value eval(EvalContext ctx) {
1498       return _val;
1499     }
1500 
1501     @Override
1502     public void collectIdentifiers(Collection<Identifier> identifiers) {
1503       // none
1504     }
1505 
1506     @Override
1507     protected void toExprString(
1508         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1509       sb.append(_str);
1510     }
1511   }
1512 
1513   private static final class EThisValue extends Expr
1514   {
1515     @Override
1516     public boolean isConstant() {
1517       return false;
1518     }
1519     @Override
1520     public Value eval(EvalContext ctx) {
1521       return ctx.getThisColumnValue();
1522     }
1523     @Override
1524     public void collectIdentifiers(Collection<Identifier> identifiers) {
1525       // none
1526     }
1527     @Override
1528     protected void toExprString(
1529         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1530       if(isDebug) {
1531         sb.append("<THIS_COL>");
1532       }
1533     }
1534   }
1535 
1536   private static final class ELiteralValue extends Expr
1537   {
1538     private final Value _val;
1539 
1540     private ELiteralValue(Value.Type valType, Object value) {
1541       _val = toLiteralValue(valType, value);
1542     }
1543 
1544     @Override
1545     public boolean isConstant() {
1546       return true;
1547     }
1548 
1549     @Override
1550     public Value eval(EvalContext ctx) {
1551       return _val;
1552     }
1553 
1554     @Override
1555     public void collectIdentifiers(Collection<Identifier> identifiers) {
1556       // none
1557     }
1558 
1559     @Override
1560     protected void toExprString(
1561         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1562       if(_val.getType() == Value.Type.STRING) {
1563         literalStrToString((String)_val.get(), sb);
1564       } else if(_val.getType().isTemporal()) {
1565         sb.append("#").append(_val.getAsString(ctx)).append("#");
1566       } else {
1567         sb.append(_val.get());
1568       }
1569     }
1570   }
1571 
1572   private static final class EObjValue extends Expr
1573   {
1574     private final Identifier _identifier;
1575 
1576     private EObjValue(Identifier identifier) {
1577       _identifier = identifier;
1578     }
1579 
1580     @Override
1581     public boolean isConstant() {
1582       return false;
1583     }
1584 
1585     @Override
1586     public Value eval(EvalContext ctx) {
1587       return ctx.getIdentifierValue(_identifier);
1588     }
1589 
1590     @Override
1591     public void collectIdentifiers(Collection<Identifier> identifiers) {
1592       identifiers.add(_identifier);
1593     }
1594 
1595     @Override
1596     protected void toExprString(
1597         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1598       sb.append(_identifier);
1599     }
1600   }
1601 
1602   private static class EParen extends Expr
1603   {
1604     private final Expr _expr;
1605 
1606     private EParen(Expr expr) {
1607       _expr = expr;
1608     }
1609 
1610     @Override
1611     public boolean isConstant() {
1612       return _expr.isConstant();
1613     }
1614 
1615     @Override
1616     protected boolean isValidationExpr() {
1617       return _expr.isValidationExpr();
1618     }
1619 
1620     @Override
1621     public Value eval(EvalContext ctx) {
1622       return _expr.eval(ctx);
1623     }
1624 
1625     @Override
1626     public void collectIdentifiers(Collection<Identifier> identifiers) {
1627       _expr.collectIdentifiers(identifiers);
1628     }
1629 
1630     @Override
1631     protected void toExprString(
1632         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1633       sb.append("(");
1634       _expr.toString(ctx, sb, isDebug);
1635       sb.append(")");
1636     }
1637   }
1638 
1639   private static class EFunc extends Expr
1640   {
1641     private final Function _func;
1642     private final List<Expr> _params;
1643 
1644     private EFunc(Function func, List<Expr> params) {
1645       _func = func;
1646       _params = params;
1647     }
1648 
1649     @Override
1650     public boolean isConstant() {
1651       return _func.isPure() && areConstant(_params);
1652     }
1653 
1654     @Override
1655     public Value eval(EvalContext ctx) {
1656       return _func.eval(ctx, exprListToValues(_params, ctx));
1657     }
1658 
1659     @Override
1660     public void collectIdentifiers(Collection<Identifier> identifiers) {
1661       for(Expr param : _params) {
1662         param.collectIdentifiers(identifiers);
1663       }
1664     }
1665 
1666     @Override
1667     protected void toExprString(
1668         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1669       sb.append(_func.getName()).append("(");
1670 
1671       if(!_params.isEmpty()) {
1672         exprListToString(_params, ",", ctx, sb, isDebug);
1673       }
1674 
1675       sb.append(")");
1676     }
1677   }
1678 
1679   private static abstract class EBaseBinaryOp extends Expr
1680     implements LeftAssocExpr, RightAssocExpr
1681   {
1682     protected final OpType _op;
1683     protected Expr _left;
1684     protected Expr _right;
1685 
1686     private EBaseBinaryOp(OpType op, Expr left, Expr right) {
1687       _op = op;
1688       _left = left;
1689       _right = right;
1690     }
1691 
1692     @Override
1693     public boolean isConstant() {
1694       return areConstant(_left, _right);
1695     }
1696 
1697     @Override
1698     public OpType getOp() {
1699       return _op;
1700     }
1701 
1702     @Override
1703     public Expr getLeft() {
1704       return _left;
1705     }
1706 
1707     @Override
1708     public void setLeft(Expr left) {
1709       _left = left;
1710     }
1711 
1712     @Override
1713     public Expr getRight() {
1714       return _right;
1715     }
1716 
1717     @Override
1718     public void setRight(Expr right) {
1719       _right = right;
1720     }
1721 
1722     @Override
1723     public void collectIdentifiers(Collection<Identifier> identifiers) {
1724       _left.collectIdentifiers(identifiers);
1725       _right.collectIdentifiers(identifiers);
1726     }
1727 
1728     @Override
1729     protected void toExprString(
1730         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1731       appendLeadingExpr(_left, ctx, sb, isDebug)
1732         .append(_op).append(" ");
1733       _right.toString(ctx, sb, isDebug);
1734     }
1735   }
1736 
1737   private static class EBinaryOp extends EBaseBinaryOp
1738   {
1739     private EBinaryOp(BinaryOp op, Expr left, Expr right) {
1740       super(op, left, right);
1741     }
1742 
1743     @Override
1744     public Value eval(EvalContext ctx) {
1745       return ((BinaryOp)_op).eval(ctx, _left.eval(ctx), _right.eval(ctx));
1746     }
1747   }
1748 
1749   private static class EUnaryOp extends Expr
1750     implements RightAssocExpr
1751   {
1752     private final OpType _op;
1753     private Expr _expr;
1754 
1755     private EUnaryOp(UnaryOp op, Expr expr) {
1756       _op = op;
1757       _expr = expr;
1758     }
1759 
1760     @Override
1761     public boolean isConstant() {
1762       return _expr.isConstant();
1763     }
1764 
1765     @Override
1766     public OpType getOp() {
1767       return _op;
1768     }
1769 
1770     @Override
1771     public Expr getRight() {
1772       return _expr;
1773     }
1774 
1775     @Override
1776     public void setRight(Expr right) {
1777       _expr = right;
1778     }
1779 
1780     @Override
1781     public Value eval(EvalContext ctx) {
1782       return ((UnaryOp)_op).eval(ctx, _expr.eval(ctx));
1783     }
1784 
1785     @Override
1786     public void collectIdentifiers(Collection<Identifier> identifiers) {
1787       _expr.collectIdentifiers(identifiers);
1788     }
1789 
1790     @Override
1791     protected void toExprString(
1792         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1793       sb.append(_op);
1794       if(isDebug || ((UnaryOp)_op).needsSpace()) {
1795         sb.append(" ");
1796       }
1797       _expr.toString(ctx, sb, isDebug);
1798     }
1799   }
1800 
1801   private static class ECompOp extends EBaseBinaryOp
1802   {
1803     private ECompOp(CompOp op, Expr left, Expr right) {
1804       super(op, left, right);
1805     }
1806 
1807     @Override
1808     protected boolean isValidationExpr() {
1809       return true;
1810     }
1811 
1812     @Override
1813     public Value eval(EvalContext ctx) {
1814       return ((CompOp)_op).eval(ctx, _left.eval(ctx), _right.eval(ctx));
1815     }
1816   }
1817 
1818   private static class EImplicitCompOp extends ECompOp
1819   {
1820     private EImplicitCompOp(Expr right) {
1821       super(CompOp.EQ, THIS_COL_VALUE, right);
1822     }
1823 
1824     @Override
1825     protected void toExprString(
1826         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1827       // only output the full "implicit" comparison in debug mode
1828       if(isDebug) {
1829         super.toExprString(ctx, sb, isDebug);
1830       } else {
1831         // just output the explicit part of the expression
1832         _right.toString(ctx, sb, isDebug);
1833       }
1834     }
1835   }
1836 
1837   private static class ELogicalOp extends EBaseBinaryOp
1838   {
1839     private ELogicalOp(LogOp op, Expr left, Expr right) {
1840       super(op, left, right);
1841     }
1842 
1843     @Override
1844     protected boolean isValidationExpr() {
1845       return true;
1846     }
1847 
1848     @Override
1849     public Value eval(final EvalContext ctx) {
1850 
1851       // logical operations do short circuit evaluation, so we need to delay
1852       // computing results until necessary
1853       return ((LogOp)_op).eval(ctx, new DelayedValue(_left, ctx),
1854                                new DelayedValue(_right, ctx));
1855     }
1856   }
1857 
1858   private static abstract class ESpecOp extends Expr
1859     implements LeftAssocExpr
1860   {
1861     protected final SpecOp _op;
1862     protected Expr _expr;
1863 
1864     private ESpecOp(SpecOp op, Expr expr) {
1865       _op = op;
1866       _expr = expr;
1867     }
1868 
1869     @Override
1870     public boolean isConstant() {
1871       return _expr.isConstant();
1872     }
1873 
1874     @Override
1875     public OpType getOp() {
1876       return _op;
1877     }
1878 
1879     @Override
1880     public Expr getLeft() {
1881       return _expr;
1882     }
1883 
1884     @Override
1885     public void setLeft(Expr left) {
1886       _expr = left;
1887     }
1888 
1889     @Override
1890     public void collectIdentifiers(Collection<Identifier> identifiers) {
1891       _expr.collectIdentifiers(identifiers);
1892     }
1893 
1894     @Override
1895     protected boolean isValidationExpr() {
1896       return true;
1897     }
1898   }
1899 
1900   private static class ENullOp extends ESpecOp
1901   {
1902     private ENullOp(SpecOp op, Expr expr) {
1903       super(op, expr);
1904     }
1905 
1906     @Override
1907     public Value eval(EvalContext ctx) {
1908       return _op.eval(ctx, _expr.eval(ctx), null, null);
1909     }
1910 
1911     @Override
1912     protected void toExprString(
1913         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1914       appendLeadingExpr(_expr, ctx, sb, isDebug)
1915         .append(_op);
1916     }
1917   }
1918 
1919   private static class ELikeOp extends ESpecOp
1920   {
1921     private final String _patternStr;
1922     private Pattern _pattern;
1923 
1924     private ELikeOp(SpecOp op, Expr expr, String patternStr) {
1925       super(op, expr);
1926       _patternStr = patternStr;
1927     }
1928 
1929     private Pattern getPattern()
1930     {
1931       if(_pattern == null) {
1932         _pattern = likePatternToRegex(_patternStr);
1933       }
1934       return _pattern;
1935     }
1936 
1937     @Override
1938     public Value eval(EvalContext ctx) {
1939       return _op.eval(ctx, _expr.eval(ctx), getPattern(), null);
1940     }
1941 
1942     @Override
1943     protected void toExprString(
1944         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1945       appendLeadingExpr(_expr, ctx, sb, isDebug)
1946         .append(_op).append(" ");
1947       literalStrToString(_patternStr, sb);
1948       if(isDebug) {
1949         sb.append("(").append(getPattern()).append(")");
1950       }
1951     }
1952   }
1953 
1954   private static class EInOp extends ESpecOp
1955   {
1956     private final List<Expr> _exprs;
1957 
1958     private EInOp(SpecOp op, Expr expr, List<Expr> exprs) {
1959       super(op, expr);
1960       _exprs = exprs;
1961     }
1962 
1963     @Override
1964     public boolean isConstant() {
1965       return super.isConstant() && areConstant(_exprs);
1966     }
1967 
1968     @Override
1969     public Value eval(EvalContext ctx) {
1970       return _op.eval(ctx, _expr.eval(ctx),
1971                       exprListToDelayedValues(_exprs, ctx), null);
1972     }
1973 
1974     @Override
1975     public void collectIdentifiers(Collection<Identifier> identifiers) {
1976       for(Expr expr : _exprs) {
1977         expr.collectIdentifiers(identifiers);
1978       }
1979     }
1980 
1981     @Override
1982     protected void toExprString(
1983         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
1984       appendLeadingExpr(_expr, ctx, sb, isDebug)
1985         .append(_op).append(" (");
1986       exprListToString(_exprs, ",", ctx, sb, isDebug);
1987       sb.append(")");
1988     }
1989   }
1990 
1991   private static class EBetweenOp extends ESpecOp
1992     implements RightAssocExpr
1993   {
1994     private final Expr _startRangeExpr;
1995     private Expr _endRangeExpr;
1996 
1997     private EBetweenOp(SpecOp op, Expr expr, Expr startRangeExpr,
1998                        Expr endRangeExpr) {
1999       super(op, expr);
2000       _startRangeExpr = startRangeExpr;
2001       _endRangeExpr = endRangeExpr;
2002     }
2003 
2004     @Override
2005     public boolean isConstant() {
2006       return _expr.isConstant() && areConstant(_startRangeExpr, _endRangeExpr);
2007     }
2008 
2009     @Override
2010     public Expr getRight() {
2011       return _endRangeExpr;
2012     }
2013 
2014     @Override
2015     public void setRight(Expr right) {
2016       _endRangeExpr = right;
2017     }
2018 
2019     @Override
2020     public Value eval(EvalContext ctx) {
2021       return _op.eval(ctx, _expr.eval(ctx),
2022                       new DelayedValue(_startRangeExpr, ctx),
2023                       new DelayedValue(_endRangeExpr, ctx));
2024     }
2025 
2026     @Override
2027     public void collectIdentifiers(Collection<Identifier> identifiers) {
2028       super.collectIdentifiers(identifiers);
2029       _startRangeExpr.collectIdentifiers(identifiers);
2030       _endRangeExpr.collectIdentifiers(identifiers);
2031     }
2032 
2033     @Override
2034     protected void toExprString(
2035         LocaleContext ctx, StringBuilder sb, boolean isDebug) {
2036       appendLeadingExpr(_expr, ctx, sb, isDebug)
2037         .append(_op).append(" ");
2038       _startRangeExpr.toString(ctx, sb, isDebug);
2039       sb.append(" And ");
2040       _endRangeExpr.toString(ctx, sb, isDebug);
2041     }
2042   }
2043 
2044   /**
2045    * Base Expression wrapper for an Expr.
2046    */
2047   private static abstract class BaseExprWrapper implements Expression
2048   {
2049     private final String _rawExprStr;
2050     private final Expr _expr;
2051 
2052     private BaseExprWrapper(String rawExprStr, Expr expr) {
2053       _rawExprStr = rawExprStr;
2054       _expr = expr;
2055     }
2056 
2057     @Override
2058     public String toDebugString(LocaleContext ctx) {
2059       return _expr.toDebugString(ctx);
2060     }
2061 
2062     @Override
2063     public String toRawString() {
2064       return _rawExprStr;
2065     }
2066 
2067     @Override
2068     public String toCleanString(LocaleContext ctx) {
2069       return _expr.toCleanString(ctx);
2070     }
2071 
2072     @Override
2073     public boolean isConstant() {
2074       return _expr.isConstant();
2075     }
2076 
2077     @Override
2078     public void collectIdentifiers(Collection<Identifier> identifiers) {
2079       _expr.collectIdentifiers(identifiers);
2080     }
2081 
2082     @Override
2083     public String toString() {
2084       return toRawString();
2085     }
2086 
2087     protected Object evalValue(Value.Type resultType, EvalContext ctx) {
2088       Value val = _expr.eval(ctx);
2089 
2090       if(val.isNull()) {
2091         return null;
2092       }
2093 
2094       if(resultType == null) {
2095         // return as "native" type
2096         return val.get();
2097       }
2098 
2099       // FIXME possibly do some type coercion.  are there conversions here which don't work elsewhere? (string -> date, string -> number)?
2100       switch(resultType) {
2101       case STRING:
2102         return val.getAsString(ctx);
2103       case DATE:
2104       case TIME:
2105       case DATE_TIME:
2106         return val.getAsLocalDateTime(ctx);
2107       case LONG:
2108         return val.getAsLongInt(ctx);
2109       case DOUBLE:
2110         return val.getAsDouble(ctx);
2111       case BIG_DEC:
2112         return val.getAsBigDecimal(ctx);
2113       default:
2114         throw new IllegalStateException("unexpected result type " + resultType);
2115       }
2116     }
2117 
2118     protected Boolean evalCondition(EvalContext ctx) {
2119       Value val = _expr.eval(ctx);
2120 
2121       if(val.isNull()) {
2122         // null can't be coerced to a boolean
2123         throw new EvalException("Condition evaluated to Null");
2124       }
2125 
2126       return val.getAsBoolean(ctx);
2127     }
2128   }
2129 
2130   /**
2131    * Expression wrapper for an Expr which returns a value.
2132    */
2133   private static class ExprWrapper extends BaseExprWrapper
2134   {
2135     private final Value.Type _resultType;
2136 
2137     private ExprWrapper(String rawExprStr, Expr expr, Value.Type resultType) {
2138       super(rawExprStr, expr);
2139       _resultType = resultType;
2140     }
2141 
2142     @Override
2143     public Object eval(EvalContext ctx) {
2144       return evalValue(_resultType, ctx);
2145     }
2146   }
2147 
2148   /**
2149    * Expression wrapper for an Expr which returns a Boolean from a conditional
2150    * expression.
2151    */
2152   private static class CondExprWrapper extends BaseExprWrapper
2153   {
2154     private CondExprWrapper(String rawExprStr, Expr expr) {
2155       super(rawExprStr, expr);
2156     }
2157 
2158     @Override
2159     public Object eval(EvalContext ctx) {
2160       return evalCondition(ctx);
2161     }
2162   }
2163 
2164   /**
2165    * Expression wrapper for a <i>pure</i> Expr which caches the result of
2166    * evaluation.
2167    */
2168   private static final class MemoizedExprWrapper extends ExprWrapper
2169   {
2170     private Object _val;
2171 
2172     private MemoizedExprWrapper(String rawExprStr, Expr expr,
2173                                 Value.Type resultType) {
2174       super(rawExprStr, expr, resultType);
2175     }
2176 
2177     @Override
2178     public Object eval(EvalContext ctx) {
2179       if(_val == null) {
2180         _val = super.eval(ctx);
2181       }
2182       return _val;
2183     }
2184   }
2185 
2186   /**
2187    * Expression wrapper for a <i>pure</i> conditional Expr which caches the
2188    * result of evaluation.
2189    */
2190   private static final class MemoizedCondExprWrapper extends CondExprWrapper
2191   {
2192     private Object _val;
2193 
2194     private MemoizedCondExprWrapper(String rawExprStr, Expr expr) {
2195       super(rawExprStr, expr);
2196     }
2197 
2198     @Override
2199     public Object eval(EvalContext ctx) {
2200       if(_val == null) {
2201         _val = super.eval(ctx);
2202       }
2203       return _val;
2204     }
2205   }
2206 }