/*

   Derby - Class org.apache.derby.impl.tools.ij.utilMain

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */

package org.apache.derby.impl.tools.ij;
                
import org.apache.derby.tools.JDBCDisplayUtil;
import org.apache.derby.iapi.tools.i18n.*;

import org.apache.derby.shared.common.info.ProductVersionHolder;
import org.apache.derby.shared.common.info.ProductGenusNames;

import java.util.List;
import java.util.Stack;
import java.util.Hashtable;

import java.io.InputStream;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.StringReader;
import java.sql.DriverManager;
import java.sql.Driver;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.PreparedStatement;

/**
	This class is utilities specific to the two ij Main's.
	This factoring enables sharing the functionality for
	single and dual connection ij runs.

 */
public class utilMain {

	private StatementFinder[] commandGrabber;
	UCode_CharStream charStream;
	ijTokenManager ijTokMgr;
	ij ijParser;
	ConnectionEnv[] connEnv;
	private int currCE;
	private final int		numConnections;
	private boolean fileInput;
	private boolean initialFileInput;
	private boolean mtUse;
	private boolean firstRun = true;
	private LocalizedOutput out = null;
	private Hashtable ignoreErrors;
	/**
	 * True if to display the error code when
	 * displaying a SQLException.
	 */
	private final boolean showErrorCode;
    
    /**
     * Value of the system property ij.execptionTrace
     */
    private final String ijExceptionTrace;

	/*
		In the goodness of time, this could be an ij property
	 */
	public static final int BUFFEREDFILESIZE = 2048;

	/*
	 * command can be redirected, so we stack up command
	 * grabbers as needed.
	 */
	Stack<StatementFinder> oldGrabbers = new Stack<StatementFinder>();

	LocalizedResource langUtil = LocalizedResource.getInstance();
	/**
	 * Set up the test to run with 'numConnections' connections/users.
	 *
	 * @param numConnections	The number of connections/users to test.
	 */
	utilMain(int numConnections, LocalizedOutput out)
		throws ijFatalException
	{
		this(numConnections, out, (Hashtable)null);
	}

    /**
	 * Set up the test to run with 'numConnections' connections/users.
     * This overload allows the choice of whether the system properties
     * will be used or not.
	 *
	 * @param numConnections	The number of connections/users to test.
	 */
    utilMain(int numConnections, LocalizedOutput out, boolean loadSystemProperties)
		throws ijFatalException
	{
		this(numConnections, out, (Hashtable)null);
        if (loadSystemProperties) {
            initFromEnvironment();
        }
	}

	/**
	 * Set up the test to run with 'numConnections' connections/users.
	 *
	 * @param numConnections	The number of connections/users to test.
	 * @param ignoreErrors		A list of errors to ignore.  If null,
	 *							all errors are printed out and nothing
	 *							is fatal.  If non-null, if an error is
	 *							hit and it is in this list, it is silently	
	 *							ignore.  Otherwise, an ijFatalException is
	 *							thrown.  ignoreErrors is used for stress
	 *							tests.
	 */
	public utilMain(int numConnections, LocalizedOutput out, Hashtable ignoreErrors)
		throws ijFatalException
	{
		/* init the parser; give it no input to start with.
		 * (1 parser for entire test.)
		 */
		charStream = new UCode_CharStream(
						new StringReader(" "), 1, 1);
		ijTokMgr = new ijTokenManager(charStream);
		ijParser = new ij(ijTokMgr, this);
		this.out = out;
		if ( ignoreErrors != null ) { this.ignoreErrors = (Hashtable) ignoreErrors.clone(); }
		
		showErrorCode = 
			Boolean.valueOf(
					util.getSystemProperty("ij.showErrorCode")
					).booleanValue();
        
        ijExceptionTrace = util.getSystemProperty("ij.exceptionTrace");

		this.numConnections = numConnections;
		/* 1 StatementFinder and ConnectionEnv per connection/user. */
		commandGrabber = new StatementFinder[numConnections];
		connEnv = new ConnectionEnv[numConnections];

		for (int ictr = 0; ictr < numConnections; ictr++)
		{
		    commandGrabber[ictr] = new StatementFinder(langUtil.getNewInput(System.in), out);
			connEnv[ictr] = new ConnectionEnv(ictr, (numConnections > 1), (numConnections == 1));
		}

		/* Start with connection/user 0 */
		currCE = 0;
		fileInput = false;
		initialFileInput = false;
		firstRun = true;
	}
	
