//
// Msql client library for JAVA
// Copyright (C) 1995 Darryl Collins <darryl@minmet.uq.oz.au>
// This software is provided "as is" without any expressed or implied warranty.
//
// This package implements a client for the mSQL server designed and
// implemented by David Hughes <bambi@hughes.com.au>
//
// $Log: Msql.java,v $
// Revision 1.0.1.1  1995/11/27 02:40:55  root
// Fixed bug with receipt of vesrion string.
//
// Revision 1.0.0.3  1995/11/09 00:57:54  root
// Updated to match the standard Msql API more closely.
//
// Revision 1.0.0.2  1995/11/06 06:45:02  root
// Fixed problem with processing null returns from Query
//
// Revision 1.0.0.1  1995/11/06 06:31:53  root
// Added comments
//
// Revision 1.0  1995/11/06 06:01:45  root
// Initial revision
//

import java.awt.*;
import java.lang.*;
import java.io.*;
import java.net.*;
import java.util.*;


/**
 * Exception class to be thrown on Msql client errors
 */
class MsqlException extends java.lang.Throwable {

	String strMessage;

	public MsqlException(String s) {
		strMessage = s;
	}

	public String getMessage() {
		return strMessage;
	}
}

/**
 * Msql Class - implements a connection to a remote server
 * and a JAVA variant of the Msql API as defined by David Hughes
 */
public class Msql {

	// Communication variables
	private Socket s;
	private DataInputStream in;
	private DataOutputStream out;

	// User name and version info
	private String userName;
	private String strVersion;

	// Temporary storage for reply packets
	private String strReply;

	// TCP port
	private static final int rootPort = 1112;
	private static final int mortalPort = 4333;
	private int msqlPort = rootPort;

	/**
	 *  Constructor
	 */
	public Msql() {
	}

	/**
	 *  Msql Constructor - establishes connection to named host
	 */
	public Msql(String msqlServer) throws MsqlException {

		if(msqlServer != null) 	
			Connect(msqlServer);
	}

	/**
	 *  Connect - establishes connection to named host
	 *
 	 * 	1. Open socket.
	 *	2. Establish IO Streams on socket.
	 *	3. Obtain version info.
	 *	4. Supply user name.
	 *	5. Retrieve status.
	 */
	public void Connect(String msqlServer) throws MsqlException {

		try {
			// Open connection 
			s = new Socket(msqlServer, msqlPort);

			// Get Streams
			in = new DataInputStream(s.getInputStream());
			out = new DataOutputStream(s.getOutputStream());

			// Read Version Info 
			strVersion = recvString();
			
			// Send Username
			sendString(userName = System.getProperty("user.name"));

			// Get reply
			strReply = recvString();

			if(!strReply.startsWith("-100:",0))
				throw new MsqlException("Msql connection failed");

		} catch(IOException e) {
			throw new MsqlException("IO Exception during connection");
		}
	}

	/**
	 *  Connect using a different port.
	 */
	public void Connect(String msqlServer, boolean rootServer) throws MsqlException {

		if(rootServer)
			msqlPort = rootPort;
		else
			msqlPort = mortalPort;

		Connect(msqlServer);
	}

	/**
	 *  Sends a String as an mSQL packet.  The trailing linefeed
	 *  is added automatically by the routine.
	 */
	private void sendString(String s) throws IOException {
		
		int i,j,k;

		j = k = s.length()+1;

		out.writeByte(k&0xff); k >>= 8;
		out.writeByte(k&0xff); k >>= 8;
		out.writeByte(k&0xff); k >>= 8;
		out.writeByte(k&0xff);
	
		for(i=0; i<j-1; i++) {
			byte b = (byte)s.charAt(i);
			out.writeByte(b);
		}
	
		out.writeByte((byte)'\n');
	}

	/**
	 *  Receives a packet and converts it to a String
	 *  (trailing linefeed is dropped)
	 */
	private String recvString() throws IOException {

		StringBuffer s = new StringBuffer();

		int i,j,k=0;

		j = in.readByte(); k += (j&0xff);
		j = in.readByte(); k += (j&0xff)<<8;
		j = in.readByte(); k += (j&0xff)<<16;
		j = in.readByte(); k += (j&0xff)<<24;

		for(i=0; i<k; i++) {
			char c = (char)in.readByte();
			if(i < k-1) s.append(c);
		}

		return s.toString();
	}

	/**
	 *  Close - close the connection to the server
	 */
	public void Close() throws MsqlException {

		try {
			sendString("1");
			s.close();
		} catch(IOException e) {
			throw new MsqlException("QUIT: "+e.getMessage());
		}
	}

