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 (< and >) 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 > 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 > 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 -> <EOF> 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 -> ( ( "<" | ">" | "<=" | ">=" ) 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 -> <IDENTIFIER> 877 * nodeListOptional -> ( "." <IDENTIFIER> )* 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 -> <IDENTIFIER> 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 -> <INTEGER_LITERAL> 1016 * | <FLOATING_POINT_LITERAL> 1017 * | <CHARACTER_LITERAL> 1018 * | <STRING_LITERAL> 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}