	/**
	 * Initialize the connections from the environment.
	 *
	 */
	public void initFromEnvironment()
	{
		ijParser.initFromEnvironment();
		
		for (int ictr = 0; ictr < numConnections; ictr++)
		{
			try {
				connEnv[ictr].init(out);
			} catch (SQLException s) {
				JDBCDisplayUtil.ShowException(out, s); // will continue past connect failure
			} catch (ClassNotFoundException c) {
				JDBCDisplayUtil.ShowException(out, c); // will continue past driver failure
			} catch (InstantiationException i) {
				JDBCDisplayUtil.ShowException(out, i); // will continue past driver failure
			} catch (IllegalAccessException ia) {
				JDBCDisplayUtil.ShowException(out, ia); // will continue past driver failure
			} catch (NoSuchMethodException ia) {
				JDBCDisplayUtil.ShowException(out, ia); // will continue past driver failure
			} catch (java.lang.reflect.InvocationTargetException ia) {
				JDBCDisplayUtil.ShowException(out, ia); // will continue past driver failure
            } catch (ijException ie) {
                JDBCDisplayUtil.ShowException(out, ie); // will continue past driver failure
            }
		}
	}


	/**
	 * run ij over the specified input, sending output to the
	 * specified output. Any prior input and output will be lost.
	 *
	 * @param in source for input to ij
	 * @param out sink for output from ij
	 */
	public void go(LocalizedInput[] in, LocalizedOutput out)
				   throws ijFatalException
	{
		this.out = out;
		
		ijParser.setConnection(connEnv[currCE], (numConnections > 1));
		fileInput = initialFileInput = (!in[currCE].isStandardInput());

		for (int ictr = 0; ictr < commandGrabber.length; ictr++) {
			commandGrabber[ictr].ReInit(in[ictr]);
		}

		if (firstRun) {

			// figure out which version this is
            InputStream versionStream = util.getResourceAsStream(
                    "/" + ProductGenusNames.TOOLS_INFO);

			// figure out which version this is
			ProductVersionHolder ijVersion = 
				ProductVersionHolder.getProductVersionHolderFromMyEnv(versionStream);

			String version;
			if (ijVersion != null)
			{
				version = "" + ijVersion.getMajorVersion() + "." +
					ijVersion.getMinorVersion();
			}
			else
			{
				version = "?";
			}

   			out.println(langUtil.getTextMessage("IJ_IjVers30C199", version));
			for (int i=connEnv.length-1;i>=0;i--) { // print out any initial warnings...
				Connection c = connEnv[i].getConnection();
				if (c!=null) {
					JDBCDisplayUtil.ShowWarnings(out,c);
				}
			}
			firstRun = false;

			supportIJProperties(connEnv[currCE]);
    	}
		this.out = out;
		runScriptGuts();
		cleanupGo(in);
	}
	
	/**
	 * Support to run a script. Performs minimal setup
	 * to set the passed in connection into the existing
	 * ij setup, ConnectionEnv.
	 * @param conn
	 * @param in
	 */
	public int goScript(Connection conn,
			LocalizedInput in)
	{
	    connEnv[0].addSession(conn, (String) null);
        ijParser.setConnection(connEnv[0], (numConnections > 1));
	    supportIJProperties(connEnv[0]);   
	    		
		fileInput = initialFileInput = !in.isStandardInput();
		commandGrabber[0].ReInit(in);
		return runScriptGuts();
	}
	
	private void supportIJProperties(ConnectionEnv env) {
	  //check if the property is set to not show select count and set the static variable
        //accordingly. 
        boolean showNoCountForSelect = Boolean.valueOf(util.getSystemProperty("ij.showNoCountForSelect")).booleanValue();
        JDBCDisplayUtil.setShowSelectCount( !showNoCountForSelect );

        //check if the property is set to not show initial connections and accordingly set the
        //static variable.
        boolean showNoConnectionsAtStart = Boolean.valueOf(util.getSystemProperty("ij.showNoConnectionsAtStart")).booleanValue();

        if (!(showNoConnectionsAtStart)) {
            try {
                ijResult result = ijParser.showConnectionsMethod(true);
                displayResult(out,result,env.getConnection());
            } catch (SQLException ex) {
                handleSQLException(out,ex);
            }
        }        
    }