	/**
	 *  SelectDB - Select the database to work with
	 */
	public void SelectDB(String db) throws MsqlException {

		try {
			sendString("2 "+db+"\n");
			strReply = recvString();
			if(strReply.startsWith("-1:",0))
				throw new MsqlException("DB_INIT: "+strReply.substring(3));

		} catch(IOException e) {
			throw new MsqlException("DB_INIT: "+e.getMessage());
		}
	}

	/**
	 *  Send a Query to the server.  Returns an instance of MsqlResult 
	 *  which contains both the selected rows and field info returned
	 *  by the server.
	 */
	public MsqlResult Query(String s) throws MsqlException {

		int m,n;
		MsqlData table, fields;
		MsqlResult results;

		try {
			// Send the command
			sendString("3 "+s+"\n");
			
			// Retrieve the reply
			strReply = recvString();

			// Check for errors
			if(!strReply.startsWith("1:"))
				throw new MsqlException("QUERY: "+strReply.substring(2));
			// Determine number of fields
			m = strReply.indexOf(':',2);
			if(m == -1)
				return null;

			try {
				n=Integer.parseInt(strReply.substring(2,m));
			} catch(NumberFormatException e) {
				throw new MsqlException("Invalid format for number fo fields in reply");
			}

			// Receive the row data
			table = recvData(n);

			// Receive the field data
			fields = recvData(5);

			results = new MsqlResult(table, fields);
		} catch(IOException e) {
			throw new MsqlException("QUERY: "+e.getMessage());
		}

		return results;
	}

	/**
	 *  List Databases available on server.  Returns an instance
	 *  of MsqlData containing a list of databases.
	 */
	public MsqlData ListDBs() throws MsqlException {

		MsqlData result;

		try {
			sendString("4\n");
			result = recvData(1);
		} catch(IOException e) {
			throw new MsqlException("DB_LIST: "+e.getMessage());
		}

		return result;
	}

	/**
	 *  List tables in the selected database.  Returns an instance of
	 *  MsqlData containing list of tables.
	 */
	public MsqlData ListTables() throws MsqlException {

		MsqlData result;

		try {
			sendString("5\n");
			result = recvData(1);
		} catch(IOException e) {
			throw new MsqlException("TABLE_LIST: "+e.getMessage());
		}

		return result;
	}

	/**
	 *  List Fields in a given table.  Returns an instance of Msqldata
	 *  containing field information.
	 */
	public MsqlData ListFields(String s) throws MsqlException {

		MsqlData result;

		try {
			sendString("6 "+s+"\n");
			result = recvData(5);
		} catch(IOException e) {
			throw new MsqlException("FIELD_LIST: "+e.getMessage());
		}

		return result;
	}

	/**
	 *  Create a new database.  Included for completeness only since
	 *  command can only be executed if issued from UNIX domain socket.
	 */
	public void CreateDB(String s) throws MsqlException {

		try {
			sendString("7 "+s+"\n");
			strReply = recvString();
			if(strReply.startsWith("-1:",0))
				throw new MsqlException("CREATE_DB: "+strReply.substring(3));

		} catch(IOException e) {
			throw new MsqlException("CREATE_DB: "+e.getMessage());
		}
	}

	/**
	 *  Drop a  database.  Included for completeness only since
	 *  command can only be executed if issued from UNIX domain socket.
	 */
	public void DropDB(String s) throws MsqlException {

		try {
			sendString("8 "+s+"\n");
			strReply = recvString();
			if(strReply.startsWith("-1:",0))
				throw new MsqlException("DROP_DB: "+strReply.substring(3));

		} catch(IOException e) {
			throw new MsqlException("DROP_DB: "+e.getMessage());
		}
	}

	/**
	 *  Reload access control list.  Included for completeness only since
	 *  command can only be executed if issued from UNIX domain socket.
	 */
	public void ReloadACL(String s) throws MsqlException {

		try {
			sendString("9\n");
			strReply = recvString();
			if(strReply.startsWith("-1:",0))
				throw new MsqlException("RELOAD_ACL: "+strReply.substring(3));

		} catch(IOException e) {
			throw new MsqlException("RELOAD_ACL: "+e.getMessage());
		}
	}

	/**
	 *  Shutdown server.  Included for completeness only since
	 *  command can only be executed if issued from UNIX domain socket.
	 */
	public void Shutdown() throws MsqlException {

		try {
			sendString("10\n");
			strReply = recvString();
			if(strReply.startsWith("-1:",0))
				throw new MsqlException("SHUTDOWN: "+strReply.substring(3));

		} catch(IOException e) {
			throw new MsqlException("SHUTDOWN: "+e.getMessage());
		}
	}

