001: /*
002: * ScriptParser.java
003: *
004: * This file is part of SQL Workbench/J, http://www.sql-workbench.net
005: *
006: * Copyright 2002-2008, Thomas Kellerer
007: * No part of this code maybe reused without the permission of the author
008: *
009: * To contact the author please send an email to: support@sql-workbench.net
010: *
011: */
012: package workbench.sql;
013:
014: import java.io.BufferedReader;
015: import java.io.File;
016: import java.io.FileNotFoundException;
017: import java.io.IOException;
018: import java.util.ArrayList;
019: import java.util.Iterator;
020: import java.util.NoSuchElementException;
021: import workbench.log.LogMgr;
022: import workbench.resource.Settings;
023: import workbench.util.EncodingUtil;
024: import workbench.util.FileUtil;
025: import workbench.util.StringUtil;
026:
027: /**
028: * A class to parse a SQL script and return the individual commands
029: * in the script. The actual parsing is done by using an instance
030: * of {@link IteratingScriptParser}
031: *
032: * @author support@sql-workbench.net
033: */
034: public class ScriptParser implements Iterator {
035:
036: private String originalScript = null;
037: private ArrayList<ScriptCommandDefinition> commands = null;
038: private DelimiterDefinition delimiter = DelimiterDefinition.STANDARD_DELIMITER;
039: private DelimiterDefinition alternateDelimiter;
040: private int currentIteratorIndex = -42;
041: private boolean checkEscapedQuotes = true;
042: private IteratingScriptParser iteratingParser = null;
043: private boolean emptyLineIsSeparator = false;
044: private boolean supportOracleInclude = true;
045: private boolean checkSingleLineCommands = true;
046: private boolean returnTrailingWhitesapce = false;
047: private String alternateLineComment = "--";
048: private boolean useAlternateDelimiter = false;
049:
050: private int maxFileSize;
051:
052: public ScriptParser() {
053: this (Settings.getInstance().getInMemoryScriptSizeThreshold());
054: }
055:
056: /**
057: * Create a ScriptParser for the given Script.
058: * The delimiter to be used will be evaluated dynamically
059: */
060: public ScriptParser(String aScript) {
061: this .setScript(aScript);
062: }
063:
064: /** Create a ScriptParser
065: *
066: * The actual script needs to be specified with setScript()
067: * The delimiter will be evaluated dynamically
068: */
069: public ScriptParser(int fileSize) {
070: maxFileSize = fileSize;
071: }
072:
073: /**
074: * Initialize a ScriptParser from a file.
075: * The delimiter will be evaluated dynamically
076: */
077: public ScriptParser(File f) throws IOException {
078: this (f, null);
079: }
080:
081: /**
082: * Initialize a ScriptParser from a file.
083: * The delimiter will be evaluated dynamically
084: */
085: public ScriptParser(File f, String encoding) throws IOException {
086: setFile(f, encoding);
087: }
088:
089: public void setFile(File f) throws IOException {
090: setFile(f, null);
091: }
092:
093: /**
094: * Define the source file for this ScriptParser.
095: * Depending on the size the file might be read into memory or not
096: */
097: public void setFile(File f, String encoding) throws IOException {
098: if (!f.exists())
099: throw new FileNotFoundException(f.getName() + " not found");
100:
101: if (f.length() < this .maxFileSize) {
102: this .readScriptFromFile(f, encoding);
103: this .findDelimiterToUse();
104: } else {
105: this .iteratingParser = new IteratingScriptParser(f,
106: encoding);
107: configureParserInstance(this .iteratingParser);
108: }
109: }
110:
111: public void readScriptFromFile(File f) throws IOException {
112: this .readScriptFromFile(f, null);
113: }
114:
115: public void readScriptFromFile(File f, String encoding)
116: throws IOException {
117: BufferedReader in = null;
118: StringBuilder content = null;
119: try {
120: content = new StringBuilder((int) f.length());
121: in = EncodingUtil.createBufferedReader(f, encoding);
122: String line = in.readLine();
123: while (line != null) {
124: content.append(line);
125: content.append('\n');
126: line = in.readLine();
127: }
128: } catch (Exception e) {
129: LogMgr.logError("ScriptParser.readFile()",
130: "Error reading file " + f.getAbsolutePath(), e);
131: content = null;
132: } finally {
133: FileUtil.closeQuitely(in);
134: }
135: this .setScript(content == null ? "" : content.toString());
136: }
137:
138: public void allowEmptyLineAsSeparator(boolean flag) {
139: this .emptyLineIsSeparator = flag;
140: }
141:
142: public void setAlternateLineComment(String comment) {
143: this .alternateLineComment = comment;
144: }
145:
146: public void setReturnStartingWhitespace(boolean flag) {
147: this .returnTrailingWhitesapce = flag;
148: }
149:
150: public void setCheckForSingleLineCommands(boolean flag) {
151: this .checkSingleLineCommands = flag;
152: }
153:
154: public void setSupportOracleInclude(boolean flag) {
155: this .supportOracleInclude = flag;
156: }
157:
158: /**
159: * Define the script to be parsed.
160: * The delimiter to be used will be checked automatically
161: * First the it will check if the script ends with the alternate delimiter
162: * if this is not the case, the script will be checked if it ends with GO
163: * If so, GO will be used (MS SQL Server script style)
164: * If none of the above is true, ; (semicolon) will be used
165: */
166: public void setScript(String aScript) {
167: if (aScript == null)
168: throw new NullPointerException("SQL script may not be null");
169: if (aScript.equals(this .originalScript))
170: return;
171: this .originalScript = aScript;
172: this .findDelimiterToUse();
173: this .commands = null;
174: this .iteratingParser = null;
175: }
176:
177: public void setDelimiter(DelimiterDefinition delim) {
178: this .setDelimiters(delim, null);
179: }
180:
181: /**
182: * Sets the alternate delimiter. This implies that
183: * by default the semicolon is used, and only if
184: * the alternate delimiter is detected, that will be used.
185: *
186: * If only one delimiter should be used (and no automatic checking
187: * for an alternate delimiter), use {@link #setDelimiter(DelimiterDefinition)}
188: */
189: public void setAlternateDelimiter(DelimiterDefinition alt) {
190: setDelimiters(DelimiterDefinition.STANDARD_DELIMITER, alt);
191: }
192:
193: /**
194: * Define the delimiters to be used. If the (in-memory) script ends with
195: * the defined alternate delimiter, then the alternate is used, otherwise
196: * the default
197: */
198: public void setDelimiters(DelimiterDefinition defaultDelim,
199: DelimiterDefinition alternateDelim) {
200: this .delimiter = defaultDelim;
201: this .alternateDelimiter = alternateDelim;
202:
203: if (this .originalScript != null) {
204: findDelimiterToUse();
205: }
206: }
207:
208: /**
209: * Try to find out which delimiter should be used for the current script.
210: * First it will check if the script ends with the alternate delimiter
211: * if this is not the case, the script will be checked if it ends with GO
212: * If so, GO will be used (MS SQL Server script style)
213: * If none of the above is true, ; (semicolon) will be used
214: */
215: private void findDelimiterToUse() {
216: if (this .alternateDelimiter == null)
217: return;
218: if (this .originalScript == null)
219: return;
220:
221: useAlternateDelimiter = (alternateDelimiter
222: .terminatesScript(originalScript));
223: this .commands = null;
224: }
225:
226: /**
227: * Return the index from the overall script mapped to the
228: * index inside the specified command. For a single command
229: * script scriptCursorLocation will be the same as
230: * the location inside the dedicated command.
231: * @param commandIndex the index for the command to check
232: * @param cursorPos the index in the overall script
233: * @return the relative index inside the command
234: */
235: public int getIndexInCommand(int commandIndex, int cursorPos) {
236: if (this .commands == null)
237: this .parseCommands();
238: if (commandIndex < 0 || commandIndex >= this .commands.size())
239: return -1;
240: ScriptCommandDefinition b = this .commands.get(commandIndex);
241: int start = b.getStartPositionInScript();
242: int end = b.getEndPositionInScript();
243: int relativePos = (cursorPos - start);
244: int commandLength = (end - start);
245: if (relativePos > commandLength) {
246: // This can happen when trimming the statements.
247: relativePos = commandLength;
248: }
249: return relativePos;
250: }
251:
252: /**
253: * Return the command index for the command which is located at
254: * the given index of the current script.
255: */
256: public int getCommandIndexAtCursorPos(int cursorPos) {
257: if (this .commands == null)
258: this .parseCommands();
259: if (cursorPos < 0)
260: return -1;
261: int count = this .commands.size();
262: if (count == 1)
263: return 0;
264: if (count == 0)
265: return -1;
266: for (int i = 0; i < count - 1; i++) {
267: ScriptCommandDefinition b = this .commands.get(i);
268: ScriptCommandDefinition next = this .commands.get(i + 1);
269: if (b.getWhitespaceStart() <= cursorPos
270: && b.getEndPositionInScript() >= cursorPos)
271: return i;
272: if (cursorPos > b.getEndPositionInScript()
273: && cursorPos < next.getEndPositionInScript())
274: return i + 1;
275: if (b.getEndPositionInScript() > cursorPos
276: && next.getWhitespaceStart() <= cursorPos)
277: return i + 1;
278: }
279: ScriptCommandDefinition b = this .commands.get(count - 1);
280: if (b.getWhitespaceStart() <= cursorPos
281: && b.getEndPositionInScript() >= cursorPos)
282: return count - 1;
283: return -1;
284: }
285:
286: /**
287: * Get the starting offset in the original script for the command indicated by index
288: */
289: public int getStartPosForCommand(int index) {
290: if (this .commands == null)
291: this .parseCommands();
292: if (index < 0 || index >= this .commands.size())
293: return -1;
294: ScriptCommandDefinition b = this .commands.get(index);
295: int start = b.getStartPositionInScript();
296: return start;
297: }
298:
299: /**
300: * Get the starting offset in the original script for the command indicated by index
301: */
302: public int getEndPosForCommand(int index) {
303: if (this .commands == null)
304: this .parseCommands();
305: if (index < 0 || index >= this .commands.size())
306: return -1;
307: ScriptCommandDefinition b = this .commands.get(index);
308: return b.getEndPositionInScript();
309: }
310:
311: /**
312: * Find the position in the original script for the next start of line
313: */
314: public int findNextLineStart(int pos) {
315: if (this .originalScript == null)
316: return -1;
317: if (pos < 0)
318: return pos;
319: int len = this .originalScript.length();
320: if (pos >= len)
321: return pos;
322: char c = this .originalScript.charAt(pos);
323: while (pos < len && (c == '\n' || c == '\r')) {
324: pos++;
325: c = this .originalScript.charAt(pos);
326: }
327: return pos;
328: }
329:
330: /**
331: * Return the command at the given index position.
332: */
333: public String getCommand(int index) {
334: return getCommand(index, true);
335: }
336:
337: /**
338: * Return the command at the given index position.
339: */
340: public String getCommand(int index, boolean rightTrimCommand) {
341: if (this .commands == null)
342: this .parseCommands();
343: if (index < 0 || index >= this .commands.size())
344: return null;
345: ScriptCommandDefinition c = this .commands.get(index);
346: String s = originalScript
347: .substring(c.getStartPositionInScript(), c
348: .getEndPositionInScript());
349: if (rightTrimCommand)
350: return StringUtil.rtrim(s);
351: else
352: return s;
353: }
354:
355: public int getSize() {
356: if (this .commands == null)
357: this .parseCommands();
358: return this .commands.size();
359: }
360:
361: /**
362: * Return an Iterator which allows to iterate over
363: * the commands from the script. The Iterator
364: * will return objects of type {@link ScriptCommandDefinition}
365: */
366: public Iterator getIterator() {
367: startIterator();
368: return this ;
369: }
370:
371: public void startIterator() {
372: this .currentIteratorIndex = 0;
373: if (this .iteratingParser == null && this .commands == null) {
374: this .parseCommands();
375: } else if (this .iteratingParser != null) {
376: configureParserInstance(this .iteratingParser);
377: this .iteratingParser.reset();
378:
379: }
380: }
381:
382: public void done() {
383: if (this .iteratingParser != null) {
384: this .iteratingParser.done();
385: }
386: this .currentIteratorIndex = -42;
387: }
388:
389: /**
390: * Check for quote characters that are escaped using a
391: * backslash. If turned on (flag == true) the following
392: * SQL statement would be valid (different to the SQL standard):
393: * <pre>INSERT INTO myTable (column1) VALUES ('Arthurs\'s house');</pre>
394: * but the following Script would generate an error:
395: * <pre>INSERT INTO myTable (file_path) VALUES ('c:\');</pre>
396: * because the last quote would not bee seen as a closing quote
397: */
398: public void setCheckEscapedQuotes(boolean flag) {
399: this .checkEscapedQuotes = flag;
400: }
401:
402: public String getDelimiterString() {
403: if (this .useAlternateDelimiter)
404: return this .alternateDelimiter.getDelimiter();
405: return this .delimiter.getDelimiter();
406: }
407:
408: private void configureParserInstance(IteratingScriptParser p) {
409: p.setSupportOracleInclude(this .supportOracleInclude);
410: p.allowEmptyLineAsSeparator(this .emptyLineIsSeparator);
411: p.setCheckEscapedQuotes(this .checkEscapedQuotes);
412: p.setDelimiter(useAlternateDelimiter ? this .alternateDelimiter
413: : this .delimiter);
414: p.setReturnStartingWhitespace(this .returnTrailingWhitesapce);
415: p.setAlternateLineComment(this .alternateLineComment);
416: p.setDelimiter(useAlternateDelimiter ? this .alternateDelimiter
417: : this .delimiter);
418:
419: if (useAlternateDelimiter) {
420: p.setCheckForSingleLineCommands(false);
421: } else {
422: p
423: .setCheckForSingleLineCommands(this .checkSingleLineCommands);
424: }
425: }
426:
427: /**
428: * Parse the given SQL Script into a List of single SQL statements.
429: */
430: private void parseCommands() {
431: this .commands = new ArrayList<ScriptCommandDefinition>();
432: IteratingScriptParser p = new IteratingScriptParser();
433: configureParserInstance(p);
434: p.setScript(this .originalScript);
435:
436: ScriptCommandDefinition c = null;
437: int index = 0;
438:
439: while ((c = p.getNextCommand()) != null) {
440: c.setIndexInScript(index);
441: index++;
442: this .commands.add(c);
443: }
444: }
445:
446: /**
447: * Check if more commands are present.
448: */
449: public boolean hasNext() {
450: if (this .currentIteratorIndex == -42)
451: throw new IllegalStateException("Iterator not initialized");
452: if (this .iteratingParser != null) {
453: return this .iteratingParser.hasMoreCommands();
454: } else {
455: return this .currentIteratorIndex < this .commands.size();
456: }
457: }
458:
459: /**
460: * Return the next SQL command from the script.
461: * This is delegated to {@link #getNextCommand()}
462: * @return a String object representing the SQL command
463: * @throws IllegalStateException if the Iterator has not been initialized using {@link #getIterator()}
464: * @see IteratingScriptParser#getNextCommand()
465: * @see #getNextCommand()
466: */
467: public Object next() throws NoSuchElementException {
468: if (this .currentIteratorIndex == -42)
469: throw new NoSuchElementException("Iterator not initialized");
470: return getNextCommand();
471: }
472:
473: /**
474: * Return the next {@link ScriptCommandDefinition} from the script.
475: *
476: * @throws IllegalStateException if the Iterator has not been initialized using {@link #getIterator()}
477: * @see IteratingScriptParser#getNextCommand()
478: * @see #next()
479: */
480: public String getNextCommand() throws NoSuchElementException {
481: if (this .currentIteratorIndex == -42)
482: throw new NoSuchElementException("Iterator not initialized");
483: ScriptCommandDefinition command = null;
484: String result = null;
485: if (this .iteratingParser != null) {
486: command = this .iteratingParser.getNextCommand();
487: if (command == null)
488: return null;
489: result = command.getSQL();
490: } else {
491: command = this .commands.get(this .currentIteratorIndex);
492: result = this .originalScript.substring(command
493: .getStartPositionInScript(), command
494: .getEndPositionInScript());
495: this .currentIteratorIndex++;
496: }
497:
498: return result;
499: }
500:
501: /**
502: * Not implemented, as removing commands is not possible.
503: * A call to this method simply does nothing.
504: */
505: public void remove() {
506: }
507:
508: }
|