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