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