001: //
002: // Copyright 1998 CDS Networks, Inc., Medford Oregon
003: //
004: // All rights reserved.
005: //
006: // Redistribution and use in source and binary forms, with or without
007: // modification, are permitted provided that the following conditions are met:
008: // 1. Redistributions of source code must retain the above copyright
009: // notice, this list of conditions and the following disclaimer.
010: // 2. Redistributions in binary form must reproduce the above copyright
011: // notice, this list of conditions and the following disclaimer in the
012: // documentation and/or other materials provided with the distribution.
013: // 3. All advertising materials mentioning features or use of this software
014: // must display the following acknowledgement:
015: // This product includes software developed by CDS Networks, Inc.
016: // 4. The name of CDS Networks, Inc. may not be used to endorse or promote
017: // products derived from this software without specific prior
018: // written permission.
019: //
020: // THIS SOFTWARE IS PROVIDED BY CDS NETWORKS, INC. ``AS IS'' AND
021: // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
022: // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
023: // ARE DISCLAIMED. IN NO EVENT SHALL CDS NETWORKS, INC. BE LIABLE
024: // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
025: // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
026: // OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
027: // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
028: // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
029: // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
030: // SUCH DAMAGE.
031: //
032:
033: package com.internetcds.jdbc.tds;
034:
035: import java.sql.*;
036:
037: abstract public class EscapeProcessor {
038: public static final String cvsVersion = "$Id: EscapeProcessor.java,v 1.2 2007-10-19 13:21:40 sinisa Exp $";
039:
040: String input;
041:
042: public EscapeProcessor(String sql) {
043: input = sql;
044: } // EscapeProcessor()
045:
046: abstract public String expandDBSpecificFunction(
047: String escapeSequence) throws SQLException;
048:
049: /**
050: * is the string made up only of digits?
051: * <p>
052: * Note- Leading/trailing spaces or signs are not considered digits.
053: *
054: * @return true if the string has only digits, false otherwise.
055: */
056: private static boolean validDigits(String str) {
057: boolean result = true;
058: int i;
059:
060: for (i = 0, result = true; result && i < str.length(); i++) {
061: result = result && Character.isDigit(str.charAt(i));
062: }
063: return result;
064: } // validDigits()
065:
066: /**
067: * Given a string and an index into that string return the index
068: * of the next non-whitespace character.
069: *
070: * @return index of next non-whitespace character.
071: */
072: private static int skipWhitespace(String str, int i) {
073: while (i < str.length()
074: && Character.isWhitespace(str.charAt(i))) {
075: i++;
076: }
077: return i;
078: } // skipWhitespace()
079:
080: /**
081: * Given a string and an index into that string, advanvance the index
082: * iff it is on a quote character.
083: *
084: * @return
085: */
086: private static int skipQuote(String str, int i) {
087:
088: // skip over the leading quote if it exists
089: if (i < str.length()
090: && (str.charAt(i) == '\'' || str.charAt(i) == '"')) {
091: // XXX Note- The spec appears to prohibit the quote character,
092: // but many drivers allow it. We should probably control this
093: // with a flag.
094: i++;
095: }
096: return i;
097: } // skipQuote()
098:
099: /**
100: * Convert a JDBC SQL escape date sequence into a datestring recognized
101: * by SQLServer.
102: *
103: */
104: private static String getDate(String str) throws SQLException {
105: int i;
106:
107: // skip over the "d "
108: i = 2;
109:
110: // skip any additional spaces
111: i = skipWhitespace(str, i);
112:
113: i = skipQuote(str, i);
114:
115: // i is now up to the point where the date had better start.
116: if (((str.length() - i) < 10) || str.charAt(i + 4) != '-'
117: || str.charAt(i + 7) != '-') {
118: throw new SQLException("Malformed date");
119: }
120:
121: String year = str.substring(i, i + 4);
122: String month = str.substring(i + 5, i + 5 + 2);
123: String day = str.substring(i + 5 + 3, i + 5 + 3 + 2);
124:
125: // Make sure the year, month, and day are numeric
126: if (!validDigits(year) || !validDigits(month)
127: || !validDigits(day)) {
128: throw new SQLException("Malformed date");
129: }
130:
131: // Make sure there isn't any garbage after the date
132: i = i + 10;
133: i = skipWhitespace(str, i);
134: i = skipQuote(str, i);
135: i = skipWhitespace(str, i);
136:
137: if (i < str.length()) {
138: throw new SQLException("Malformed date");
139: }
140:
141: return "'" + year + month + day + "'";
142: } // getDate()
143:
144: /**
145: * Convert a JDBC SQL escape time sequence into a time string recognized
146: * by SQLServer.
147: *
148: */
149: private static String getTime(String str) throws SQLException {
150: int i;
151:
152: // skip over the "t "
153: i = 2;
154:
155: // skip any additional spaces
156: i = skipWhitespace(str, i);
157:
158: i = skipQuote(str, i);
159:
160: // i is now up to the point where the date had better start.
161: if (((str.length() - i) < 8) || str.charAt(i + 2) != ':'
162: || str.charAt(i + 5) != ':') {
163: throw new SQLException("Malformed time");
164: }
165:
166: String hour = str.substring(i, i + 2);
167: String minute = str.substring(i + 3, i + 3 + 2);
168: String second = str.substring(i + 3 + 3, i + 3 + 3 + 2);
169:
170: // Make sure the year, month, and day are numeric
171: if (!validDigits(hour) || !validDigits(minute)
172: || !validDigits(second)) {
173: throw new SQLException("Malformed time");
174: }
175:
176: // Make sure there isn't any garbage after the time
177: i = i + 8;
178: i = skipWhitespace(str, i);
179: i = skipQuote(str, i);
180: i = skipWhitespace(str, i);
181:
182: if (i < str.length()) {
183: throw new SQLException("Malformed time");
184: }
185:
186: return "'" + hour + ":" + minute + ":" + second + "'";
187: } // getTime()
188:
189: /**
190: * Convert a JDBC SQL escape timestamp sequence into a date-time string
191: * by SQLServer.
192: *
193: */
194: private static String getTimestamp(String str) throws SQLException {
195: int i;
196:
197: // skip over the "d "
198: i = 2;
199:
200: // skip any additional spaces
201: i = skipWhitespace(str, i);
202:
203: i = skipQuote(str, i);
204:
205: // i is now up to the point where the date had better start.
206: if (((str.length() - i) < 19) || str.charAt(i + 4) != '-'
207: || str.charAt(i + 7) != '-') {
208: throw new SQLException("Malformed date");
209: }
210:
211: String year = str.substring(i, i + 4);
212: String month = str.substring(i + 5, i + 5 + 2);
213: String day = str.substring(i + 5 + 3, i + 5 + 3 + 2);
214:
215: // Make sure the year, month, and day are numeric
216: if (!validDigits(year) || !validDigits(month)
217: || !validDigits(day)) {
218: throw new SQLException("Malformed date");
219: }
220:
221: // Make sure there is at least one space between date and time
222: i = i + 10;
223: if (!Character.isWhitespace(str.charAt(i))) {
224: throw new SQLException("Malformed date");
225: }
226:
227: // skip the whitespace
228: i = skipWhitespace(str, i);
229:
230: // see if it could be a time
231: if (((str.length() - i) < 8) || str.charAt(i + 2) != ':'
232: || str.charAt(i + 5) != ':') {
233: throw new SQLException("Malformed time");
234: }
235: String hour = str.substring(i, i + 2);
236: String minute = str.substring(i + 3, i + 3 + 2);
237: String second = str.substring(i + 3 + 3, i + 3 + 3 + 2);
238: String fraction = "000";
239: i = i + 8;
240: if (str.length() > i && str.charAt(i) == '.') {
241: fraction = "";
242: i++;
243: while (str.length() > i
244: && validDigits(str.substring(i, i + 1))) {
245: fraction = fraction + str.substring(i, i + 1);
246: i++;
247: }
248: if (fraction.length() > 3) {
249: fraction = fraction.substring(0, 3);
250: } else {
251: while (fraction.length() < 3) {
252: fraction = fraction + "0";
253: }
254: }
255: }
256:
257: // Make sure there isn't any garbage after the time
258: i = skipWhitespace(str, i);
259: i = skipQuote(str, i);
260: i = skipWhitespace(str, i);
261:
262: if (i < str.length()) {
263: throw new SQLException("Malformed date");
264: }
265:
266: return ("'" + year + month + day + " " + hour + ":" + minute
267: + ":" + second + "." + fraction + "'");
268: } // getTimestamp()
269:
270: public String expandEscape(String escapeSequence)
271: throws SQLException {
272: String str = new String(escapeSequence);
273: String result = null;
274:
275: // XXX Is it always okay to trim leading and trailing blanks?
276: str = str.trim();
277:
278: if (str.startsWith("fn ")) {
279: str = str.substring(3);
280:
281: result = expandCommonFunction(str);
282: if (result == null) {
283: result = expandDBSpecificFunction(str);
284: }
285: } else if (str.startsWith("call ")
286: || (str.startsWith("?=") && str.substring(2).trim()
287: .startsWith("call "))) {
288: throw new SQLException("Not implemented yet");
289: } else if (str.startsWith("d ")) {
290: result = getDate(str);
291: } else if (str.startsWith("t ")) {
292: result = getTime(str);
293: } else if (str.startsWith("ts ")) {
294: result = getTimestamp(str);
295: } else if (str.startsWith("oj ")) {
296: throw new SQLException("Not implemented yet");
297: } else {
298: throw new SQLException("Unrecognized escape sequence-\n"
299: + escapeSequence);
300: }
301:
302: return result;
303: } // expandEscape()
304:
305: /**
306: * Expand functions that are common to both SQLServer and Sybase
307: *
308: */
309: public String expandCommonFunction(String str) {
310: String result = null;
311:
312: if (str.equalsIgnoreCase("user()")) {
313: result = " user_name() ";
314: } else if (str.equalsIgnoreCase("now()")) {
315: result = " getdate() ";
316: }
317: return result;
318: } // expandCommonFunction()
319:
320: public String nativeString() throws SQLException {
321: return nativeString(input, '\\');
322: } // nativeString()
323:
324: private String nativeString(String sql, char escapeCharacter)
325: throws SQLException {
326: String result = "";
327:
328: String escape = "";
329: int i;
330:
331: // Simple finite state machine. Bonehead, but it works.
332: final int normal = 0;
333:
334: final int inString = 1;
335: final int inStringWithBackquote = 2;
336:
337: final int inEscape = 3;
338: final int inEscapeInString = 4;
339: final int inEscapeInStringWithBackquote = 5;
340:
341: int state = normal;
342: char ch;
343:
344: int escapeStartedAt = -1;
345: i = 0;
346: while (i < sql.length()) {
347: ch = sql.charAt(i);
348: switch (state) {
349: case normal: {
350: if (ch == '{') {
351: escapeStartedAt = i;
352: state = inEscape;
353: escape = "";
354: } else {
355: result = result + ch;
356:
357: if (ch == '\'')
358: state = inString;
359: }
360: break;
361: }
362: case inString:
363: case inStringWithBackquote: {
364: if ((i + 1) < sql.length()
365: && ch == escapeCharacter
366: && (sql.charAt(i + 1) == '_' || sql
367: .charAt(i + 1) == '%')) {
368: i++;
369: ch = sql.charAt(i);
370: result = result + '\\' + ch;
371: } else {
372: result = result + ch;
373: if (state == inStringWithBackquote) {
374: state = inString;
375: } else {
376: if (ch == '\\')
377: state = inStringWithBackquote;
378: if (ch == '\'')
379: state = normal;
380: }
381: }
382: break;
383: }
384: case inEscape: {
385: if (ch == '}') {
386: // At this point there are a couple of things to
387: // consider. First, if the escape is of the form
388: // "{escape 'c'} but it is not at the end of the SQL
389: // we consider that a malformed SQL string. If it
390: // is the "{escape 'c'}" clause and it is at the end
391: // of the string then we have to go through and
392: // reparse this whole thing again, this time with an
393: // escape character. Any other escape is handled in
394: // the expandEscape method()
395:
396: if (escape.startsWith("escape ")) {
397: char c;
398:
399: // make sure it is the last thing in the sql
400: if (i + 1 != sql.length()) {
401: throw new SQLException(
402: "Malformed statement. "
403: + "escape clause must be at "
404: + "the end of the query");
405: }
406:
407: // parse the sql again, this time without the
408: // ending string but with the escape character
409: // set
410:
411: c = findEscapeCharacter(sql
412: .substring(escapeStartedAt));
413:
414: result = nativeString(sql.substring(0,
415: escapeStartedAt), c);
416: state = normal;
417: } else {
418: state = normal;
419: result = result + expandEscape(escape);
420: escapeStartedAt = -1;
421: }
422: } else {
423: escape = escape + ch;
424: if (ch == '\'') {
425: state = inEscapeInString;
426: }
427: }
428: break;
429: }
430: case inEscapeInString:
431: case inEscapeInStringWithBackquote: {
432: escape = escape + ch;
433: if (state == inEscapeInStringWithBackquote) {
434: state = inEscapeInString;
435: } else {
436: if (ch == '\\')
437: state = inEscapeInStringWithBackquote;
438: if (ch == '\'')
439: state = inEscape;
440: }
441: break;
442: }
443: default: {
444: throw new SQLException(
445: "Internal error. Unknown state in FSM");
446: }
447: }
448: i++;
449: }
450:
451: if (state != normal && state != inString) {
452: throw new SQLException("Syntax error in SQL escape syntax");
453: }
454: return result;
455: } // nativeString()
456:
457: static char findEscapeCharacter(String original_str)
458: throws SQLException {
459: String str = new String(original_str);
460:
461: str = str.trim();
462: if (str.charAt(0) != '{' || str.charAt(str.length() - 1) != '}'
463: || str.length() < 12) {
464: throw new SQLException("Internal Error");
465: }
466:
467: str = str.substring(1, str.length() - 1);
468: str = str.trim();
469:
470: if (!str.startsWith("escape")) {
471: throw new SQLException("Internal Error");
472: }
473:
474: str = str.substring(6);
475: str = str.trim();
476: if (str.length() != 3 || str.charAt(0) != '\''
477: || str.charAt(2) != '\'') {
478: throw new SQLException("Malformed escape clause- |"
479: + original_str + "|");
480: }
481:
482: return str.charAt(1);
483: } // findEscapeCharacter()
484: }
|