View Javadoc
1   package com.randomnoun.common.jexl.sql;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   import java.text.SimpleDateFormat;
7   import java.util.ArrayList;
8   import java.util.Date;
9   import java.util.Enumeration;
10  import java.util.List;
11  
12  import org.apache.log4j.Logger;
13  
14  import com.randomnoun.common.Text;
15  import com.randomnoun.common.jexl.DateSpan;
16  import com.randomnoun.common.jexl.EvalFallbackException;
17  import com.randomnoun.common.jexl.ast.AdditiveExpression;
18  import com.randomnoun.common.jexl.ast.ArgumentList;
19  import com.randomnoun.common.jexl.ast.Arguments;
20  import com.randomnoun.common.jexl.ast.BooleanLiteral;
21  import com.randomnoun.common.jexl.ast.ConditionalAndExpression;
22  import com.randomnoun.common.jexl.ast.EqualityExpression;
23  import com.randomnoun.common.jexl.ast.Expression;
24  import com.randomnoun.common.jexl.ast.FunctionCall;
25  import com.randomnoun.common.jexl.ast.Literal;
26  import com.randomnoun.common.jexl.ast.MultiplicativeExpression;
27  import com.randomnoun.common.jexl.ast.Name;
28  import com.randomnoun.common.jexl.ast.NodeChoice;
29  import com.randomnoun.common.jexl.ast.NodeSequence;
30  import com.randomnoun.common.jexl.ast.NodeToken;
31  import com.randomnoun.common.jexl.ast.NullLiteral;
32  import com.randomnoun.common.jexl.ast.PrimaryExpression;
33  import com.randomnoun.common.jexl.ast.RelationalExpression;
34  import com.randomnoun.common.jexl.ast.TopLevelExpression;
35  import com.randomnoun.common.jexl.ast.UnaryExpression;
36  import com.randomnoun.common.jexl.eval.EvalContext;
37  import com.randomnoun.common.jexl.eval.EvalException;
38  import com.randomnoun.common.jexl.eval.EvalFunction;
39  import com.randomnoun.common.jexl.eval.Evaluator;
40  
41  
42  /**
43   * This class traverses a TopLevelExpression object and translates the AST graph
44   * into a text representation that forms the 'WHERE' clause of an SQL query.
45   *
46   * <p>Only a subset of expressions are supported by the SQL Generator; an
47   * EvalException is thrown if an invalid expression is supplied.
48   *
49   * <p>The context passed into this class contains:
50   * <ul>
51   *   <li>internal parameters that effect how the SQL is generated (such as the database vendor)
52   *   <li>evaluatable functions (run at translation time)
53   *   <li>translatable functions (converted into SQL at translation time)
54   *   <li>database columns used in the SQL
55   *   <li>variables (which are substituted at <i>translation</i> time into the value
56   *     of that variable)
57   *   <li>positional parameters (which are substituted at <i>execution</i> time into
58   *     JDBC positional parameters)
59   * </ul>
60   *
61   * <h3>Variables</h3>
62   *
63   * It's probably worth going into the difference between the two different types of variables.
64   * If I have a variable that I know the value of at translation time; e.g. the variable
65   * <code>messageVar</code> is <code>MT500</code>, I can generate standalone SQL that uses this variable:
66   *
67   * <pre style="code">
68   *   // [1] parse the expression into an AST
69   *   String exprString = "externalMessageType == messageVar";
70   *   ExpressionParser parser = new ExpressionParser(new StringReader(exprString));
71   *   TopLevelExpression expr = parser.TopLevelExpression();
72   *
73   *   // [2] set up an evaluation context to define variables
74   *   EvalContext context = new EvalContext();
75   *
76   *   // [3] set up database columns used in the query
77   *   //     note the first 'externalMessageType' in this line refers to the name
78   *   //     of the column in exprString, and the second 'externalMessageType' refers
79   *   //     to the name of the column in the table we're querying. It's clearer
80   *   //     if these are set to the same value, but there's no other reason they have to.
81   *   context.setVariable("externalMessageType", new SqlColumn("externalMessageType"));
82   *
83   *   // [4] set up variables used in the query
84   *   context.setVariable("messageVar", "MT500");
85   *
86   *   // [5] generate the SQL
87   *   SqlGenerator generator = new SqlGenerator();
88   *   String result = (String) generator.visit(expr, context);
89   * </pre>
90   *
91   * <p>The result here being <code>externalMessageType = 'MT500'</code>.
92   *
93   * <p>If, however, I won't know <code>messageVar</code> until execution time, I can still
94   * generate the SQL using a PositionalParameter object:
95   *
96   * <pre style="code">
97   *   // parse the expression and setup context as per [1], [2] and [3] above
98   *
99   *   // [4] set up an evaluation context to define variables
100  *   context.setVariable("messageVar", new PositionalParameter("messageVar"));
101  *
102  *   // [5] generate the SQL as above
103  *   SqlGenerator generator = new SqlGenerator();
104  *   String result = (String) generator.visit(expr, context);
105  * </pre>
106  *
107  * The result here being <code>externalMessageType = ?</code> instead. The value of the
108  * '<code>?</code>' in the result is supplied
109  * by the program at execution time, using standard JDBC positional parameters. But, you're
110  * probably thinking, how do I know <i>which</i> positional parameter to use? After all,
111  * I could have had more than one PositionalParameter's in the context, and they may
112  * appear in any order or any number of times in the result String. To determine how to
113  * supply these values to JDBC, the context variable VAR_PARAMETERS is set to a List of
114  * Strings, each being the name of the positional parameter above. In the example above,
115  * if we then executed
116  *
117  * <pre style="code">
118  *   List parameList = (List) context.getVariable(SqlGenerator.VAR_PARAMETERS);
119  * </pre>
120  *
121  * this will set paramList to a one-element list, containing the String "messageVar"
122  * (corresponding to the single '<code>?</code>' in the result String). We can
123  * iterate over this list to create the actual parameters to pass into JDBC when running
124  * the query.
125  *
126  * <h3>Static dates</h3>
127  * If a date is inserted into an SQL query, we need to know which database we're running
128  * on, since each vendor seems to have their own way of representing fixed dates. This is
129  * set using the VAR_DATABASE_TYPE variable in the ExpressionContext, which is set to
130  * either DATABASE_ORACLE, DATABASE_DB2, DATABASE_SQLSERVER or DATABASE_MYSQL.
131  *
132  * <h3>Evaluatable functions</h3>
133  * Any functions which implement EvalFunction are evaluated at translation time, (i.e.
134  * are not translated into SQL). This is useful for things like nextWorkingDay(), which
135  * need to execute before the SQL is passed through to the database.
136  *
137  * <h3>Translatable functions</h3>
138  * Any functions which implement SqlFunction are translated into SQL and evaluated by
139  * the database. This is useful for things like between() and like(), which are
140  * converted into relational operators (&lt; and &gt;) and LIKE operators.
141  *
142  * <p>If a function implements both EvalFunction and SqlFunction, then this class
143  * will always attempt to translate the function into SQL using the SqlFunction interface.
144  *
145  * <h3>Database columns</h3>
146  * If a column is to be referenced from within the SQL, it must be set up in the EvalContext
147  * as well (as per the 'externalMessageType' lines in the previous example).
148  *
149  * <h4>Column types</h4>
150  * If not specified, the type of the column is VARCHAR, although this can be set
151  * in the constructor:
152  *
153  * <pre style="code">
154  *   SqlColumn arrivalDate = new SqlColumn("arrivalDate", SqlColumn.DATE);
155  * </pre>
156  *
157  * <p>(All java.sql.Types.* constants are supported here).
158  *
159  * <h4>Column tables</h4>
160  * <p>If our resultset JOINs many tables, and we wish to retrieve a value from a specific
161  * table, that can also be placed in the SqlColumn constructor.
162  *
163  * <pre style="code">
164  *   // e.g. SELECT tableA.id, tableB.id FROM tableA, tableB WHERE tableA.x = tableB.x
165  *   SqlColumn tableA_id = new SqlColumn("id", "tableA");
166  *   SqlColumn tableB_id = new SqlColumn("id", "tableB");
167  * </pre>
168  *
169  * <!-- 
170  * <h4>Money values</h4>
171  * <p>If we wish to do monetary comparisons and the 'money value' spans two columns (currency and
172  * amount), we can use the custom 'MONEYVALUE' type:
173  *
174  * <pre style="code">
175  *   SqlColumn moneyColumn = new SqlColumn("amount", SqlColumn.MONEYVALUE);
176  *   moneyColumn.setCurrencyCodeName("currency");
177  * </pre>
178  *
179  * <p>this sets up a column which retrieves it's amount from the 'amount' value, and
180  * the currency the 'currency' column of the table. MONEYVALUE columns can only be
181  * compared against other MONEYVALUE columns or MoneyValue objects, e.g.:
182  *
183  * <pre style="code">
184  *    String exprString = "amount &gt; toMoneyValue('USD', 10000)";
185  *    ...
186  *    context.setFunction("toMoneyValue", new EvalFunction.MoneyValueFunction() );
187  *    context.setVariable("amount", moneyColumn);
188  * </pre
189  *
190  * which will generate SQL of the form <code>currency = "USD" AND amount &gt; 10000</code>.
191  * (MoneyValueFunction is an EvalFunction which returns a MoneyValue object).
192  * -->
193  *
194  * <h4>FixedDate values</h4>
195  * <p>Another custom data type is the 'fixed date', which is a date represented as a
196  * string, in YYYYMMDD format. It is treated internally as identical to a VARCHAR
197  * column, but allows operations on these columns to be aware that the information
198  * being stored in it is a date.
199  *
200  * <p>Other references:
201  * <ul>
202  *   <li>DB2 date syntax: http://www-106.ibm.com/developerworks/db2/library/techarticle/0211yip/0211yip3.html
203  *   <li>SQL date syntax: http://msdn.microsoft.com/library/en-us/tsqlref/ts_ca-co_2f3o.asp
204  *   <li>Oracle date syntax: http://www-db.stanford.edu/~ullman/fcdb/oracle/or-time.html
205  * </ul>
206  *
207  * 
208  * @author knoxg
209  */
210 public class SqlGenerator
211     extends Evaluator {
212 
213     /** Internal variable to contain database type.
214      *  (internal variables start with "." since they are not valid identifiers,
215      *  and therefore cannot be referenced from within Expressions). */
216     public static final String VAR_DATABASE_TYPE = ".databaseType";
217 
218     /** Internal variable used to contain positional parameter names. */
219     public static final String VAR_PARAMETERS = ".parameters";
220     
221     /** Internal variable to configure empty string comparison handling.
222      * 
223      * When set to Boolean.TRUE, a comparison with the String constant "" will also 
224      * include NULL values. See {{@link #comparisonOpToSql(String, EvalContext, Object, Object)}
225      */
226     public static final String VAR_EMPTY_STRING_COMPARISON_INCLUDES_NULL = ".emptyStringComparisonIncludesNull";
227     
228 
229     /** Oracle VAR_DATABASE_TYPE value */
230     public static final String DATABASE_ORACLE = "Oracle";
231 
232     /** DB2 VAR_DATABASE_TYPE value */
233     public static final String DATABASE_DB2 = "DB2";
234 
235     /** SQL Server VAR_DATABASE_TYPE value */
236     public static final String DATABASE_SQLSERVER = "SqlServer";
237 
238 	/** MySQL VAR_DATABASE_TYPE value */
239 	public static final String DATABASE_MYSQL = "MySQL";
240 
241 	/** Jet (MSAccess) VAR_DATABASE_TYPE value */
242 	public static final String DATABASE_JET = "JET";
243 
244 	
245     // these are non-printable to reduce the chance of conflicts within strings.
246 
247     /** Used to internally delimit positional paramaeters in generated SQL */
248     private static final String POS_MARKER_LEFT = "\uF0000";
249 
250     /** Used to internally delimit positional paramaeters in generated SQL */
251     private static final String POS_MARKER_RIGHT = "\uF0001";
252 
253     /** Logger instance for this class */
254     Logger logger = Logger.getLogger(SqlGenerator.class);
255 
256     /** Generates code for comparisons of MoneyValue and Date types. Other data types
257      * fall back to "(" + lhs + " " + sqlOp + " " + rhs + ")".
258      *
259      * @param sqlOp The operation we wish to insert into the SQL.
260      *   (NB: this is *NOT* the operation in the .javacc syntax; e.g.
261      *    use "=" as an sqlOp, not "==" ).
262      * @param newSqlOp A modified SQL operator to use when dealing with dates, in
263      *   certain conditions. See the wiki for details.
264      * @param lhs The left hand side of the operation
265      * @param rhs The right hand side of the operation
266      * @return The SQL for this comparison
267      */
268     public static SqlText comparisonOpToSql(String sqlOp, EvalContext evalContext, Object lhs, Object rhs) {
269         // Date date = null;
270         // boolean startOfDay = false;
271 
272         SqlColumn sqlColumn = (lhs instanceof SqlColumn) ? (SqlColumn) lhs : null;
273         int dataType = (sqlColumn == null) ? -1 : sqlColumn.getDataType();
274         DateSpan dateSpan;
275 
276         // Expression formats
277         // DateFormat isoDateFormat = new SimpleDateFormat("yyyy-MM-dd");
278         // DateFormat isoDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
279 
280         if (rhs instanceof Date) {
281             throw new EvalException("Date objects not supported (use DateSpan instead)");
282         }
283 
284         // manipulate the conditional operator        
285         switch (dataType) {
286             case -1:
287                 // lhs is not an SqlColumn; treat normally
288                 break;
289 
290             /*
291             case SqlColumn.MONEYVALUE:
292                 if (!(rhs instanceof MoneyValue)) {
293                     throw new EvalException("The lhs of this comparison is a MONEYVALUE SqlColumn; " + 
294                       "rhs can only be a MoneyValue literal (found " + rhs.getClass().getName() + "')");
295                 }
296                 MoneyValue money = (MoneyValue) rhs;
297                 return new SqlText("((" + sqlColumn.getFullCurrencyCodeName() + " = '" + 
298                   money.getCurrency().getCurrencyCode() + "') AND (" + 
299                   sqlColumn.getFullName() + " " + sqlOp + " " + money.getAmount() + "))");
300 			*/
301                 
302             case SqlColumn.TIMEVALUE: // used to represent ConditionTO.DATATYPE_DATETIME columns
303                 if (!(rhs instanceof DateSpan)) {
304                     throw new EvalException("The lhs of this comparison is a TIMEVALUE SqlColumn; " + 
305                       "rhs can only be a DateSpan literal (found " + rhs.getClass().getName() + "')");
306                 }
307                 dateSpan = (DateSpan) rhs;
308                 if (dateSpan.isDay()) {
309                     if (sqlOp.equals("=")) {
310                         return new SqlText("(" + toSql(evalContext, lhs) + " >= " + toSql(evalContext, dateSpan.getStartAsDate()) + 
311                           " AND " + toSql(evalContext, lhs) + " <= " + toSql(evalContext, dateSpan.getEndAsDate()) + ")");
312                     } else if (sqlOp.equals("<>")) {
313                         return new SqlText("(" + toSql(evalContext, lhs) + " < " + toSql(evalContext, dateSpan.getStartAsDate()) + 
314                           " OR " + toSql(evalContext, lhs) + " > " + toSql(evalContext, dateSpan.getEndAsDate()) + ")");
315                     } else if (sqlOp.equals(">")) {
316                         rhs = dateSpan.getEndAsDate();
317                     } else if (sqlOp.equals(">=")) {
318                         rhs = dateSpan.getStartAsDate();
319                     } else if (sqlOp.equals("<")) {
320                         rhs = dateSpan.getStartAsDate();
321                     } else if (sqlOp.equals("<=")) {
322                         rhs = dateSpan.getEndAsDate();
323                     } else {
324                         throw new EvalException("Unknown SQL operator '" + sqlOp + "'");
325                     }
326                 } else {
327                     rhs = dateSpan.getStartAsDate();
328                 }
329                 break;
330 
331             case SqlColumn.FIXEDDATEVALUE: // used to represent ConditionTO.DATATYPE_FIXEDDATE columns
332                 if (!(rhs instanceof DateSpan)) {
333                     throw new EvalException("The lhs of this comparison is a FIXEDDATEVALUE SqlColumn; " + "can only compare against a DateSpan literal");
334                 }
335                 dateSpan = (DateSpan) rhs;
336                 if (!dateSpan.isDay()) {
337                     throw new EvalException("The lhs of this comparison is a FIXEDDATEVALUE SqlColumn; " + "DateSpan cannot contain time components");
338                 }
339 
340                 // just do a string comparison
341                 rhs = dateSpan.getStartAsYyyymmdd();
342 
343                 break;
344             default:
345             // any other data types are compared normally, as below.
346         }
347 
348         if (rhs instanceof SqlColumn) {
349             // query builder doesn't allow these types of expressions, and throwing an exception just keeps the code simpler.
350             throw new EvalException("rhs SqlColumn not implemented");
351         }
352 
353         boolean emptyStringIncludesNull = Boolean.TRUE.equals(evalContext.getVariable(VAR_EMPTY_STRING_COMPARISON_INCLUDES_NULL)); 
354         
355         // default action: just compare the lhs to the rhs
356         SqlText result = new SqlText("(" + toSql(evalContext, lhs) + " " + sqlOp + " " + toSql(evalContext, rhs) + ")");
357 
358         if (emptyStringIncludesNull) {
359             // empty/null checks against strings
360         	if ("=".equals(sqlOp) && "".equals(rhs)) {
361 	            result = new SqlText("(" + result + " OR (" + toSql(evalContext, lhs) + " IS NULL))");
362 	        } else if ("<>".equals(sqlOp) && "".equals(rhs)) {
363 	            result = new SqlText("(" + result + " OR (NOT " + toSql(evalContext, lhs) + " IS NULL))");
364 	        } else if ("<>".equals(sqlOp) && !"".equals(rhs)) {
365 	            result = new SqlText("(" + result + " OR (" + toSql(evalContext, lhs) + " IS NULL))");
366 	        } 
367         }
368 
369         return result;
370     }
371 
372     /**
373      * Return the database type from this context
374      *
375      * @param evalContext The context passed to the .visit() method of this class
376      * @return a DATABASE_* constant
377      *
378      * @throws EvalException if database type is not set, or is invalid
379      */
380     public static String getDatabaseType(EvalContext evalContext) {
381         String databaseType = (String) evalContext.getVariable(VAR_DATABASE_TYPE);
382 
383         if (databaseType == null) {
384             throw new EvalException("Database type must be set to translate this expression");
385         }
386 
387         if (databaseType.equals(DATABASE_DB2) || 
388           databaseType.equals(DATABASE_ORACLE) || 
389           databaseType.equals(DATABASE_SQLSERVER) ||
390           databaseType.equals(DATABASE_JET) ||
391           databaseType.equals(DATABASE_MYSQL)) {
392             return databaseType;
393         }
394 
395         throw new EvalException("Unknown database type '" + databaseType + "'");
396     }
397 
398 
399     /** Convert an evaluated node object into it's SQL representation.
400      *  The code must have evaluated to a literal String or Number by the
401      *  time this method is called, or an EvalException is thrown.
402      *
403      * @param obj The object to convert to SQL
404      * @return The SQL representation of this object
405      */
406     public static SqlText toSql(EvalContext evalContext, Object obj) {
407         if (obj == null) {
408             // ?
409             throw new EvalException("Null values not supported");
410 
411         } else if (obj instanceof SqlColumn) {
412             return new SqlText(((SqlColumn) obj).getFullName());
413 
414         } else if (obj instanceof SqlText) {
415             return (SqlText) obj;
416 
417         } else if (obj instanceof String) {
418             return new SqlText(escapeStringLiteral(evalContext, (String) obj));
419 
420         } else if (obj instanceof Number) {
421             // NB: this will probably fail when processing E+ exponent notation 
422             // in numbers, even though it is actually allowed in expression.jj.
423             // This shouldn't occur in practice, however. 
424             return new SqlText(obj.toString());
425 
426         } else if (obj instanceof Date) {
427             String databaseType = getDatabaseType(evalContext);
428             Date date;
429             SimpleDateFormat sdf;
430 
431             if (databaseType.equals(DATABASE_ORACLE)) {
432                 date = (Date) obj;
433                 // @TODO (low priority) we may need to set timezone for sdf below to the database timezone, but we'll just assume the server tz = database tz
434                 sdf = new SimpleDateFormat("yyyy/MM/dd:HH:mm:ss");
435                 return new SqlText("TO_DATE('" + sdf.format(date) + "', 'yyyy/mm/dd:HH24:mi:ss')");
436 
437             } else if (databaseType.equals(DATABASE_DB2)) {
438                 date = (Date) obj;
439                 sdf = new SimpleDateFormat("yyyy-MM-dd-HH.mm.ss");
440                 return new SqlText("TIMESTAMP('" + sdf.format(date) + "')");
441 
442             } else if (databaseType.equals(DATABASE_SQLSERVER)) {
443                 date = (Date) obj;
444                 sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
445                 return new SqlText("CONVERT(datetime, '" + sdf.format(date) + "', 20)");
446 
447             } else if (databaseType.equals(DATABASE_MYSQL)) {
448                 date = (Date) obj;
449                 sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
450                 return new SqlText("'" + sdf.format(date) + "'");
451 
452             } else if (databaseType.equals(DATABASE_JET)) {
453                 date = (Date) obj;
454                 sdf = new SimpleDateFormat("#dd/MM/yyyy HH:mm:ss#"); // american date ordering in JET SQL. ODBC might reverse this though. let's see.
455                 return new SqlText("'" + sdf.format(date) + "'");
456 
457                 
458             } else {
459                 throw new EvalException("Date constants unsupported for database type '" + databaseType + "'");
460             }
461 
462         } else if (obj instanceof Boolean b) {
463             return b ? new SqlText("TRUE") : new SqlText("FALSE");
464 
465         } else {
466             throw new EvalException("Cannot convert class '" + obj.getClass().getName() + "' to SQL");
467         }
468     }
469 
470     /**
471      * Escape a string literal. (in oracle, this surrounds a value with quotes and
472      * replaces quotes with doubled-up quote characters).
473      *
474      * @param evalContext
475      * @param string
476      * @return
477      */
478     public static String escapeStringLiteral(EvalContext evalContext, String string) {
479         String databaseType = getDatabaseType(evalContext);
480 
481         if (databaseType.equals(DATABASE_ORACLE)) {
482             string = "'" + Text.replaceString(string, "'", "''") + "'";
483         } else if (databaseType.equals(DATABASE_DB2)) {
484             string = "'" + Text.replaceString(string, "'", "''") + "'";
485         } else if (databaseType.equals(DATABASE_SQLSERVER)) {
486             string = "'" + Text.replaceString(string, "'", "''") + "'";
487         } else if (databaseType.equals(DATABASE_MYSQL)) {
488         	// mysql string escaping rules: https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
489         	
490         	// we're not going to escape backslashes in here as the user may have intended to already escape
491         	// some characters in LIKE expressions; we don't want to convert "\% starts with a percentage sign"
492         	// to "\\% starts with a percentage sign". 
493         	// That also means the user is responsible for converting newlines to "\n" etc
494         	// (nb: you can actually have newlines in string literals in mysql)
495         	
496         	// also note \% is treated by mysql as "\%" outside of a like expression
497         	// and       \_ is treated by mysql as "\_" outside of a like expression
498         	// but most other escapes, and unknown escapes are converted to single characters; e.g. 
499         	//           \x is treated by mysql as "x" outside of a like expression
500         	// but again this is up to the user to grok.
501         	
502         	// if I start using this class across databases again may want to come up with some 
503         	// crossvendor escaping rules which will have a myriad of tiny bugs in it 
504         	
505             string = "'" + Text.replaceString(string, "'", "\\'") + "'";
506         } else if (databaseType.equals(DATABASE_JET)) {
507             string = "'" + Text.replaceString(string, "'", "\\'") + "'";
508 
509         } else {
510             throw new EvalException("Do not know how to escape string literals in database type '" + databaseType + "'");
511         }
512 
513         return string;
514     }
515 
516     /**
517      * Escape the text used in a LIKE function, so that % and _ characters have no
518      * special meaning. Note that an escaped like literal still needs to be passed
519      * through escapeStringLiteral in order to surround it with quotes/escape quotes, etc...
520      *
521      * <p>(This isn't done in this method because we want to be able to modify the LIKE
522      * value that the user enters e.g. for the startsWith() function)
523      *
524      * @param evalContext The evaluation context (containing the database type)
525      * @param string The string to be escaped
526 
527      * @return The escaped string
528      */
529     public static String escapeLikeLiteral(EvalContext evalContext, String string) {
530         String databaseType = getDatabaseType(evalContext);
531 
532 		// only the Oracle rules below have been verified; should probably check
533 		// the rest
534         if (databaseType.equals(DATABASE_ORACLE)) {
535             string = Text.replaceString(string, "%", "\\%");  // % becomes \%
536             string = Text.replaceString(string, "_", "\\_");  // _ becomes \_
537         } else if (databaseType.equals(DATABASE_SQLSERVER)) {
538 			string = Text.replaceString(string, "%", "\\%");  // % becomes \%
539 			string = Text.replaceString(string, "_", "\\_");  // _ becomes \_
540         } else if (databaseType.equals(DATABASE_DB2)) {
541 			string = Text.replaceString(string, "%", "\\%");  // % becomes \%
542 			string = Text.replaceString(string, "_", "\\_");  // _ becomes \_
543         } else if (databaseType.equals(DATABASE_MYSQL)) {
544 			string = Text.replaceString(string, "%", "\\%");  // % becomes \%
545 			string = Text.replaceString(string, "_", "\\_");  // _ becomes \_
546         } else if (databaseType.equals(DATABASE_JET)) {
547 			string = Text.replaceString(string, "%", "\\%");  // % becomes \% . Possibly '*' in JET.
548 			string = Text.replaceString(string, "_", "\\_");  // _ becomes \_
549 			
550         } else {
551             throw new EvalException("Do not know how to escape string literals in database type '" + databaseType + "'");
552         }
553 
554         return string;
555     }
556 
557     /** Convert a TopLevelExpression node to it's SQL representation. The SQL is returned
558      * as a String. If any positional parameters have been referenced inside the SQL,
559      * then the context variable VAR_PARAMETERS is set, containing an ordered list of Strings,
560      * containing the positional parameter names.
561      *
562      * <p>The PRE text in these javadocs correspond to the javacc expansion of
563      * this node; e.g. in expression.jj, the rule for TopLevelExpression
564      * is
565      *
566      * <pre>
567      * void TopLevelExpression():
568      * {}
569      * {
570      *   Expression() <EOF>
571      * }
572      * </pre>
573      *
574      * <p>jtb then creates two fields in the TopLevelExpression object, "expression" (containing
575      * the Expression() node) and "nodeToken" (containing the EOF token). This is documented
576      * in the form below (this is included for all jtb-generated visit methods):
577      *
578      * <p>See the class javadocs for detailed instructions on how to use this method.
579      *
580      * <PRE>
581      * expression -> Expression()
582      * nodeToken -> &lt;EOF&gt;
583      * </PRE>
584      */
585     public Object visit(TopLevelExpression n, EvalContext context) {
586         EvalContext evalContext = (EvalContext) context;
587         String sql = n.expression.accept(this, context).toString();
588 
589         // replace {positionalNames} with '?' markers and remember what we've hit so far ... 
590         int pos = sql.indexOf(POS_MARKER_LEFT);
591         int pos2;
592         List<String> paramList = new ArrayList<>();
593 
594         while (pos != -1) {
595             pos2 = sql.indexOf(POS_MARKER_RIGHT, pos + POS_MARKER_LEFT.length());
596 
597             if (pos2 == -1) {
598                 throw new EvalException("Internal error (unclosed positional parameter)");
599             }
600 
601             paramList.add(sql.substring(pos + POS_MARKER_LEFT.length(), pos2));
602             sql = sql.substring(0, pos) + "?" + sql.substring(pos2 + POS_MARKER_RIGHT.length());
603             pos = sql.indexOf(POS_MARKER_LEFT);
604         }
605 
606         if (paramList.size() > 0) {
607             evalContext.setVariable(VAR_PARAMETERS, paramList);
608         } else {
609             evalContext.unsetVariable(VAR_PARAMETERS);
610         }
611 
612         return sql.toString();
613     }
614 
615     /** Generate the SQL for an Expression node.
616      *
617      * <PRE>
618      * conditionalAndExpression -> ConditionalAndExpression()
619      * nodeListOptional -> ( "||" ConditionalAndExpression() )*
620      * </PRE>
621      */
622     public Object visit(Expression n, EvalContext context) {
623         NodeSequence seq;
624         Object lhs;
625         Object rhs;
626         EvalContext evalContext = (EvalContext) context;
627 
628         lhs = n.conditionalAndExpression.accept(this, context);
629 
630         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
631             seq = (NodeSequence) e.nextElement();
632             rhs = seq.elementAt(1).accept(this, context);
633             lhs = new SqlText("(" + toSql(evalContext, lhs) + " OR " + toSql(evalContext, rhs) + ")");
634         }
635 
636         return lhs;
637     }
638 
639     /** Generate the SQL for a ConditionalAndExpression node.
640      *
641      * <PRE>
642      * equalityExpression -> EqualityExpression()
643      * nodeListOptional -> ( "&&" EqualityExpression() )*
644      * </PRE>
645      */
646     public Object visit(ConditionalAndExpression n, EvalContext context) {
647         NodeSequence seq;
648         Object lhs;
649         Object rhs;
650         EvalContext evalContext = (EvalContext) context;
651 
652         lhs = n.equalityExpression.accept(this, context);
653 
654         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
655             seq = (NodeSequence) e.nextElement();
656             rhs = seq.elementAt(1).accept(this, context);
657             lhs = new SqlText("(" + toSql(evalContext, lhs) + " AND " + toSql(evalContext, rhs) + ")");
658         }
659 
660         return lhs;
661     }
662 
663     /** Generate the SQL for a EqualityExpression node.
664      *
665      * <PRE>
666      * relationalExpression -> RelationalExpression()
667      * nodeListOptional -> ( ( "==" | "!=" ) RelationalExpression() )*
668      * </PRE>
669      */
670     public Object visit(EqualityExpression n, EvalContext context) {
671         NodeSequence seq;
672         Object lhs;
673         Object rhs;
674         String[] ops = { "==", "!=" };
675         EvalContext evalContext = (EvalContext) context;
676 
677         lhs = n.relationalExpression.accept(this, context);
678 
679         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
680             seq = (NodeSequence) e.nextElement();
681             rhs = seq.elementAt(1).accept(this, context);
682 
683             int which = ((NodeChoice) seq.elementAt(0)).which;
684 
685             switch (which) {
686                 // this requires a special-case on null rhs, but 
687                 // leaving this as it won't occur in my app at the moment. (I 
688                 // use the isNull() functions for this sort of thing)
689                 case 0:
690                     lhs = comparisonOpToSql("=", evalContext, lhs, rhs);
691                     break;
692                 case 1:
693                     lhs = comparisonOpToSql("<>", evalContext, lhs, rhs);
694                     break;
695                 default:
696                     throw new EvalException("Internal error (EqualityExpression)");
697             }
698         }
699 
700         return lhs;
701     }
702 
703     /** Generate the SQL for a RelationalExpression node.
704      *
705      * <PRE>
706      * additiveExpression -> AdditiveExpression()
707      * nodeListOptional -> ( ( "&lt;" | "&gt;" | "&lt;=" | "&gt;=" ) AdditiveExpression() )*
708      * </PRE>
709      */
710     public Object visit(RelationalExpression n, EvalContext context) {
711         NodeSequence seq;
712         Object lhs;
713         Object rhs;
714         String[] ops = { "<", ">", "<=", ">=" };
715         EvalContext evalContext = (EvalContext) context;
716 
717         lhs = n.additiveExpression.accept(this, context);
718 
719         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
720             seq = (NodeSequence) e.nextElement();
721             rhs = seq.elementAt(1).accept(this, context);
722 
723             int which = ((NodeChoice) seq.elementAt(0)).which;
724 
725             // logger.debug("Running op '" + ops[which] + "' on lhs:" + lhs + ", rhs:" + rhs);
726             // note comparisons ops here may be modified.
727             switch (which) {
728                 case 0:
729                     lhs = comparisonOpToSql("<", evalContext, lhs, rhs);
730                     break;
731                 case 1:
732                     lhs = comparisonOpToSql(">", evalContext, lhs, rhs);
733                     break;
734                 case 2:
735                     lhs = comparisonOpToSql("<=", evalContext, lhs, rhs);
736                     break;
737                 case 3:
738                     lhs = comparisonOpToSql(">=", evalContext, lhs, rhs);
739                     break;
740                 default:
741                     throw new EvalException("Internal error (RelationalExpression)");
742             }
743         }
744 
745         return lhs;
746     }
747 
748     /** Generate the SQL for a AdditiveExpression node.
749      *
750      * <PRE>
751      * multiplicativeExpression -> MultiplicativeExpression()
752      * nodeListOptional -> ( ( "+" | "-" ) MultiplicativeExpression() )*
753      * </PRE>
754      */
755     public Object visit(AdditiveExpression n, EvalContext context) {
756         NodeSequence seq;
757         Object lhs;
758         Object rhs;
759         String[] ops = { "+", "-" };
760         EvalContext evalContext = (EvalContext) context;
761 
762         lhs = n.multiplicativeExpression.accept(this, context);
763 
764         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
765             seq = (NodeSequence) e.nextElement();
766             rhs = seq.elementAt(1).accept(this, context);
767 
768             int which = ((NodeChoice) seq.elementAt(0)).which;
769 
770             // logger.debug("Running op '" + ops[which] + "' on lhs:" + lhs + ", rhs:" + rhs);
771             switch (which) {
772                 case 0:
773                     // NB: does not support String addition, only numeric addition
774                     lhs = new SqlText("(" + toSql(evalContext, lhs) + " + " + toSql(evalContext, rhs) + ")");
775                     break;
776                 case 1:
777                     lhs = new SqlText("(" + toSql(evalContext, lhs) + " - " + toSql(evalContext, rhs) + ")");
778                     break;
779                 default:
780                     throw new EvalException("Internal error (AdditiveExpression)");
781             }
782         }
783 
784         return lhs;
785     }
786 
787     /** Generate the SQL for a MultiplicativeExpression node.
788      *
789      * <PRE>
790      * unaryExpression -> UnaryExpression()
791      * nodeListOptional -> ( ( "*" | "/" | "%" ) UnaryExpression() )*
792      * </PRE>
793      */
794     public Object visit(MultiplicativeExpression n, EvalContext context) {
795         NodeSequence seq;
796         Object lhs;
797         Object rhs;
798         String[] ops = { "*", "/", "%" };
799 
800         lhs = n.unaryExpression.accept(this, context);
801 
802         EvalContext evalContext = (EvalContext) context;
803 
804         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
805             seq = (NodeSequence) e.nextElement();
806             rhs = seq.elementAt(1).accept(this, context);
807 
808             int which = ((NodeChoice) seq.elementAt(0)).which;
809 
810             // logger.debug("Running op '" + ops[which] + "' on lhs:" + lhs + ", rhs:" + rhs);
811             switch (which) {
812                 case 0:
813                     lhs = new SqlText("(" + toSql(evalContext, lhs) + " * " + toSql(evalContext, rhs) + ")");
814                     break;
815                 case 1:
816                     lhs = new SqlText("(" + toSql(evalContext, lhs) + " / " + toSql(evalContext, rhs) + ")");
817                     break;
818                 case 2:
819                     lhs = new SqlText("(" + toSql(evalContext, lhs) + " % " + toSql(evalContext, rhs) + ")");
820                     break;
821                 default:
822                     throw new EvalException("Internal error (MultiplicativeExpression)");
823             }
824         }
825 
826         return lhs;
827     }
828 
829     /** Generate the SQL for a UnaryExpression node.
830      *
831      * <PRE>
832      * nodeChoice -> ( "~" | "!" | "-" ) UnaryExpression()
833      *       | PrimaryExpression()
834      * </PRE>
835      */
836     public Object visit(UnaryExpression n, EvalContext context) {
837         NodeSequence seq;
838         Object lhs;
839         EvalContext evalContext = (EvalContext) context;
840 
841         if (n.nodeChoice.which == 0) {
842             seq = (NodeSequence) n.nodeChoice.choice;
843             lhs = seq.elementAt(1).accept(this, context);
844 
845             int which = ((NodeChoice) seq.elementAt(0)).which;
846 
847             // String op = ((NodeToken) ((NodeChoice) nl.nodes.get(0)).choice).tokenImage;
848             switch (which) {
849                 case 0:
850                     // long rhs =  makeNumeric ( ((Node) nl.nodes.get(0)).accept(this, context) ); 
851                     // return new Long(~rhs); 
852                     return new SqlText("(~" + toSql(evalContext, lhs) + ")");
853                 case 1:
854                     return new SqlText("(NOT " + toSql(evalContext, lhs) + ")");
855                 case 2:
856                     return new SqlText("(- " + toSql(evalContext, lhs) + ")");
857                 default:
858                     throw new EvalException("Internal error (UnaryExpression)");
859             }
860         } else {
861             return n.nodeChoice.choice.accept(this, context);
862         }
863     }
864 
865     /** Generate the SQL for a PrimaryExpression node.
866      *
867      * <PRE>
868      * nodeChoice -> FunctionCall()
869      *       | Name()
870      *       | Literal()
871      *       | "(" Expression() ")"
872      * </PRE>
873      */
874     public Object visit(PrimaryExpression n, EvalContext context) {
875         NodeSequence seq;
876         int which = n.nodeChoice.which;
877 
878         switch (which) {
879             case 0:
880             case 1:
881             case 2:
882                 return n.nodeChoice.choice.accept(this, context);
883             case 3:
884                 seq = (NodeSequence) n.nodeChoice.choice;
885                 Object obj = seq.elementAt(1).accept(this, context);
886 
887                 return obj;
888             default:
889                 throw new EvalException("Internal parser error (PrimaryExpression)");
890         }
891     }
892 
893     /** Generate the SQL for a Name node.
894      *
895      * <PRE>
896      * nodeToken -> &lt;IDENTIFIER&gt;
897      * nodeListOptional -> ( "." &lt;IDENTIFIER&gt; )*
898      * </PRE>
899      */
900     public Object visit(Name n, EvalContext context) {
901         EvalContext evalContext = (EvalContext) context;
902         String varComponentName;
903         String varBaseName = n.nodeToken.tokenImage;
904 
905         // logger.debug("Fetching var '" + varBaseName + "'");
906         if (!evalContext.hasVariable(varBaseName)) {
907             throw new EvalException("Unknown variable '" + varBaseName + "'");
908         }
909 
910         Object value = evalContext.getVariable(varBaseName);
911         NodeSequence seq;
912 
913         /*
914            if (value == null) {
915                 logger.debug(" = null");
916            } else {
917                 logger.debug(" = " + value.getClass().getName() + " with value '" + value + "'");
918            }
919          */
920         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
921             seq = (NodeSequence) e.nextElement();
922             varComponentName = ((NodeToken) seq.elementAt(1)).tokenImage;
923 
924             // logger.debug("Fetching component '" + varComponentName + "' from var '" + varBaseName + "'");
925             if (evalContext.hasVariableComponent(value, varBaseName, varComponentName)) {
926                 throw new EvalException("Unknown variable component '" + varComponentName + "' in variable '" + varBaseName + "'");
927             }
928 
929             value = evalContext.getVariableComponent(value, varBaseName, varComponentName);
930             varBaseName = varBaseName + "." + varComponentName;
931         }
932 
933 
934 
935 
936         // insert positional parameters into the generated SQL as 
937         //    {paramName}
938         //  ... these will get substituted with '?' just before 
939         // visit(TopLevelExpression) returns the result to the caller.
940         if (value instanceof PositionalParameter) {
941             PositionalParameter posParameter = (PositionalParameter) value;
942 
943             return new SqlText(POS_MARKER_LEFT + posParameter.getName() + POS_MARKER_RIGHT);
944         }
945 
946 
947         // otherwise just get the value itself (this will be converted into SqlText
948         // by other visit() methods)
949         return value;
950     }
951 
952     /** Generate the SQL for a FunctionCall node.
953      *
954      * <PRE>
955      * nodeToken -> &lt;IDENTIFIER&gt;
956      * arguments -> Arguments()
957      * </PRE>
958      */
959     public Object visit(FunctionCall n, EvalContext context) {
960         List argumentList = (List) n.arguments.accept(this, context);
961         String functionName = n.nodeToken.tokenImage;
962         EvalContext evalContext = (EvalContext) context;
963         Object function = evalContext.getFunction(functionName);
964         Evaluator evaluator = new Evaluator();
965 
966         if (function == null) {
967             throw new EvalException("Unknown function '" + functionName + "'");
968 
969         } else if (function instanceof SqlFunction) {
970             SqlFunction sqlFunction = (SqlFunction) function;
971             try {
972                 return new SqlText(sqlFunction.toSql(functionName, evalContext, argumentList));
973             } catch (EvalFallbackException ee) {
974                 // I think this only occurs in replaced promptable values
975                 
976                 logger.info("evaluating fallback for '" + functionName + "'");
977                 argumentList = (List) evaluator.visit((Arguments) n.arguments, context);
978                 return ((EvalFunction) sqlFunction).evaluate(functionName, evalContext, argumentList);
979             }
980 
981         } else if (function instanceof EvalFunction) {
982             EvalFunction evalFunction = (EvalFunction) function;
983             // evaluate the argument list (pass in the context to allow TZ_* references)
984             argumentList = (List) evaluator.visit((Arguments) n.arguments, context);
985             return evalFunction.evaluate(functionName, evalContext, argumentList);
986 
987         }
988 
989         throw new EvalException("Cannot translate or evaluate function '" + function + "'");
990     }
991 
992     /** Generate the SQL for an Arguments node
993      *
994      * <PRE>
995      * nodeToken -> "("
996      * nodeOptional -> [ ArgumentList() ]
997      * nodeToken1 -> ")"
998      * </PRE>
999      */
1000     public Object visit(Arguments n, EvalContext context) {
1001         if (n.nodeOptional.present()) {
1002             return n.nodeOptional.accept(this, context);
1003         } else {
1004             return new ArrayList<>(0);
1005         }
1006     }
1007 
1008     /** Generate the SQL for a ArgumentList node.
1009      *
1010      *  <p>(Since an ArgumentList is fed directly into the EvalFunction to generate SQL,
1011      *  we just return a List of arguments)
1012      *
1013      * <PRE>
1014      * expression -> Expression()
1015      * nodeListOptional -> ( "," Expression() )*
1016      * </PRE>
1017      */
1018     public Object visit(ArgumentList n, EvalContext context) {
1019         NodeSequence seq;
1020         List<Object> arguments = new ArrayList<>();
1021 
1022         arguments.add(n.expression.accept(this, context));
1023 
1024         for (Enumeration<?> e = n.nodeListOptional.elements(); e.hasMoreElements();) {
1025             seq = (NodeSequence) e.nextElement();
1026             arguments.add(seq.elementAt(1).accept(this, context));
1027         }
1028 
1029         return arguments;
1030     }
1031 
1032     /** Generate the SQL for a Literal node.
1033      *
1034      * <PRE>
1035      * nodeChoice -> &lt;INTEGER_LITERAL&gt;
1036      *       | &lt;FLOATING_POINT_LITERAL&gt;
1037      *       | &lt;CHARACTER_LITERAL&gt;
1038      *       | &lt;STRING_LITERAL&gt;
1039      *       | BooleanLiteral()
1040      *       | NullLiteral()
1041      * </PRE>
1042      *
1043      */
1044     public Object visit(Literal n, EvalContext context) {
1045         String token = null;
1046 
1047         if (n.nodeChoice.choice instanceof NodeToken) {
1048             token = ((NodeToken) n.nodeChoice.choice).tokenImage;
1049         }
1050 
1051         switch (n.nodeChoice.which) {
1052             case 0:
1053                 return Long.valueOf(token);
1054             case 1:
1055                 return Double.valueOf(token);
1056             case 2:
1057                 return Character.valueOf(token.charAt(1));
1058             case 3:
1059                 return Text.unescapeJava(token.substring(1, token.length() - 1));
1060         }
1061 
1062         // must be 'true', 'false', or 'null'
1063         return n.nodeChoice.accept(this, context);
1064     }
1065 
1066     /** Generate the SQL for a BooleanLiteral node.
1067      *
1068      * <PRE>
1069      * nodeChoice -> "true"
1070      *       | "false"
1071      * </PRE>
1072      */
1073     public Object visit(BooleanLiteral n, EvalContext context) {
1074         if (n.nodeChoice.which == 0) {
1075             return Boolean.valueOf(true);
1076         } else {
1077             return Boolean.valueOf(false);
1078         }
1079     }
1080 
1081     /** Generate the SQL for a NullLiteral node.
1082      *
1083      * <PRE>
1084      * nodeToken -> "null"
1085      * </PRE>
1086      */
1087     public Object visit(NullLiteral n, EvalContext context) {
1088         return null;
1089     }
1090 
1091     /** This is never executed (we do not evaluate tokens) */
1092     public Object visit(NodeToken n, EvalContext context) {
1093         return n.tokenImage;
1094     }
1095 }