    /**
	 * Run the guts of the script. Split out to allow
	 * calling from the full ij and the minimal goScript.
     * @return The number of errors seen in the script.
	 *
	 */
	private int runScriptGuts() {

        int scriptErrorCount = 0;
		
		boolean done = false;
		String command = null;
		while (!ijParser.exit && !done) {
			try{
				ijParser.setConnection(connEnv[currCE], (numConnections > 1));
			} catch(Throwable t){
				//do nothing
				}

			connEnv[currCE].doPrompt(true, out);
   			try {
   				command = null;
				out.flush();
				command = commandGrabber[currCE].nextStatement();

				// if there is no next statement,
				// pop back to the top saved grabber.
				while (command == null && ! oldGrabbers.empty()) {
					// close the old input file if not System.in
					if (fileInput) commandGrabber[currCE].close();
					commandGrabber[currCE] = oldGrabbers.pop();
					if (oldGrabbers.empty())
						fileInput = initialFileInput;
					command = commandGrabber[currCE].nextStatement();
				}

				// if there are no grabbers left,
				// we are done.
				if (command == null && oldGrabbers.empty()) {
					done = true;
				}
				else {
					boolean	elapsedTimeOn = ijParser.getElapsedTimeState();
					long	beginTime = 0;
					long	endTime;

					if (fileInput) {
						out.println(command+";");
						out.flush();
					}

					charStream.ReInit(new StringReader(command), 1, 1);
					ijTokMgr.ReInit(charStream);
					ijParser.ReInit(ijTokMgr);

					if (elapsedTimeOn) {
						beginTime = System.currentTimeMillis();
					}

					ijResult result = ijParser.ijStatement();
					displayResult(out,result,connEnv[currCE].getConnection());

					// if something went wrong, an SQLException or ijException was thrown.
					// we can keep going to the next statement on those (see catches below).
					// ijParseException means we try the SQL parser.

					/* Print the elapsed time if appropriate */
					if (elapsedTimeOn) {
						endTime = System.currentTimeMillis();
						out.println(langUtil.getTextMessage("IJ_ElapTime0Mil", 
						langUtil.getNumberAsString(endTime - beginTime)));
					}

					// would like when it completes a statement
					// to see if there is stuff after the ;
					// and before the <EOL> that we will IGNORE
					// (with a warning to that effect)
				}

    			} catch (ParseException e) {
 					if (command != null)
                        scriptErrorCount += doCatch(command) ? 0 : 1;
				} catch (TokenMgrError e) {
 					if (command != null)
                        scriptErrorCount += doCatch(command) ? 0 : 1;
    			} catch (SQLException e) {
                    scriptErrorCount++;
					// SQL exception occurred in ij's actions; print and continue
					// unless it is considered fatal.
					handleSQLException(out,e);
    			} catch (ijException e) {
                    scriptErrorCount++;
					// exception occurred in ij's actions; print and continue
    			  	out.println(langUtil.getTextMessage("IJ_IjErro0",e.getMessage()));
					doTrace(e);
    			} catch (Throwable e) {
                    scriptErrorCount++;
    			  	out.println(langUtil.getTextMessage("IJ_JavaErro0",e.toString()));
                    e.printStackTrace();
					doTrace(e);
				}

			/* Go to the next connection/user, if there is one */
			currCE = ++currCE % connEnv.length;
		}
        
        return scriptErrorCount;
	}
	
	/**
	 * Perform cleanup after a script has been run.
	 * Close the input streams if required and shutdown
	 * derby on an exit.
	 * @param in
	 */
	private void cleanupGo(LocalizedInput[] in) {

		// we need to close all sessions when done; otherwise we have
		// a problem when a single VM runs successive IJ threads
		try {
			for (int i = 0; i < connEnv.length; i++) {
				connEnv[i].removeAllSessions();
			}
		} catch (SQLException se ) {
			handleSQLException(out,se);
		}
		// similarly must close input files
		for (int i = 0; i < numConnections; i++) {
			try {
				in[i].close();	
			} catch (Exception e ) {
    			  	out.println(langUtil.getTextMessage("IJ_CannotCloseInFile",
					e.toString()));
			}
		}

		/*
			If an exit was requested, then we will be shutting down.
		 */
		if (ijParser.exit || (initialFileInput && !mtUse)) {
			Driver d = null;
			try {
			    d = DriverManager.getDriver("jdbc:derby:");
			} catch (Throwable e) {
				d = null;
			}
			if (d!=null) { // do we have a driver running? shutdown on exit.
				try {
					DriverManager.getConnection("jdbc:derby:;shutdown=true");
				} catch (SQLException e) {
					// ignore the errors, they are expected.
				}
			}
		}
  	}