	/**
	 *  Retrieve the results of a query.  Returns an instance of MsqlData.
	 *  Retrieves pakcets from server until an error status (-1:) or
	 *  success status (-100:) is received.
	 */
	private MsqlData recvData(int n) throws MsqlException {

		MsqlData result = new MsqlData(n);

		boolean finished = false;

		while(!finished) {
			try {
				strReply = recvString();
			} catch(IOException e) {
				throw new MsqlException(e.getMessage());
			}

			if(strReply.startsWith("-1:",0))
				throw new MsqlException(strReply.substring(3));

			if(strReply.startsWith("-100:",0))
				finished = true;
			else
				result.addRow(strReply);
		}

		return result;
	}
}

/**
 *  Object which contains a set of results returned from the server.
 */
class MsqlData {

	// Maximum number of rows it can cope with
	static final int maxRows = 4096;

	
	// Number of rows and columns in table
	private int nRows=0, nCols=0;

	// Data storage
	private MsqlRow strData[];

	// Cursor for data access
	private int nCursor=0; 

	/**
 	 *  Constructor - accepts number of fields as argument
	 */
	public MsqlData(int n) {
		nCols = n;
		strData = new MsqlRow[maxRows];
	}

	/**
	 *  Constructor - copies an existing object
	 */
	public MsqlData(MsqlData d) {

		nRows = d.nRows;
		nCols = d.nCols;

		strData = new MsqlRow[maxRows];

		for(int i=0; i<nRows; i++)
			strData[i] = new MsqlRow(d.strData[i]);
	}

	/** 
	 *  Add a new row to the table.  Accepts a raw string from the 
	 *  server as an argument and parses it to identify column contents
	 */
	public void addRow(String s) {

		int i,j,k,l;

		if(nRows >= maxRows)
			return;

		strData[nRows] = new MsqlRow(nCols);

		for(i=j=0; i<nCols; i++) {
			k = s.indexOf(':',j);
			l = Integer.parseInt(s.substring(j,k));

			strData[nRows].addField(i, s.substring(k+1,k+1+l));

			j = k+l+1;
		}
	
		nRows++;
	}

	/**
	 *  Return a specific row from the table
	 */
	public String [] getRow(int i) {
		
		return strData[i].rowData();
	}

	/**
	 *  Fetch the next row from the table
	 */
	public String [] FetchRow() {

		if(nCursor < nRows)
			return strData[nCursor++].rowData();
		
		return null;
	}

	/**
	 *  Data Seek
	 */
	public void DataSeek(int i) {

		if(i < nRows)
			nCursor = i;
	}

	/**
	 *  Return the number of fields in the table
	 */
	public int NumFields() {
		
		return nCols;
	}

	/**
	 *  return the number of rows in the table
	 */
	public int NumRows() {

		return nRows;
	}
}

/**
 *  Object containing the results of a select query.
 */
class MsqlResult {

	private MsqlData rowData;
	private MsqlData fieldData;

	/**
	 *  Constructor - Accepts row and field info as arguments
	 */
	public MsqlResult(MsqlData table, MsqlData fields) {

		rowData = new MsqlData(table);
		fieldData = new MsqlData(fields);
	}

	/**
	 *  Fetch the next row from the table
	 */
	public String [] FetchRow() {

		return rowData.FetchRow();
	}

	/**
	 *  Fetch the next field from the list of fields
	 */
	public String [] FetchField() {

		return fieldData.FetchRow();
	}

	/**
	 *  Return number of rows in table
	 */
	public int NumRows() {
		return rowData.NumRows();
	}

	/** 	
	 *  Return number of fields in table
	 */
	public int NumFields() {
		return fieldData.NumRows();
	}

	/**
	 *  Data Seek
	 */
	public void DataSeek(int i) {

		rowData.DataSeek(i);
	}

	/**
	 *  Field Seek
	 */
	public void FieldSeek(int i) {

		fieldData.DataSeek(i);
	}
}


/**
 *  Storage for a single row of a table
 */
class MsqlRow {

	private int nCols;
	private String strData[];

	/**
	 *  Constructor - accepts number of cols as argument
	 */
	public MsqlRow(int n) {

		nCols = n;
		strData = new String[nCols];
	}

	/**
	 *  Constructor - duplicates another row
	 */
	public MsqlRow(MsqlRow r) {
		
		nCols = r.nCols;
		strData = new String[nCols];
	
		for(int i=0; i<nCols; i++)
			strData[i] = new String(r.strData[i]);
	}

	/**
	 *  Returns contents as array of strings 
	 */
	public String [] rowData() {

		return strData;
	}

	/**
	 *  Adds a new field to the row
	 */
	public void addField(int i, String s) {

		strData[i] = new String(s);
	}

	/**
	 *  Returns number of cols
	 */
	public int NumFields() {
		
		return nCols;
	}
}
