0001: // jTDS JDBC Driver for Microsoft SQL Server and Sybase
0002: // Copyright (C) 2004 The jTDS Project
0003: //
0004: // This library is free software; you can redistribute it and/or
0005: // modify it under the terms of the GNU Lesser General Public
0006: // License as published by the Free Software Foundation; either
0007: // version 2.1 of the License, or (at your option) any later version.
0008: //
0009: // This library is distributed in the hope that it will be useful,
0010: // but WITHOUT ANY WARRANTY; without even the implied warranty of
0011: // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0012: // Lesser General Public License for more details.
0013: //
0014: // You should have received a copy of the GNU Lesser General Public
0015: // License along with this library; if not, write to the Free Software
0016: // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
0017: //
0018: package net.sourceforge.jtds.jdbc;
0019:
0020: import java.sql.SQLException;
0021: import java.util.ArrayList;
0022: import java.util.HashMap;
0023:
0024: import net.sourceforge.jtds.jdbc.cache.SimpleLRUCache;
0025: import net.sourceforge.jtds.jdbc.cache.SQLCacheKey;
0026:
0027: /**
0028: * Process JDBC escape strings and parameter markers in the SQL string.
0029: * <p>
0030: * This code recognizes the following escapes:
0031: * <ol>
0032: * <li>Date {d 'yyyy-mm-dd'}
0033: * <li>Time {t 'hh:mm:ss'}
0034: * <li>Timestamp {ts 'yyyy-mm-dd hh:mm:ss.nnn'}
0035: * <li>ESCAPE {escape 'x'}
0036: * <li>Function {fn xxxx([arg,arg...])}
0037: * NB The concat(arg, arg) operator is converted to (arg + arg)
0038: * <li>OuterJoin {oj .....}
0039: * <li>Call {?=call proc [arg, arg...]}
0040: * or {call proc [arg, arg...]}
0041: * </ol>
0042: * Notes:
0043: * <ol>
0044: * <li>This code is designed to be as efficient as possible and as
0045: * result the validation done here is limited.
0046: * <li>SQL comments are parsed correctly thanks to code supplied by
0047: * Joel Fouse.
0048: * </ol>
0049: *
0050: * @author Mike Hutchinson
0051: * @version $Id: SQLParser.java,v 1.29 2007/07/08 17:28:23 bheineman Exp $
0052: */
0053: class SQLParser {
0054: /**
0055: * Serialized version of a parsed SQL query (the value stored in the cache
0056: * for a parsed SQL).
0057: * <p/>
0058: * Holds the parsed SQL query and the names, positions and return value and
0059: * unicode flags for the parameters.
0060: */
0061: private static class CachedSQLQuery {
0062: final String[] parsedSql;
0063: final String[] paramNames;
0064: final int[] paramMarkerPos;
0065: final boolean[] paramIsRetVal;
0066: final boolean[] paramIsUnicode;
0067:
0068: CachedSQLQuery(String[] parsedSql, ArrayList params) {
0069: this .parsedSql = parsedSql;
0070:
0071: if (params != null) {
0072: final int size = params.size();
0073: paramNames = new String[size];
0074: paramMarkerPos = new int[size];
0075: paramIsRetVal = new boolean[size];
0076: paramIsUnicode = new boolean[size];
0077:
0078: for (int i = 0; i < size; i++) {
0079: ParamInfo paramInfo = (ParamInfo) params.get(i);
0080: paramNames[i] = paramInfo.name;
0081: paramMarkerPos[i] = paramInfo.markerPos;
0082: paramIsRetVal[i] = paramInfo.isRetVal;
0083: paramIsUnicode[i] = paramInfo.isUnicode;
0084: }
0085: } else {
0086: paramNames = null;
0087: paramMarkerPos = null;
0088: paramIsRetVal = null;
0089: paramIsUnicode = null;
0090: }
0091: }
0092: }
0093:
0094: /** LRU cache of previously parsed SQL */
0095: private static SimpleLRUCache cache;
0096:
0097: /** Original SQL string */
0098: private final String sql;
0099: /** Input buffer with SQL statement. */
0100: private final char[] in;
0101: /** Current position in input buffer. */
0102: private int s;
0103: /** Length of input buffer. */
0104: private final int len;
0105: /** Output buffer to contain parsed SQL. */
0106: private final char[] out;
0107: /** Current position in output buffer. */
0108: private int d;
0109: /**
0110: * Parameter list to be populated or <code>null</code> if no parameters
0111: * are expected.
0112: */
0113: private final ArrayList params;
0114: /** Current expected terminator character. */
0115: private char terminator;
0116: /** Procedure name in call escape. */
0117: private String procName;
0118: /** First SQL keyword or identifier in statement. */
0119: private String keyWord;
0120: /** First table name in from clause */
0121: private String tableName;
0122: /** Connection object for server specific parsing. */
0123: private final ConnectionJDBC2 connection;
0124:
0125: /**
0126: * Parse the SQL statement processing JDBC escapes and parameter markers.
0127: *
0128: * @param extractTable
0129: * true to return the first table name in the FROM clause of a select
0130: * @return The processed SQL statement, any procedure name, the first SQL
0131: * keyword and (optionally) the first table name as
0132: * elements 0 1, 2 and 3 of the returned <code>String[]</code>.
0133: * @throws SQLException if a parse error occurs
0134: */
0135: static String[] parse(String sql, ArrayList paramList,
0136: ConnectionJDBC2 connection, boolean extractTable)
0137: throws SQLException {
0138: // Don't cache extract table parse requests, just process it
0139: if (extractTable) {
0140: SQLParser parser = new SQLParser(sql, paramList, connection);
0141: return parser.parse(extractTable);
0142: }
0143:
0144: SimpleLRUCache cache = getCache(connection);
0145:
0146: SQLCacheKey cacheKey = new SQLCacheKey(sql, connection);
0147:
0148: // By not synchronizing on the cache, we're admitting that the possibility of multiple
0149: // parses of the same statement can occur. However, it is 1) unlikely under normal
0150: // usage, and 2) harmless to the cache. By avoiding a synchronization block around
0151: // the get()-parse()-put(), we reduce the contention greatly in the nominal case.
0152: CachedSQLQuery cachedQuery = (CachedSQLQuery) cache
0153: .get(cacheKey);
0154: if (cachedQuery == null) {
0155: // Parse and cache SQL
0156: SQLParser parser = new SQLParser(sql, paramList, connection);
0157: cachedQuery = new CachedSQLQuery(
0158: parser.parse(extractTable), paramList);
0159: cache.put(cacheKey, cachedQuery);
0160: } else {
0161: // Create full ParamInfo objects out of cached object
0162: final int length = (cachedQuery.paramNames == null) ? 0
0163: : cachedQuery.paramNames.length;
0164: for (int i = 0; i < length; i++) {
0165: ParamInfo paramInfo = new ParamInfo(
0166: cachedQuery.paramNames[i],
0167: cachedQuery.paramMarkerPos[i],
0168: cachedQuery.paramIsRetVal[i],
0169: cachedQuery.paramIsUnicode[i]);
0170: paramList.add(paramInfo);
0171: }
0172: }
0173: return cachedQuery.parsedSql;
0174: }
0175:
0176: // --------------------------- Private Methods --------------------------------
0177:
0178: /**
0179: * Retrieves the statement cache, creating it if required.
0180: *
0181: * @return the cache as a <code>SimpleLRUCache</code>
0182: */
0183: private synchronized static SimpleLRUCache getCache(
0184: ConnectionJDBC2 connection) {
0185: if (cache == null) {
0186: int maxStatements = connection.getMaxStatements();
0187: maxStatements = Math.max(0, maxStatements);
0188: maxStatements = Math.min(1000, maxStatements);
0189: cache = new SimpleLRUCache(maxStatements);
0190: }
0191: return cache;
0192: }
0193:
0194: /** Lookup table to test if character is part of an identifier. */
0195: private static boolean identifierChar[] = { false, false, false,
0196: false, false, false, false, false, false, false, false,
0197: false, false, false, false, false, false, false, false,
0198: false, false, false, false, false, false, false, false,
0199: false, false, false, false, false, false, false, false,
0200: true, true, false, false, false, false, false, false,
0201: false, false, false, false, false, true, true, true, true,
0202: true, true, true, true, true, true, false, false, false,
0203: false, false, false, true, true, true, true, true, true,
0204: true, true, true, true, true, true, true, true, true, true,
0205: true, true, true, true, true, true, true, true, true, true,
0206: true, false, false, false, false, true, false, true, true,
0207: true, true, true, true, true, true, true, true, true, true,
0208: true, true, true, true, true, true, true, true, true, true,
0209: true, true, true, true, false, false, false, false, false };
0210:
0211: /**
0212: * Determines if character could be part of an SQL identifier.
0213: * <p/>
0214: * Characters > 127 are assumed to be unicode letters in other
0215: * languages than english which is reasonable in this application.
0216: * @param ch the character to test.
0217: * @return <code>boolean</code> true if ch in A-Z a-z 0-9 @ $ # _.
0218: */
0219: private static boolean isIdentifier(int ch) {
0220: return ch > 127 || identifierChar[ch];
0221: }
0222:
0223: /**
0224: * Constructs a new parser object to process the supplied SQL.
0225: *
0226: * @param sqlIn the SQL statement to parse
0227: * @param paramList the parameter list array to populate or
0228: * <code>null</code> if no parameters are expected
0229: * @param connection the parent Connection object
0230: */
0231: private SQLParser(String sqlIn, ArrayList paramList,
0232: ConnectionJDBC2 connection) {
0233: sql = sqlIn;
0234: in = sql.toCharArray();
0235: len = in.length;
0236: out = new char[len + 256]; // Allow extra for curdate/curtime
0237: params = paramList;
0238: procName = "";
0239:
0240: this .connection = connection;
0241: }
0242:
0243: /**
0244: * Inserts a String literal in the output buffer.
0245: *
0246: * @param txt The text to insert.
0247: */
0248: private void copyLiteral(String txt) throws SQLException {
0249: final int len = txt.length();
0250:
0251: for (int i = 0; i < len; i++) {
0252: final char c = txt.charAt(i);
0253:
0254: if (c == '?') {
0255: if (params == null) {
0256: throw new SQLException(Messages.get(
0257: "error.parsesql.unexpectedparam", String
0258: .valueOf(s)), "2A000");
0259: }
0260: // param marker embedded in escape
0261: ParamInfo pi = new ParamInfo(d, connection
0262: .getUseUnicode());
0263: params.add(pi);
0264: }
0265:
0266: out[d++] = c;
0267: }
0268: }
0269:
0270: /**
0271: * Copies over an embedded string literal unchanged.
0272: */
0273: private void copyString() {
0274: char saveTc = terminator;
0275: char tc = in[s];
0276:
0277: if (tc == '[') {
0278: tc = ']';
0279: }
0280:
0281: terminator = tc;
0282:
0283: out[d++] = in[s++];
0284:
0285: while (in[s] != tc) {
0286: out[d++] = in[s++];
0287: }
0288:
0289: out[d++] = in[s++];
0290:
0291: terminator = saveTc;
0292: }
0293:
0294: /**
0295: * Copies over possible SQL keyword eg 'SELECT'
0296: */
0297: private String copyKeyWord() {
0298: int start = d;
0299:
0300: while (s < len && isIdentifier(in[s])) {
0301: out[d++] = in[s++];
0302: }
0303:
0304: return String.valueOf(out, start, d - start).toLowerCase();
0305: }
0306:
0307: /**
0308: * Builds a new parameter item.
0309: *
0310: * @param name Optional parameter name or null.
0311: * @param pos The parameter marker position in the output buffer.
0312: */
0313: private void copyParam(String name, int pos) throws SQLException {
0314: if (params == null) {
0315: throw new SQLException(Messages
0316: .get("error.parsesql.unexpectedparam", String
0317: .valueOf(s)), "2A000");
0318: }
0319:
0320: ParamInfo pi = new ParamInfo(pos, connection.getUseUnicode());
0321: pi.name = name;
0322:
0323: if (pos >= 0) {
0324: out[d++] = in[s++];
0325: } else {
0326: pi.isRetVal = true;
0327: s++;
0328: }
0329:
0330: params.add(pi);
0331: }
0332:
0333: /**
0334: * Copies an embedded stored procedure identifier over to the output buffer.
0335: *
0336: * @return The identifier as a <code>String</code>.
0337: */
0338: private String copyProcName() throws SQLException {
0339: int start = d;
0340:
0341: do {
0342: if (in[s] == '"' || in[s] == '[') {
0343: copyString();
0344: } else {
0345: char c = in[s++];
0346:
0347: while (isIdentifier(c) || c == ';') {
0348: out[d++] = c;
0349: c = in[s++];
0350: }
0351:
0352: s--;
0353: }
0354:
0355: if (in[s] == '.') {
0356: while (in[s] == '.') {
0357: out[d++] = in[s++];
0358: }
0359: } else {
0360: break;
0361: }
0362: } while (true);
0363:
0364: if (d == start) {
0365: // Procedure name expected but found something else
0366: throw new SQLException(
0367: Messages.get("error.parsesql.syntax", "call",
0368: String.valueOf(s)), "22025");
0369: }
0370:
0371: return new String(out, start, d - start);
0372: }
0373:
0374: /**
0375: * Copies an embedded parameter name to the output buffer.
0376: *
0377: * @return The identifier as a <code>String</code>.
0378: */
0379: private String copyParamName() {
0380: int start = d;
0381: char c = in[s++];
0382:
0383: while (isIdentifier(c)) {
0384: out[d++] = c;
0385: c = in[s++];
0386: }
0387:
0388: s--;
0389:
0390: return new String(out, start, d - start);
0391: }
0392:
0393: /**
0394: * Copies over white space.
0395: */
0396: private void copyWhiteSpace() {
0397: while (s < in.length && Character.isWhitespace(in[s])) {
0398: out[d++] = in[s++];
0399: }
0400: }
0401:
0402: /**
0403: * Checks that the next character is as expected.
0404: *
0405: * @param c The expected character.
0406: * @param copy True if found character should be copied.
0407: * @throws SQLException if expected characeter not found.
0408: */
0409: private void mustbe(char c, boolean copy) throws SQLException {
0410: if (in[s] != c) {
0411: throw new SQLException(Messages.get(
0412: "error.parsesql.mustbe", String.valueOf(s), String
0413: .valueOf(c)), "22019");
0414: }
0415:
0416: if (copy) {
0417: out[d++] = in[s++];
0418: } else {
0419: s++;
0420: }
0421: }
0422:
0423: /**
0424: * Skips embedded white space.
0425: */
0426: private void skipWhiteSpace() {
0427: while (Character.isWhitespace(in[s])) {
0428: s++;
0429: }
0430: }
0431:
0432: /**
0433: * Skips single-line comments.
0434: */
0435: private void skipSingleComments() {
0436: while (s < len && in[s] != '\n' && in[s] != '\r') {
0437: // comments should be passed on to the server
0438: out[d++] = in[s++];
0439: }
0440: }
0441:
0442: /**
0443: * Skips multi-line comments
0444: */
0445: private void skipMultiComments() throws SQLException {
0446: int block = 0;
0447:
0448: do {
0449: if (s < len - 1) {
0450: if (in[s] == '/' && in[s + 1] == '*') {
0451: block++;
0452: } else if (in[s] == '*' && in[s + 1] == '/') {
0453: block--;
0454: }
0455: // comments should be passed on to the server
0456: out[d++] = in[s++];
0457: } else {
0458: throw new SQLException(Messages.get(
0459: "error.parsesql.missing", "*/"), "22025");
0460: }
0461: } while (block > 0);
0462: out[d++] = in[s++];
0463: }
0464:
0465: /**
0466: * Processes the JDBC {call procedure [(?,?,?)]} type escape.
0467: *
0468: * @throws SQLException if an error occurs
0469: */
0470: private void callEscape() throws SQLException {
0471: // Insert EXECUTE into SQL so that proc can be called as normal SQL
0472: copyLiteral("EXECUTE ");
0473: keyWord = "execute";
0474: // Process procedure name
0475: procName = copyProcName();
0476: skipWhiteSpace();
0477:
0478: if (in[s] == '(') { // Optional ( )
0479: s++;
0480: terminator = ')';
0481: skipWhiteSpace();
0482: } else {
0483: terminator = '}';
0484: }
0485:
0486: out[d++] = ' ';
0487:
0488: // Process any parameters
0489: while (in[s] != terminator) {
0490: String name = null;
0491:
0492: if (in[s] == '@') {
0493: // Named parameter
0494: name = copyParamName();
0495: skipWhiteSpace();
0496: mustbe('=', true);
0497: skipWhiteSpace();
0498:
0499: if (in[s] == '?') {
0500: copyParam(name, d);
0501: } else {
0502: // Named param has literal value can't call as RPC
0503: procName = "";
0504: }
0505: } else if (in[s] == '?') {
0506: copyParam(name, d);
0507: } else {
0508: // Literal parameter can't call as RPC
0509: procName = "";
0510: }
0511:
0512: // Now find terminator or comma
0513: while (in[s] != terminator && in[s] != ',') {
0514: if (in[s] == '{') {
0515: escape();
0516: } else if (in[s] == '\'' || in[s] == '['
0517: || in[s] == '"') {
0518: copyString();
0519: } else {
0520: out[d++] = in[s++];
0521: }
0522: }
0523:
0524: if (in[s] == ',') {
0525: out[d++] = in[s++];
0526: }
0527:
0528: skipWhiteSpace();
0529: }
0530:
0531: if (terminator == ')') {
0532: s++; // Elide
0533: }
0534:
0535: terminator = '}';
0536: skipWhiteSpace();
0537: }
0538:
0539: /**
0540: * Utility routine to validate date and time escapes.
0541: *
0542: * @param mask The validation mask
0543: * @return True if the escape was valid and processed OK.
0544: */
0545: private boolean getDateTimeField(byte[] mask) throws SQLException {
0546: skipWhiteSpace();
0547: if (in[s] == '?') {
0548: // Allow {ts ?} type construct
0549: copyParam(null, d);
0550: skipWhiteSpace();
0551: return in[s] == terminator;
0552: }
0553: out[d++] = '\'';
0554: terminator = (in[s] == '\'' || in[s] == '"') ? in[s++] : '}';
0555: skipWhiteSpace();
0556: int ptr = 0;
0557:
0558: while (ptr < mask.length) {
0559: char c = in[s++];
0560: if (c == ' ' && out[d - 1] == ' ') {
0561: continue; // Eliminate multiple spaces
0562: }
0563:
0564: if (mask[ptr] == '#') {
0565: if (!Character.isDigit(c)) {
0566: return false;
0567: }
0568: } else if (mask[ptr] != c) {
0569: return false;
0570: }
0571:
0572: if (c != '-') {
0573: out[d++] = c;
0574: }
0575:
0576: ptr++;
0577: }
0578:
0579: if (mask.length == 19) { // Timestamp
0580: int digits = 0;
0581:
0582: if (in[s] == '.') {
0583: out[d++] = in[s++];
0584:
0585: while (Character.isDigit(in[s])) {
0586: if (digits < 3) {
0587: out[d++] = in[s++];
0588: digits++;
0589: } else {
0590: s++;
0591: }
0592: }
0593: } else {
0594: out[d++] = '.';
0595: }
0596:
0597: for (; digits < 3; digits++) {
0598: out[d++] = '0';
0599: }
0600: }
0601:
0602: skipWhiteSpace();
0603:
0604: if (in[s] != terminator) {
0605: return false;
0606: }
0607:
0608: if (terminator != '}') {
0609: s++; // Skip terminator
0610: }
0611:
0612: skipWhiteSpace();
0613: out[d++] = '\'';
0614:
0615: return true;
0616: }
0617:
0618: /** Syntax mask for time escape. */
0619: private static final byte[] timeMask = { '#', '#', ':', '#', '#',
0620: ':', '#', '#' };
0621:
0622: /** Syntax mask for date escape. */
0623: private static final byte[] dateMask = { '#', '#', '#', '#', '-',
0624: '#', '#', '-', '#', '#' };
0625:
0626: /** Syntax mask for timestamp escape. */
0627: static final byte[] timestampMask = { '#', '#', '#', '#', '-', '#',
0628: '#', '-', '#', '#', ' ', '#', '#', ':', '#', '#', ':', '#',
0629: '#' };
0630:
0631: /**
0632: * Processes the JDBC escape {oj left outer join etc}.
0633: *
0634: * @throws SQLException
0635: */
0636: private void outerJoinEscape() throws SQLException {
0637: while (in[s] != '}') {
0638: final char c = in[s];
0639:
0640: switch (c) {
0641: case '\'':
0642: case '"':
0643: case '[':
0644: copyString();
0645: break;
0646: case '{':
0647: // Nested escape!
0648: escape();
0649: break;
0650: case '?':
0651: copyParam(null, d);
0652: break;
0653: default:
0654: out[d++] = c;
0655: s++;
0656: break;
0657: }
0658: }
0659: }
0660:
0661: /** Map of jdbc to sybase function names. */
0662: private static HashMap fnMap = new HashMap();
0663: /** Map of jdbc to sql server function names. */
0664: private static HashMap msFnMap = new HashMap();
0665: /** Map of jdbc to server data types for convert */
0666: private static HashMap cvMap = new HashMap();
0667:
0668: static {
0669: // Microsoft only functions
0670: msFnMap.put("length", "len($)");
0671: msFnMap.put("truncate", "round($, 1)");
0672: // Common functions
0673: fnMap.put("user", "user_name($)");
0674: fnMap.put("database", "db_name($)");
0675: fnMap.put("ifnull", "isnull($)");
0676: fnMap.put("now", "getdate($)");
0677: fnMap.put("atan2", "atn2($)");
0678: fnMap.put("mod", "($)");
0679: fnMap.put("length", "char_length($)");
0680: fnMap.put("locate", "charindex($)");
0681: fnMap.put("repeat", "replicate($)");
0682: fnMap.put("insert", "stuff($)");
0683: fnMap.put("lcase", "lower($)");
0684: fnMap.put("ucase", "upper($)");
0685: fnMap.put("concat", "($)");
0686: fnMap.put("curdate",
0687: "convert(datetime, convert(varchar, getdate(), 112))");
0688: fnMap.put("curtime",
0689: "convert(datetime, convert(varchar, getdate(), 108))");
0690: fnMap.put("dayname", "datename(weekday,$)");
0691: fnMap.put("dayofmonth", "datepart(day,$)");
0692: fnMap.put("dayofweek",
0693: "((datepart(weekday,$)+@@DATEFIRST-1)%7+1)");
0694: fnMap.put("dayofyear", "datepart(dayofyear,$)");
0695: fnMap.put("hour", "datepart(hour,$)");
0696: fnMap.put("minute", "datepart(minute,$)");
0697: fnMap.put("second", "datepart(second,$)");
0698: fnMap.put("year", "datepart(year,$)");
0699: fnMap.put("quarter", "datepart(quarter,$)");
0700: fnMap.put("month", "datepart(month,$)");
0701: fnMap.put("week", "datepart(week,$)");
0702: fnMap.put("monthname", "datename(month,$)");
0703: fnMap.put("timestampadd", "dateadd($)");
0704: fnMap.put("timestampdiff", "datediff($)");
0705: // convert jdbc to sql types
0706: cvMap.put("binary", "varbinary");
0707: cvMap.put("char", "varchar");
0708: cvMap.put("date", "datetime");
0709: cvMap.put("double", "float");
0710: cvMap.put("longvarbinary", "image");
0711: cvMap.put("longvarchar", "text");
0712: cvMap.put("time", "datetime");
0713: cvMap.put("timestamp", "timestamp");
0714: }
0715:
0716: /**
0717: * Processes the JDBC escape {fn function()}.
0718: *
0719: * @throws SQLException
0720: */
0721: private void functionEscape() throws SQLException {
0722: char tc = terminator;
0723: skipWhiteSpace();
0724: StringBuffer nameBuf = new StringBuffer();
0725: //
0726: // Capture name
0727: //
0728: while (isIdentifier(in[s])) {
0729: nameBuf.append(in[s++]);
0730: }
0731:
0732: String name = nameBuf.toString().toLowerCase();
0733: //
0734: // Now collect arguments
0735: //
0736: skipWhiteSpace();
0737: mustbe('(', false);
0738: int parenCnt = 1;
0739: int argStart = d;
0740: int arg2Start = 0;
0741: terminator = ')';
0742: while (in[s] != ')' || parenCnt > 1) {
0743: final char c = in[s];
0744:
0745: switch (c) {
0746: case '\'':
0747: case '"':
0748: case '[':
0749: copyString();
0750: break;
0751: case '{':
0752: // Process nested escapes!
0753: escape();
0754: break;
0755: case ',':
0756: if (arg2Start == 0) {
0757: arg2Start = d - argStart;
0758: }
0759: if ("concat".equals(name)) {
0760: out[d++] = '+';
0761: s++;
0762: } else if ("mod".equals(name)) {
0763: out[d++] = '%';
0764: s++;
0765: } else {
0766: out[d++] = c;
0767: s++;
0768: }
0769: break;
0770: case '(':
0771: parenCnt++;
0772: out[d++] = c;
0773: s++;
0774: break;
0775: case ')':
0776: parenCnt--;
0777: out[d++] = c;
0778: s++;
0779: break;
0780: default:
0781: out[d++] = c;
0782: s++;
0783: break;
0784: }
0785: }
0786:
0787: String args = String.valueOf(out, argStart, d - argStart)
0788: .trim();
0789:
0790: d = argStart;
0791: mustbe(')', false);
0792: terminator = tc;
0793: skipWhiteSpace();
0794:
0795: //
0796: // Process convert scalar function.
0797: // Arguments need to be reversed and the data type
0798: // argument converted to an SQL server type
0799: //
0800: if ("convert".equals(name) && arg2Start < args.length() - 1) {
0801: String arg2 = args.substring(arg2Start + 1).trim()
0802: .toLowerCase();
0803: String dataType = (String) cvMap.get(arg2);
0804:
0805: if (dataType == null) {
0806: // Will get server error if invalid type passed
0807: dataType = arg2;
0808: }
0809:
0810: copyLiteral("convert(");
0811: copyLiteral(dataType);
0812: out[d++] = ',';
0813: copyLiteral(args.substring(0, arg2Start));
0814: out[d++] = ')';
0815:
0816: return;
0817: }
0818:
0819: //
0820: // See if function mapped
0821: //
0822: String fn;
0823: if (connection.getServerType() == Driver.SQLSERVER) {
0824: fn = (String) msFnMap.get(name);
0825: if (fn == null) {
0826: fn = (String) fnMap.get(name);
0827: }
0828: } else {
0829: fn = (String) fnMap.get(name);
0830: }
0831: if (fn == null) {
0832: // Not mapped so assume simple case
0833: copyLiteral(name);
0834: out[d++] = '(';
0835: copyLiteral(args);
0836: out[d++] = ')';
0837: return;
0838: }
0839: //
0840: // Process timestamp interval constants
0841: //
0842: if (args.length() > 8
0843: && args.substring(0, 8).equalsIgnoreCase("sql_tsi_")) {
0844: args = args.substring(8);
0845: if (args.length() > 11
0846: && args.substring(0, 11).equalsIgnoreCase(
0847: "frac_second")) {
0848: args = "millisecond" + args.substring(11);
0849: }
0850: }
0851: //
0852: // Substitute mapped function name and arguments
0853: //
0854: final int len = fn.length();
0855: for (int i = 0; i < len; i++) {
0856: final char c = fn.charAt(i);
0857: if (c == '$') {
0858: // Substitute arguments
0859: copyLiteral(args);
0860: } else {
0861: out[d++] = c;
0862: }
0863: }
0864: }
0865:
0866: /**
0867: * Processes the JDBC escape {escape 'X'}.
0868: *
0869: * @throws SQLException
0870: */
0871: private void likeEscape() throws SQLException {
0872: copyLiteral("escape ");
0873: skipWhiteSpace();
0874:
0875: if (in[s] == '\'' || in[s] == '"') {
0876: copyString();
0877: } else {
0878: mustbe('\'', true);
0879: }
0880:
0881: skipWhiteSpace();
0882: }
0883:
0884: /**
0885: * Processes the JDBC escape sequences.
0886: *
0887: * @throws SQLException
0888: */
0889: private void escape() throws SQLException {
0890: char tc = terminator;
0891: terminator = '}';
0892: StringBuffer escBuf = new StringBuffer();
0893: s++;
0894: skipWhiteSpace();
0895:
0896: if (in[s] == '?') {
0897: copyParam("@return_status", -1);
0898: skipWhiteSpace();
0899: mustbe('=', false);
0900: skipWhiteSpace();
0901:
0902: while (Character.isLetter(in[s])) {
0903: escBuf.append(Character.toLowerCase(in[s++]));
0904: }
0905:
0906: skipWhiteSpace();
0907: String esc = escBuf.toString();
0908:
0909: if ("call".equals(esc)) {
0910: callEscape();
0911: } else {
0912: throw new SQLException(Messages.get(
0913: "error.parsesql.syntax", "call", String
0914: .valueOf(s)), "22019");
0915: }
0916: } else {
0917: while (Character.isLetter(in[s])) {
0918: escBuf.append(Character.toLowerCase(in[s++]));
0919: }
0920:
0921: skipWhiteSpace();
0922: String esc = escBuf.toString();
0923:
0924: if ("call".equals(esc)) {
0925: callEscape();
0926: } else if ("t".equals(esc)) {
0927: if (!getDateTimeField(timeMask)) {
0928: throw new SQLException(Messages.get(
0929: "error.parsesql.syntax", "time", String
0930: .valueOf(s)), "22019");
0931: }
0932: } else if ("d".equals(esc)) {
0933: if (!getDateTimeField(dateMask)) {
0934: throw new SQLException(Messages.get(
0935: "error.parsesql.syntax", "date", String
0936: .valueOf(s)), "22019");
0937: }
0938: } else if ("ts".equals(esc)) {
0939: if (!getDateTimeField(timestampMask)) {
0940: throw new SQLException(Messages.get(
0941: "error.parsesql.syntax", "timestamp",
0942: String.valueOf(s)), "22019");
0943: }
0944: } else if ("oj".equals(esc)) {
0945: outerJoinEscape();
0946: } else if ("fn".equals(esc)) {
0947: functionEscape();
0948: } else if ("escape".equals(esc)) {
0949: likeEscape();
0950: } else {
0951: throw new SQLException(Messages
0952: .get("error.parsesql.badesc", esc, String
0953: .valueOf(s)), "22019");
0954: }
0955: }
0956:
0957: mustbe('}', false);
0958: terminator = tc;
0959: }
0960:
0961: /**
0962: * Extracts the first table name following the keyword FROM.
0963: *
0964: * @return the table name as a <code>String</code>
0965: */
0966: private String getTableName() throws SQLException {
0967: StringBuffer name = new StringBuffer(128);
0968: copyWhiteSpace();
0969: char c = (s < len) ? in[s] : ' ';
0970: if (c == '{') {
0971: // Start of {oj ... } we can assume that there is
0972: // more than one table in select and therefore
0973: // it would not be updateable.
0974: return "";
0975: }
0976: //
0977: // Skip any leading comments before first table name
0978: //
0979: while (c == '/' || c == '-' && s + 1 < len) {
0980: if (c == '/') {
0981: if (in[s + 1] == '*') {
0982: skipMultiComments();
0983: } else {
0984: break;
0985: }
0986: } else {
0987: if (in[s + 1] == '-') {
0988: skipSingleComments();
0989: } else {
0990: break;
0991: }
0992: }
0993: copyWhiteSpace();
0994: c = (s < len) ? in[s] : ' ';
0995: }
0996:
0997: if (c == '{') {
0998: // See comment above
0999: return "";
1000: }
1001: //
1002: // Now process table name
1003: //
1004: while (s < len) {
1005: if (c == '[' || c == '"') {
1006: int start = d;
1007: copyString();
1008: name.append(String.valueOf(out, start, d - start));
1009: copyWhiteSpace();
1010: c = (s < len) ? in[s] : ' ';
1011: } else {
1012: int start = d;
1013: c = (s < len) ? in[s++] : ' ';
1014: while ((isIdentifier(c)) && c != '.' && c != ',') {
1015: out[d++] = c;
1016: c = (s < len) ? in[s++] : ' ';
1017: }
1018: name.append(String.valueOf(out, start, d - start));
1019: s--;
1020: copyWhiteSpace();
1021: c = (s < len) ? in[s] : ' ';
1022: }
1023: if (c != '.') {
1024: break;
1025: }
1026: name.append(c);
1027: out[d++] = c;
1028: s++;
1029: copyWhiteSpace();
1030: c = (s < len) ? in[s] : ' ';
1031: }
1032: return name.toString();
1033: }
1034:
1035: /**
1036: * Parses the SQL statement processing JDBC escapes and parameter markers.
1037: *
1038: * @param extractTable true to return the first table name in the FROM clause of a select
1039: * @return The processed SQL statement, any procedure name, the first
1040: * SQL keyword and (optionally) the first table name as elements 0 1, 2 and 3 of the
1041: * returned <code>String[]</code>.
1042: * @throws SQLException
1043: */
1044: String[] parse(boolean extractTable) throws SQLException {
1045: boolean isSelect = false;
1046: boolean isModified = false;
1047: boolean isSlowScan = true;
1048: try {
1049: while (s < len) {
1050: final char c = in[s];
1051:
1052: switch (c) {
1053: case '{':
1054: escape();
1055: isModified = true;
1056: break;
1057: case '[':
1058: case '"':
1059: case '\'':
1060: copyString();
1061: break;
1062: case '?':
1063: copyParam(null, d);
1064: break;
1065: case '/':
1066: if (s + 1 < len && in[s + 1] == '*') {
1067: skipMultiComments();
1068: } else {
1069: out[d++] = c;
1070: s++;
1071: }
1072: break;
1073: case '-':
1074: if (s + 1 < len && in[s + 1] == '-') {
1075: skipSingleComments();
1076: } else {
1077: out[d++] = c;
1078: s++;
1079: }
1080: break;
1081: default:
1082: if (isSlowScan && Character.isLetter(c)) {
1083: if (keyWord == null) {
1084: keyWord = copyKeyWord();
1085: if ("select".equals(keyWord)) {
1086: isSelect = true;
1087: }
1088: isSlowScan = extractTable && isSelect;
1089: break;
1090: }
1091: if (extractTable && isSelect) {
1092: String sqlWord = copyKeyWord();
1093: if ("from".equals(sqlWord)) {
1094: // Ensure only first 'from' is processed
1095: isSlowScan = false;
1096: tableName = getTableName();
1097: }
1098: break;
1099: }
1100: }
1101:
1102: out[d++] = c;
1103: s++;
1104: break;
1105: }
1106: }
1107:
1108: //
1109: // Impose a reasonable maximum limit on the number of parameters
1110: // unless the connection is sending statements unprepared (i.e. by
1111: // building a plain query) and this is not a procedure call.
1112: //
1113: if (params != null && params.size() > 255
1114: && connection.getPrepareSql() != TdsCore.UNPREPARED
1115: && procName != null) {
1116: int limit = 255; // SQL 6.5 and Sybase < 12.50
1117: if (connection.getServerType() == Driver.SYBASE) {
1118: if (connection.getDatabaseMajorVersion() > 12
1119: || connection.getDatabaseMajorVersion() == 12
1120: && connection.getDatabaseMinorVersion() >= 50) {
1121: limit = 2000; // Actually 2048 but allow some head room
1122: }
1123: } else {
1124: if (connection.getDatabaseMajorVersion() == 7) {
1125: limit = 1000; // Actually 1024
1126: } else if (connection.getDatabaseMajorVersion() > 7) {
1127: limit = 2000; // Actually 2100
1128: }
1129:
1130: }
1131: if (params.size() > limit) {
1132: throw new SQLException(Messages.get(
1133: "error.parsesql.toomanyparams", Integer
1134: .toString(limit)), "22025");
1135: }
1136: }
1137: String result[] = new String[4];
1138:
1139: // return sql and procname
1140: result[0] = (isModified) ? new String(out, 0, d) : sql;
1141: result[1] = procName;
1142: result[2] = (keyWord == null) ? "" : keyWord;
1143: result[3] = tableName;
1144: return result;
1145: } catch (IndexOutOfBoundsException e) {
1146: // Should only come here if string is invalid in some way.
1147: throw new SQLException(Messages.get(
1148: "error.parsesql.missing", String
1149: .valueOf(terminator)), "22025");
1150: }
1151: }
1152: }
|