	private void displayResult(LocalizedOutput out, ijResult result, Connection conn) throws SQLException {
		// display the result, if appropriate.
		if (result!=null) {
			if (result.isConnection()) {
				if (result.hasWarnings()) {
					JDBCDisplayUtil.ShowWarnings(out,result.getSQLWarnings());
					result.clearSQLWarnings();
				}
			} else if (result.isStatement()) {
				Statement s = result.getStatement();
				try {
				    JDBCDisplayUtil.DisplayResults(out,s,connEnv[currCE].getConnection());
				} catch (SQLException se) {
				    result.closeStatement();
					throw se;
				}
				result.closeStatement();
			} else if (result.isNextRowOfResultSet()) {
				ResultSet r = result.getNextRowOfResultSet();
				JDBCDisplayUtil.DisplayCurrentRow(out,r,connEnv[currCE].getConnection());
			} else if (result.isVector()) {
				util.DisplayVector(out,result.getVector());
				if (result.hasWarnings()) {
					JDBCDisplayUtil.ShowWarnings(out,result.getSQLWarnings());
					result.clearSQLWarnings();
				}
			} else if (result.isMulti()) {
			    try {
				    util.DisplayMulti(out,(PreparedStatement)result.getStatement(),result.getResultSet(),connEnv[currCE].getConnection());
				} catch (SQLException se) {
				    result.closeStatement();
					throw se;
				}
				result.closeStatement(); // done with the statement now
				if (result.hasWarnings()) {
					JDBCDisplayUtil.ShowWarnings(out,result.getSQLWarnings());
					result.clearSQLWarnings();
				}
			} else if (result.isResultSet()) {
				ResultSet rs = result.getResultSet();
				try {
					JDBCDisplayUtil.DisplayResults(out,rs,connEnv[currCE].getConnection(), result.getColumnDisplayList(), result.getColumnWidthList());
				} catch (SQLException se) {
					result.closeStatement();
					throw se;
				}
				result.closeStatement();
            } else if (result.isMultipleResultSetResult()) {
              List<ResultSet> resultSets = result.getMultipleResultSets();
              try {
                JDBCDisplayUtil.DisplayMultipleResults(out,resultSets,
                                     connEnv[currCE].getConnection(),
                                     result.getColumnDisplayList(),
                                     result.getColumnWidthList());
              } catch (SQLException se) {
                result.closeStatement();
                throw se;
              }
			} else if (result.isException()) {
				JDBCDisplayUtil.ShowException(out,result.getException());
			}
		}
	}

	/**
	 * catch processing on failed commands. This really ought to
	 * be in ij somehow, but it was easier to catch in Main.
	 */
	private boolean doCatch(String command) {
		// this retries the failed statement
		// as a JSQL statement; it uses the
		// ijParser since that maintains our
		// connection and state.

        
	    try {
			boolean	elapsedTimeOn = ijParser.getElapsedTimeState();
			long	beginTime = 0;
			long	endTime;

			if (elapsedTimeOn) {
				beginTime = System.currentTimeMillis();
			}

			ijResult result = ijParser.executeImmediate(command);
			displayResult(out,result,connEnv[currCE].getConnection());

			/* Print the elapsed time if appropriate */
			if (elapsedTimeOn) {
				endTime = System.currentTimeMillis();
				out.println(langUtil.getTextMessage("IJ_ElapTime0Mil_4", 
				langUtil.getNumberAsString(endTime - beginTime)));
			}
            return true;

	    } catch (SQLException e) {
			// SQL exception occurred in ij's actions; print and continue
			// unless it is considered fatal.
			handleSQLException(out,e);
	    } catch (ijException i) {
	  		out.println(langUtil.getTextMessage("IJ_IjErro0_5", i.getMessage()));
			doTrace(i);
		} catch (ijTokenException ie) {
	  		out.println(langUtil.getTextMessage("IJ_IjErro0_6", ie.getMessage()));
			doTrace(ie);
	    } catch (Throwable t) {
	  		out.println(langUtil.getTextMessage("IJ_JavaErro0_7", t.toString()));
            t.printStackTrace();
			doTrace(t);
	    }
        return false;
	}

