/*
* Copyright (C) 2002 Christian Sell
* csell@users.sourceforge.net
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* created by cse, 07.10.2002 11:57:54
*
* @version $Id: ParserThread.java,v 1.10 2010-02-11 00:16:27 gerdwagner Exp $
*/
package net.sourceforge.squirrel_sql.client.session.parser.kernel;
import net.sourceforge.squirrel_sql.client.session.parser.kernel.completions.ErrorListener;
import net.sourceforge.squirrel_sql.client.session.parser.kernel.completions.SQLSelectStatementListener;
import net.sourceforge.squirrel_sql.client.session.parser.kernel.completions.SQLStatement;
import net.sourceforge.squirrel_sql.fw.util.StringManager;
import net.sourceforge.squirrel_sql.fw.util.StringManagerFactory;
import net.sourceforge.squirrel_sql.fw.util.log.ILogger;
import net.sourceforge.squirrel_sql.fw.util.log.LoggerController;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Vector;
/**
* a thread subclass which drives the SQL parser. The thread reads from a _workingBuffer
* which always blocks until data is made available. It can thus be run in the
* background, parsing the input from the text UI as it arrives.
*
* <em>Unfortunately, it depends on the generated parser/scanner and therefore
* cannot be generalized, unless the generated classes are made to implement public
* interfaces</em>
*/
public final class ParserThread extends Thread
{
private static final StringManager s_stringMgr =
StringManagerFactory.getStringManager(ParserThread.class);
private static final ILogger s_log = LoggerController.createLogger(ParserThread.class);
public static final String PARSER_THREAD_NM = "SQLParserThread";
private String _pendingString;
private Errors _errors;
private SQLSchema _schema;
private SQLStatement _curSQLSelectStat;
private Vector<TableAliasInfo> _workingTableAliasInfos =
new Vector<TableAliasInfo>();
private TableAliasInfo[] _lastRunTableAliasInfos = new TableAliasInfo[0];
private Vector<ErrorInfo> _workingErrorInfos = new Vector<ErrorInfo>();
private ErrorInfo[] _lastRunErrorInfos = new ErrorInfo[0];
private volatile boolean _exitThread;
private ParsingFinishedListener _parsingFinishedListener;
private int _lastParserRunOffset;
private int _lastErrEnd = -1;
private int _nextStatBegin = -1;
private String _workingString;
private IncrementalBuffer _workingBuffer;
private boolean _errorDetected;
private boolean _couldNotDetectPosErrorLogged;
public ParserThread(SQLSchema schema)
{
super(PARSER_THREAD_NM);
this._schema = schema;
ErrorListener errListener = new ErrorListener()
{
public void errorDetected(String message, int line, int column)
{
onErrorDetected(message, line, column);
}
};
this._errors = new Errors(errListener);
setPriority(Thread.MIN_PRIORITY);
start();
}
private void onErrorDetected(String message, int line, int column)
{
_errorDetected = true;
int errPos = getPos(line, column);
_lastErrEnd = getTokenEnd(errPos);
_nextStatBegin = predictNextStatementBegin(errPos);
if(_lastErrEnd > _nextStatBegin)
{
return;
}
int beginPos = _lastParserRunOffset + errPos;
int endPos = _lastParserRunOffset + _lastErrEnd;
if(beginPos < endPos)
{
_workingErrorInfos.add(new ErrorInfo(message, _lastParserRunOffset + errPos , _lastParserRunOffset + _lastErrEnd-1));
}
}
private int predictNextStatementBegin(int errPos)
{
int commentIntervals[][] = calculateCommentIntervals();
// for (int i = 0; i < commentIntervals.length; i++)
// {
// System.out.println("###################");
// System.out.println(_workingString.substring(commentIntervals[i][0], commentIntervals[i][1]));
// System.out.println("###################");
// }
int ret = errPos;
while( _workingString.length() > ret && (false == startsWithBeginKeyWord(ret) || isInComment(ret, commentIntervals)) )
{
++ret;
}
// if(_workingString.length() > ret)
// {
// System.out.println("*****************************BEGIN startsWithBeginKeyWord(ret) " + startsWithBeginKeyWord(ret) + " isInComment(ret, commentIntervals)" + isInComment(ret, commentIntervals));
// System.out.println(_workingString.substring(ret));
// }
return ret;
}
private int[][] calculateCommentIntervals()
{
Vector<int[]> ret = new Vector<int[]>();
boolean inMultiLineComment = false;
boolean inLineComment = false;
boolean isaSlash = false;
boolean isaStar = false;
boolean isaMinus = false;
int[] curComment = null;
for(int i=0; i < _workingString.length(); ++i)
{
if('*' == _workingString.charAt(i) && isaSlash && false == inMultiLineComment && false == inLineComment)
{
inMultiLineComment = true;
curComment = new int[]{i-1, -1};
}
else if('/' == _workingString.charAt(i) && isaStar && false == inLineComment && inMultiLineComment)
{
inMultiLineComment = false;
curComment[1] = i;
ret.add(curComment);
curComment = null;
}
else if('-' == _workingString.charAt(i) && isaMinus && false == inMultiLineComment && false == inLineComment)
{
inLineComment = true;
curComment = new int[]{i-1, -1};
}
else if('\n' == _workingString.charAt(i) && false == inMultiLineComment && inLineComment)
{
inLineComment = false;
curComment[1] = i;
ret.add(curComment);
curComment = null;
}
if('/' == _workingString.charAt(i))
{
isaSlash = true;
}
else if('*' == _workingString.charAt(i))
{
isaStar = true;
}
else if('-' == _workingString.charAt(i))
{
isaMinus = true;
}
else
{
isaSlash = false;
isaStar = false;
isaMinus = false;
}
}
if(null != curComment)
{
curComment[1] = _workingString.length();
}
return ret.toArray(new int[ret.size()][]);
}
private boolean isInComment(int ret, int commentIntervals[][])
{
for(int i=0; i < commentIntervals.length; ++i)
{
if(commentIntervals[i][0] <= ret && ret <= commentIntervals[i][1])
{
return true;
}
}
return false;
}
private boolean startsWithBeginKeyWord(int ret)
{
return startsWithIgnoreCase(ret, "SELECT")
|| startsWithIgnoreCase(ret, "UPDATE")
|| startsWithIgnoreCase(ret, "DELETE")
|| startsWithIgnoreCase(ret, "INSERT")
|| startsWithIgnoreCase(ret, "ALTER")
|| startsWithIgnoreCase(ret, "CREATE")
|| startsWithIgnoreCase(ret, "DROP");
}
private boolean startsWithIgnoreCase(int ret, String keyWord)
{
int beginPos = ret;
int endPos;
if(ret == 0)
{
// Either are at teh beginning ...
beginPos = 0;
}
else if(Character.isWhitespace(_workingString.charAt(ret-1)))
{
// or a white space must be in front of the keyword.
beginPos = ret;
}
else
{
return false;
}
if(_workingString.length() == beginPos + keyWord.length())
{
endPos = beginPos + keyWord.length();
}
else if(_workingString.length() > beginPos + keyWord.length() && Character.isWhitespace(_workingString.charAt(beginPos + keyWord.length())))
{
endPos = beginPos + keyWord.length();
}
else
{
return false;
}
return keyWord.equalsIgnoreCase(_workingString.substring(beginPos, endPos));
}
private int getTokenEnd(int errPos)
{
int ret = errPos;
while(_workingString.length() > ret && false == Character.isWhitespace(_workingString.charAt(ret)))
{
++ret;
}
return ret;
}
private int getPos(int line, int column)
{
int ix = 0;
for (int i = 0; i < line - 1; i++)
{
ix = getNextLineStartIx(ix);
if (Integer.MAX_VALUE == ix)
{
if (false == _couldNotDetectPosErrorLogged)
{
_couldNotDetectPosErrorLogged = true;
String message = "Could not find position for line = " + line + ", column = " + column;
s_log.error(message, new IllegalStateException(message));
}
return _workingString.length();
}
}
ix += column;
return ix - 1; // -1 because column starts with 1 put pos with 0
}
private int getNextLineStartIx(int begIx)
{
int buf;
int candidate1 = Integer.MAX_VALUE;
buf = _workingString.indexOf('\n', begIx);
if(0 <= buf)
{
candidate1 = buf + 1;
}
int candidate2 = Integer.MAX_VALUE;
buf = _workingString.indexOf('\r', begIx);
if(0 <= buf)
{
if(buf + 1 <_workingString.length() && '\n' == _workingString.charAt(buf+1))
{
candidate2 = buf + 2;
}
else
{
candidate2 = buf + 1;
}
}
int ret = Math.min(candidate1, candidate2);
return ret;
}
public void notifyParser(String sqlText)
{
synchronized(this)
{
_pendingString = sqlText;
this.notify();
}
}
public void exitThread()
{
_exitThread = true;
synchronized(this)
{
this.notify();
}
}
public void setParsingFinishedListener(ParsingFinishedListener parsingFinishedListener)
{
_parsingFinishedListener = parsingFinishedListener;
}
public void run()
{
try
{
while(true)
{
synchronized(this)
{
this.wait();
_workingString = _pendingString;
_workingBuffer = new IncrementalBuffer(new StringCharacterIterator(_workingString));
}
if(_exitThread)
{
break;
}
//////////////////////////////////////////////////////////////
// On Errors we restart the parser behind the error
_errorDetected = false;
runParser();
while(_errorDetected)
{
if(_workingString.length() > _nextStatBegin)
{
_workingString = _workingString.substring(_nextStatBegin, _workingString.length());
if("".equals(_workingString.trim()))
{
break;
}
}
else
{
break;
}
_lastParserRunOffset += _nextStatBegin;
_workingBuffer = new IncrementalBuffer(new StringCharacterIterator(_workingString));
_errorDetected = false;
runParser();
}
//
////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////
// We are through with parsing. Now we store the outcome
// in _lastRun... and tell the listeners.
_lastRunTableAliasInfos = _workingTableAliasInfos.toArray(new TableAliasInfo[_workingTableAliasInfos.size()]);
_lastRunErrorInfos = _workingErrorInfos.toArray(new ErrorInfo[_workingErrorInfos.size()]);
_workingTableAliasInfos.clear();
_workingErrorInfos.clear();
_lastParserRunOffset = 0;
if(null != _parsingFinishedListener)
{
_parsingFinishedListener.parsingFinished();
}
//
/////////////////////////////////////////////////////////////
if(_exitThread)
{
break;
}
}
}
catch(ExitParserThreadRequestException eptre)
{
}
catch (Exception e)
{
if(null != _parsingFinishedListener)
{
_parsingFinishedListener.parserExitedOnException(e);
}
e.printStackTrace();
}
}
private void runParser()
{
_errors.reset();
Scanner scanner = new Scanner(_workingBuffer, _errors);
Parser parser = new Parser(scanner, _schema);
parser.addParserListener(new ParserListener()
{
public void statementAdded(SQLStatement statement)
{
onStatementAdded(statement);
}
});
parser.addSQLSelectStatementListener(new SQLSelectStatementListener()
{
public void aliasDefined(String tableName, String aliasName)
{
onAliasDefined(tableName, aliasName);
}
});
parser.parse();
}
private void onStatementAdded(SQLStatement statement)
{
_curSQLSelectStat = statement;
}
private void onAliasDefined(String tableName, String aliasName)
{
_workingTableAliasInfos.add(new TableAliasInfo(aliasName, tableName, _curSQLSelectStat.getStart() + _lastParserRunOffset));
}
public TableAliasInfo[] getTableAliasInfos()
{
return _lastRunTableAliasInfos;
}
public ErrorInfo[] getErrorInfos()
{
return _lastRunErrorInfos;
}
/**
* terminate the parser
*/
public void end()
{
IncrementalBuffer oldBuffer = this._workingBuffer;
this._workingBuffer = null;
oldBuffer.eof();
}
/**
* accept the next character sequence to be parsed
* @param chars
*/
public void accept(CharacterIterator chars)
{
_workingBuffer.waitChars(); //wait for pending chars to be processed
_workingBuffer.accept(chars); //post new characters
}
/**
* This is a Scanner.Buffer implementation which blocks until character data is
* available. The {@link #read} method is invoked from the background parsing thread.
* The parsing thread can be terimated by calling the {@link #eof} method on this object
*/
private class IncrementalBuffer extends Scanner.Buffer
{
private CharacterIterator chars;
private char current;
private boolean atEnd;
IncrementalBuffer(CharacterIterator chars)
{
this.atEnd = false;
this.chars = chars;
this.current = chars != null ? chars.first() : CharacterIterator.DONE;
}
/**
* read the next character. This method is invoked from the parser thread
* @return the next available character
*/
protected synchronized char read()
{
if(_exitThread)
{
throw new ExitParserThreadRequestException();
}
if (atEnd)
{
return eof;
}
else
{
if (current == CharacterIterator.DONE)
{
if (chars != null)
{
synchronized (chars)
{
chars.notify(); //tell the UI that this _workingBuffer is through
}
}
// try
// {
// wait();
// }
// catch (InterruptedException e)
// {
// }
}
if (atEnd)
{
if(_exitThread)
{
throw new ExitParserThreadRequestException();
}
current = eof;
return eof;
}
else
{
char prev = current;
if(_exitThread)
{
throw new ExitParserThreadRequestException();
}
current = chars.next();
return prev;
}
}
}
synchronized void eof()
{
atEnd = true;
notify();
}
/**
* Post a character sequence to be read. Notify the parser thread accordingly. Invoking
* this method should always be followed by a call to {@link #waitChars} to ensure that
* the character sequence is not overwritten before it has been fully processed.
* @param chars the chracters to be read
*/
synchronized void accept(CharacterIterator chars)
{
this.chars = chars;
this.current = chars != null ? chars.first() : CharacterIterator.DONE;
notify();
}
/**
* block the current thread until all characters from the current iterator have
* been processed
*/
void waitChars()
{
if (chars != null && current != CharacterIterator.DONE)
{
synchronized (chars)
{
try
{
chars.wait();
}
catch (InterruptedException e)
{
}
}
}
}
int getBeginIndex()
{
return chars != null ? chars.getBeginIndex() : 0;
}
protected void setIndex(int position)
{
this.current = chars.setIndex(position);
}
}
/**
* error stream which simply saves the error codes and line info
* circularily in an array of fixed size, and notifies a listener
* if requested
*/
private static class Errors extends ErrorStream
{
private int[][] errorStore;
private int count;
private ErrorListener listener;
public Errors(ErrorListener listener)
{
this.listener = listener;
errorStore = new int[5][3];
}
protected void ParsErr(int n, int line, int col)
{
errorStore[count][0] = n;
errorStore[count][1] = line;
errorStore[count][2] = col;
count = (count + 1) % 5;
if (listener != null)
super.ParsErr(n, line, col);
}
protected void SemErr(int n, int line, int col)
{
errorStore[count][0] = n;
errorStore[count][1] = line;
errorStore[count][2] = col;
count = (count + 1) % 5;
if (listener != null)
{
switch (n)
{
case ParsingConstants.KW_MINUS:
//i18n[parserthread.undefinedTable=undefined table]
StoreError(n, line, col, s_stringMgr.getString("parserthread.undefinedTable"));
break;
default:
super.SemErr(n, line, col);
}
}
}
protected void StoreError(int n, int line, int col, String s)
{
if (listener != null)
listener.errorDetected(s, line, col);
}
public void reset()
{
errorStore = new int[5][3];
}
}
}