	/**
	 * This routine displays SQL exceptions and decides whether they
	 * are fatal or not, based on the ignoreErrors field. If they
	 * are fatal, an ijFatalException is thrown.
	 * Lifted from ij/util.java:ShowSQLException
	 */
	private void handleSQLException(LocalizedOutput out, SQLException e) 
		throws ijFatalException
	{
		String errorCode;
		String sqlState = null;
		SQLException fatalException = null;

		if (showErrorCode) {
			errorCode = langUtil.getTextMessage("IJ_Erro0", 
			langUtil.getNumberAsString(e.getErrorCode()));
		}
		else {
			errorCode = "";
		}

		boolean syntaxErrorOccurred = false;
		for (; e!=null; e=e.getNextException())
		{
			sqlState = e.getSQLState();
			if ("42X01".equals(sqlState))
				syntaxErrorOccurred = true;
			/*
			** If we are to throw errors, then throw the exceptions
			** that aren't in the ignoreErrors list.  If
			** the ignoreErrors list is null we don't throw
			** any errors.
			*/
		 	if (ignoreErrors != null) 
			{
				if ((sqlState != null) &&
					(ignoreErrors.get(sqlState) != null))
				{
					continue;
				}
				else
				{
					fatalException = e;
				}
			}

			String st1 = JDBCDisplayUtil.mapNull(e.getSQLState(),langUtil.getTextMessage("IJ_NoSqls"));
			String st2 = JDBCDisplayUtil.mapNull(e.getMessage(),langUtil.getTextMessage("IJ_NoMess"));
			out.println(langUtil.getTextMessage("IJ_Erro012",  st1, st2, errorCode));
			doTrace(e);
		}
		if (fatalException != null)
		{
			throw new ijFatalException(fatalException);
		}
		if (syntaxErrorOccurred)
			out.println(langUtil.getTextMessage("IJ_SuggestHelp"));
	}

	/**
	 * stack trace dumper
	 */
	private void doTrace(Throwable t) {
		if (ijExceptionTrace != null) {
			t.printStackTrace(out);
		}
		out.flush();
	}

	void newInput(String fileName) {
		FileInputStream newFile = null;
		try {
			newFile = new FileInputStream(fileName);
      	} catch (FileNotFoundException e) {
        	throw ijException.fileNotFound();
		}
		if (newFile == null) return;

		// if the file was opened, move to use it for input.
		oldGrabbers.push(commandGrabber[currCE]);
	    commandGrabber[currCE] = 
                new StatementFinder(langUtil.getNewInput(new BufferedInputStream(newFile, BUFFEREDFILESIZE)), null);
		fileInput = true;
	}

	void newResourceInput(String resourceName) {
		InputStream is = util.getResourceAsStream(resourceName);
		if (is==null) throw ijException.resourceNotFound();
		oldGrabbers.push(commandGrabber[currCE]);
	    commandGrabber[currCE] = 
                new StatementFinder(langUtil.getNewEncodedInput(new BufferedInputStream(is, BUFFEREDFILESIZE), "UTF8"), null);
		fileInput = true;
	}

	/**
	 * REMIND: eventually this might be part of StatementFinder,
	 * used at each carriage return to show that it is still "live"
	 * when it is reading multi-line input.
	 */
	static void doPrompt(boolean newStatement, LocalizedOutput out, String tag) 
	 {
		if (newStatement) {
	  		out.print("ij"+(tag==null?"":tag)+"> ");
		}
		else {
			out.print("> ");
		}
		out.flush();
	}

	void setMtUse(boolean b) {
		mtUse = b;
	}

    /**
     * Check that the cursor is scrollable.
     *
     * @param rs the ResultSet to check
     * @param operation which operation this is checked for
     * @exception ijException if the cursor isn't scrollable
     * @exception SQLException if a database error occurs
     */
    private void checkScrollableCursor(ResultSet rs, String operation)
            throws ijException, SQLException {
        if (rs.getType() == ResultSet.TYPE_FORWARD_ONLY) {
            throw ijException.forwardOnlyCursor(operation);
        }
    }

	/**
	 * Position on the specified row of the specified ResultSet.
	 *
	 * @param rs	The specified ResultSet.
	 * @param row	The row # to move to.
	 *				(Negative means from the end of the result set.)
	 *
	 * @return	NULL.
	 *
	 * @exception	SQLException thrown on error.
	 *				(absolute() not supported pre-JDBC2.0)
	 */
	ijResult absolute(ResultSet rs, int row)
		throws SQLException
	{
        checkScrollableCursor(rs, "ABSOLUTE");
		// 0 is an *VALID* value for row
		return new ijRowResult(rs, rs.absolute(row));
	}

	/**
	 * Move the cursor position by the specified amount.
	 *
	 * @param rs	The specified ResultSet.
	 * @param row	The # of rows to move.
	 *				(Negative means toward the beginning of the result set.)
	 *
	 * @return	NULL.
	 *
	 * @exception	SQLException thrown on error.
	 *				(relative() not supported pre-JDBC2.0)
	 */
	ijResult relative(ResultSet rs, int row)
		throws SQLException
	{
        checkScrollableCursor(rs, "RELATIVE");
		return new ijRowResult(rs, rs.relative(row));
	}

	/**
	 * Position before the first row of the specified ResultSet
	 * and return NULL to the user.
	 *
	 * @param rs	The specified ResultSet.
	 *
	 * @return	NULL.
	 *
	 * @exception	SQLException thrown on error.
	 *				(beforeFirst() not supported pre-JDBC2.0)
	 */
	ijResult beforeFirst(ResultSet rs)
		throws SQLException
	{
        checkScrollableCursor(rs, "BEFORE FIRST");
		rs.beforeFirst();
		return new ijRowResult(rs, false);
	}

	/**
	 * Position on the first row of the specified ResultSet
	 * and return that row to the user.
	 *
	 * @param rs	The specified ResultSet.
	 *
	 * @return	The first row of the ResultSet.
	 *
	 * @exception	SQLException thrown on error.
	 *				(first() not supported pre-JDBC2.0)
	 */
	ijResult first(ResultSet rs)
		throws SQLException
	{
        checkScrollableCursor(rs, "FIRST");
		return new ijRowResult(rs, rs.first());
	}

	/**
	 * Position after the last row of the specified ResultSet
	 * and return NULL to the user.
	 *
	 * @param rs	The specified ResultSet.
	 *
	 * @return	NULL.
	 *
	 * @exception	SQLException thrown on error.
	 *				(afterLast() not supported pre-JDBC2.0)
	 */
	ijResult afterLast(ResultSet rs)
		throws SQLException
	{
        checkScrollableCursor(rs, "AFTER LAST");
		rs.afterLast();
		return new ijRowResult(rs, false);
	}

	/**
	 * Position on the last row of the specified ResultSet
	 * and return that row to the user.
	 *
	 * @param rs	The specified ResultSet.
	 *
	 * @return	The last row of the ResultSet.
	 *
	 * @exception	SQLException thrown on error.
	 *				(last() not supported pre-JDBC2.0)
	 */
	ijResult last(ResultSet rs)
		throws SQLException
	{
        checkScrollableCursor(rs, "LAST");
		return new ijRowResult(rs, rs.last());
	}

	/**
	 * Position on the previous row of the specified ResultSet
	 * and return that row to the user.
	 *
	 * @param rs	The specified ResultSet.
	 *
	 * @return	The previous row of the ResultSet.
	 *
	 * @exception	SQLException thrown on error.
	 *				(previous() not supported pre-JDBC2.0)
	 */
	ijResult previous(ResultSet rs)
		throws SQLException
	{
        checkScrollableCursor(rs, "PREVIOUS");
		return new ijRowResult(rs, rs.previous());
	}

	/**
	 * Get the current row number
	 *
	 * @param rs	The specified ResultSet.
	 *
	 * @return	The current row number
	 *
	 * @exception	SQLException thrown on error.
	 *				(getRow() not supported pre-JDBC2.0)
	 */
	int getCurrentRowNumber(ResultSet rs)
		throws SQLException
	{
        checkScrollableCursor(rs, "GETCURRENTROWNUMBER");
		return rs.getRow();
	}
}
