package postgresql;

import java.io.*;
import java.net.*;
import java.sql.*;
import java.util.*;
import postgresql.Field;
import postgresql.fastpath.*;
import postgresql.largeobject.*;
import postgresql.util.*;

/**
 * $Id: Connection.java,v 1.17 1999/05/18 23:17:15 peter Exp $
 *
 * This abstract class is used by postgresql.Driver to open either the JDBC1 or
 * JDBC2 versions of the Connection class.
 *
 */
public abstract class Connection
{
  // This is the network stream associated with this connection
  public PG_Stream pg_stream;
  
  // This is set by postgresql.Statement.setMaxRows()
  public int maxrows = 0;		// maximum no. of rows; 0 = unlimited
  
  private String PG_HOST;
  private int PG_PORT;
  private String PG_USER;
  private String PG_PASSWORD;
  private String PG_DATABASE;
  private boolean PG_STATUS;
  
  public boolean CONNECTION_OK = true;
  public boolean CONNECTION_BAD = false;
  
  public boolean autoCommit = true;
  public boolean readOnly = false;
  
  public Driver this_driver;
  private String this_url;
  private String cursor = null;	// The positioned update cursor name
  
  // These are new for v6.3, they determine the current protocol versions
  // supported by this version of the driver. They are defined in
  // src/include/libpq/pqcomm.h
  protected static final int PG_PROTOCOL_LATEST_MAJOR = 1;
  protected static final int PG_PROTOCOL_LATEST_MINOR = 0;
  private static final int SM_DATABASE	= 64;
  private static final int SM_USER	= 32;
  private static final int SM_OPTIONS	= 64;
  private static final int SM_UNUSED	= 64;
  private static final int SM_TTY	= 64;
  
  private static final int AUTH_REQ_OK       = 0;
  private static final int AUTH_REQ_KRB4     = 1;
  private static final int AUTH_REQ_KRB5     = 2;
  private static final int AUTH_REQ_PASSWORD = 3;
  private static final int AUTH_REQ_CRYPT    = 4;
  
  // New for 6.3, salt value for crypt authorisation
  private String salt;
  
  // This is used by Field to cache oid -> names.
  // It's here, because it's shared across this connection only.
  // Hence it cannot be static within the Field class, because it would then
  // be across all connections, which could be to different backends.
  public Hashtable fieldCache = new Hashtable();
  
  // Now handle notices as warnings, so things like "show" now work
  public SQLWarning firstWarning = null;
  
    /**
     * This is called by Class.forName() from within postgresql.Driver
     */
    public Connection()
    {
    }
    
    /**
     * This method actually opens the connection. It is called by Driver.
     *
     * @param host the hostname of the database back end
     * @param port the port number of the postmaster process
     * @param info a Properties[] thing of the user and password
     * @param database the database to connect to
     * @param u the URL of the connection
     * @param d the Driver instantation of the connection
     * @return a valid connection profile
     * @exception SQLException if a database access error occurs
     */
    protected void openConnection(String host, int port, Properties info, String database, String url, Driver d) throws SQLException
    {
    // Throw an exception if the user or password properties are missing
    // This occasionally occurs when the client uses the properties version
    // of getConnection(), and is a common question on the email lists
    if(info.getProperty("user")==null)
      throw new PSQLException("postgresql.con.user");
    if(info.getProperty("password")==null)
      throw new PSQLException("postgresql.con.pass");
    
    this_driver = d;
    this_url = new String(url);
    PG_DATABASE = new String(database);
    PG_PASSWORD = new String(info.getProperty("password"));
    PG_USER = new String(info.getProperty("user"));
    PG_PORT = port;
    PG_HOST = new String(host);
    PG_STATUS = CONNECTION_BAD;
    
    // Now make the initial connection
    try
      {
	pg_stream = new PG_Stream(host, port);
      } catch (ConnectException cex) {
	// Added by Peter Mount <peter@retep.org.uk>
	// ConnectException is thrown when the connection cannot be made.
	// we trap this an return a more meaningful message for the end user
	throw new PSQLException ("postgresql.con.refused");
      } catch (IOException e) {
	throw new PSQLException ("postgresql.con.failed",e);
      }
      
      // Now we need to construct and send a startup packet
      try
	{
	  // Ver 6.3 code
	  pg_stream.SendInteger(4+4+SM_DATABASE+SM_USER+SM_OPTIONS+SM_UNUSED+SM_TTY,4);
	  pg_stream.SendInteger(PG_PROTOCOL_LATEST_MAJOR,2);
	  pg_stream.SendInteger(PG_PROTOCOL_LATEST_MINOR,2);
	  pg_stream.Send(database.getBytes(),SM_DATABASE);
	  
	  // This last send includes the unused fields
	  pg_stream.Send(PG_USER.getBytes(),SM_USER+SM_OPTIONS+SM_UNUSED+SM_TTY);
	  
	  // now flush the startup packets to the backend
	  pg_stream.flush();
	  
	  // Now get the response from the backend, either an error message
	  // or an authentication request
	  int areq = -1; // must have a value here
	  do {
	    int beresp = pg_stream.ReceiveChar();
	    switch(beresp)
	      {
	      case 'E':
		// An error occured, so pass the error message to the
		// user.
		//
		// The most common one to be thrown here is:
		// "User authentication failed"
		//
		throw new SQLException(pg_stream.ReceiveString(4096));
		
	      case 'R':
		// Get the type of request
		areq = pg_stream.ReceiveIntegerR(4);
		
		// Get the password salt if there is one
		if(areq == AUTH_REQ_CRYPT) {
		  byte[] rst = new byte[2];
		  rst[0] = (byte)pg_stream.ReceiveChar();
		  rst[1] = (byte)pg_stream.ReceiveChar();
		  salt = new String(rst,0,2);
		  DriverManager.println("Salt="+salt);
		}
		
		// now send the auth packet
		switch(areq)
		  {
		  case AUTH_REQ_OK:
		    break;
		    
		  case AUTH_REQ_KRB4:
		    DriverManager.println("postgresql: KRB4");
		    throw new PSQLException("postgresql.con.kerb4");
		    
		  case AUTH_REQ_KRB5:
		    DriverManager.println("postgresql: KRB5");
		    throw new PSQLException("postgresql.con.kerb5");
		    
		  case AUTH_REQ_PASSWORD:
		    DriverManager.println("postgresql: PASSWORD");
		    pg_stream.SendInteger(5+PG_PASSWORD.length(),4);
		    pg_stream.Send(PG_PASSWORD.getBytes());
		    pg_stream.SendInteger(0,1);
		    pg_stream.flush();
		    break;
		    
		  case AUTH_REQ_CRYPT:
		    DriverManager.println("postgresql: CRYPT");
		    String crypted = UnixCrypt.crypt(salt,PG_PASSWORD);
		    pg_stream.SendInteger(5+crypted.length(),4);
		    pg_stream.Send(crypted.getBytes());
		    pg_stream.SendInteger(0,1);
		    pg_stream.flush();
		    break;
		    
		  default:
		    throw new PSQLException("postgresql.con.auth",new Integer(areq));
		  }
		break;
		
	      default:
		throw new PSQLException("postgresql.con.authfail");
	      }
	    } while(areq != AUTH_REQ_OK);
	  
	} catch (IOException e) {
	  throw new PSQLException("postgresql.con.failed",e);
	}
	
      // Originally we issued a SHOW DATESTYLE statement to find the databases default
      // datestyle. However, this caused some problems with timestamps, so in 6.5, we
      // went the way of ODBC, and set the connection to ISO.
      //
      // This may cause some clients to break when they assume anything other than ISO,
      // but then - they should be using the proper methods ;-)
      //
      //
      firstWarning = null;
      
      ExecSQL("set datestyle to 'ISO'");
      
      // Initialise object handling
      initObjectTypes();
      
      // Mark the connection as ok, and cleanup
      firstWarning = null;
      PG_STATUS = CONNECTION_OK;
    }
    
    // These methods used to be in the main Connection implementation. As they
    // are common to all implementations (JDBC1 or 2), they are placed here.
    // This should make it easy to maintain the two specifications.
    
    /**
     * This adds a warning to the warning chain.
     * @param msg message to add
     */
    public void addWarning(String msg)
    {
	DriverManager.println(msg);
	
	// Add the warning to the chain
	if(firstWarning!=null)
	    firstWarning.setNextWarning(new SQLWarning(msg));
	else
	    firstWarning = new SQLWarning(msg);
	
	// Now check for some specific messages
	
	// This is obsolete in 6.5, but I've left it in here so if we need to use this
	// technique again, we'll know where to place it.
	//
	// This is generated by the SQL "show datestyle"
	//if(msg.startsWith("NOTICE:") && msg.indexOf("DateStyle")>0) {
	//// 13 is the length off "DateStyle is "
	//msg = msg.substring(msg.indexOf("DateStyle is ")+13);
	//  
	//for(int i=0;i<dateStyles.length;i+=2)
	//if(msg.startsWith(dateStyles[i]))
	//currentDateStyle=i+1; // this is the index of the format
	//}
    }
    
    /**
     * Send a query to the backend.  Returns one of the ResultSet
     * objects.
     *
     * <B>Note:</B> there does not seem to be any method currently
     * in existance to return the update count.
     *
     * @param sql the SQL statement to be executed
     * @return a ResultSet holding the results
     * @exception SQLException if a database error occurs
     */
    public java.sql.ResultSet ExecSQL(String sql) throws SQLException
    {
	// added Oct 7 1998 to give us thread safety.
	synchronized(pg_stream) {
	    
	    Field[] fields = null;
	    Vector tuples = new Vector();
	    byte[] buf = new byte[sql.length()];
	    int fqp = 0;
	    boolean hfr = false;
	    String recv_status = null, msg;
		int update_count = 1;
	    SQLException final_error = null;
	    
	    if (sql.length() > 8192)
		throw new PSQLException("postgresql.con.toolong",sql);
	    try
		{
		    pg_stream.SendChar('Q');
		    buf = sql.getBytes();
		    pg_stream.Send(buf);
		    pg_stream.SendChar(0);
		    pg_stream.flush();
		} catch (IOException e) {
		    throw new PSQLException("postgresql.con.ioerror",e);
		}
	    
	    while (!hfr || fqp > 0)
		{
		    Object tup=null;	// holds rows as they are recieved
		    
		    int c = pg_stream.ReceiveChar();
		    
		    switch (c)
			{
			case 'A':	// Asynchronous Notify
			    int pid = pg_stream.ReceiveInteger(4);
			    msg = pg_stream.ReceiveString(8192);
			    break;
			case 'B':	// Binary Data Transfer
			    if (fields == null)
				throw new PSQLException("postgresql.con.tuple");
			    tup = pg_stream.ReceiveTuple(fields.length, true);
			    // This implements Statement.setMaxRows()
			    if(maxrows==0 || tuples.size()<maxrows)
				tuples.addElement(tup);
			    break;
			case 'C':	// Command Status
			    recv_status = pg_stream.ReceiveString(8192);
				
				// Now handle the update count correctly.
				if(recv_status.startsWith("INSERT") || recv_status.startsWith("UPDATE")) {
					try {
						update_count = Integer.parseInt(recv_status.substring(1+recv_status.lastIndexOf(' ')));
					} catch(NumberFormatException nfe) {
						throw new PSQLException("postgresql.con.fathom",recv_status);
					}
				}
			    if (fields != null)
				hfr = true;
			    else
				{
				    try
					{
					    pg_stream.SendChar('Q');
					    pg_stream.SendChar(' ');
					    pg_stream.SendChar(0);
					    pg_stream.flush();
					} catch (IOException e) {
					    throw new PSQLException("postgresql.con.ioerror",e);
					}
				    fqp++;
				}
			    break;
			case 'D':	// Text Data Transfer
			    if (fields == null)
				throw new PSQLException("postgresql.con.tuple");
			    tup = pg_stream.ReceiveTuple(fields.length, false);
			    // This implements Statement.setMaxRows()
			    if(maxrows==0 || tuples.size()<maxrows)
				tuples.addElement(tup);
			    break;
			case 'E':	// Error Message
			    msg = pg_stream.ReceiveString(4096);
			    final_error = new SQLException(msg);
			    hfr = true;
			    break;
			case 'I':	// Empty Query
			    int t = pg_stream.ReceiveChar();
			    
			    if (t != 0)
				throw new PSQLException("postgresql.con.garbled");
			    if (fqp > 0)
				fqp--;
			    if (fqp == 0)
				hfr = true;
			    break;
			case 'N':	// Error Notification
			    addWarning(pg_stream.ReceiveString(4096));
			    break;
			case 'P':	// Portal Name
			    String pname = pg_stream.ReceiveString(8192);
			    break;
			case 'T':	// MetaData Field Description
			    if (fields != null)
				throw new PSQLException("postgresql.con.multres");
			    fields = ReceiveFields();
			    break;
			default:
			    throw new PSQLException("postgresql.con.type",new Character((char)c));
			}
		}
	    if (final_error != null)
		throw final_error;
		
	    return getResultSet(this, fields, tuples, recv_status, update_count);
	}
    }

    /**
     * Receive the field descriptions from the back end
     *
     * @return an array of the Field object describing the fields
     * @exception SQLException if a database error occurs
     */
    private Field[] ReceiveFields() throws SQLException
    {
	int nf = pg_stream.ReceiveIntegerR(2), i;
	Field[] fields = new Field[nf];
	
	for (i = 0 ; i < nf ; ++i)
	    {
		String typname = pg_stream.ReceiveString(8192);
		int typid = pg_stream.ReceiveIntegerR(4);
		int typlen = pg_stream.ReceiveIntegerR(2);
		fields[i] = new Field(this, typname, typid, typlen);
	    }
	return fields;
    }
    
    /**
     * In SQL, a result table can be retrieved through a cursor that
     * is named.  The current row of a result can be updated or deleted
     * using a positioned update/delete statement that references the
     * cursor name.
     *
     * We support one cursor per connection.
     *
     * setCursorName sets the cursor name.
     *
     * @param cursor the cursor name
     * @exception SQLException if a database access error occurs
     */
    public void setCursorName(String cursor) throws SQLException
    {
	this.cursor = cursor;
    }
    
    /**
     * getCursorName gets the cursor name.
     *
     * @return the current cursor name
     * @exception SQLException if a database access error occurs
     */
    public String getCursorName() throws SQLException
    {
	return cursor;
    }
    
    /**
     * We are required to bring back certain information by
     * the DatabaseMetaData class.  These functions do that.
     *
     * Method getURL() brings back the URL (good job we saved it)
     *
     * @return the url
     * @exception SQLException just in case...
     */
    public String getURL() throws SQLException
    {
	return this_url;
    }
    
    /**
     * Method getUserName() brings back the User Name (again, we
     * saved it)
     *
     * @return the user name
     * @exception SQLException just in case...
     */
    public String getUserName() throws SQLException
    {
	return PG_USER;
    }
    
    /**
     * This returns the Fastpath API for the current connection.
     *
     * <p><b>NOTE:</b> This is not part of JDBC, but allows access to
     * functions on the postgresql backend itself.
     *
     * <p>It is primarily used by the LargeObject API
     *
     * <p>The best way to use this is as follows:
     *
     * <p><pre>
     * import postgresql.fastpath.*;
     * ...
     * Fastpath fp = ((postgresql.Connection)myconn).getFastpathAPI();
     * </pre>
     *
     * <p>where myconn is an open Connection to postgresql.
     *
     * @return Fastpath object allowing access to functions on the postgresql
     * backend.
     * @exception SQLException by Fastpath when initialising for first time
     */
    public Fastpath getFastpathAPI() throws SQLException
    {
	if(fastpath==null)
	    fastpath = new Fastpath(this,pg_stream);
	return fastpath;
    }
    
    // This holds a reference to the Fastpath API if already open
    private Fastpath fastpath = null;
    
    /**
     * This returns the LargeObject API for the current connection.
     *
     * <p><b>NOTE:</b> This is not part of JDBC, but allows access to
     * functions on the postgresql backend itself.
     *
     * <p>The best way to use this is as follows:
     *
     * <p><pre>
     * import postgresql.largeobject.*;
     * ...
     * LargeObjectManager lo = ((postgresql.Connection)myconn).getLargeObjectAPI();
     * </pre>
     *
     * <p>where myconn is an open Connection to postgresql.
     *
     * @return LargeObject object that implements the API
     * @exception SQLException by LargeObject when initialising for first time
     */
    public LargeObjectManager getLargeObjectAPI() throws SQLException
    {
	if(largeobject==null)
	    largeobject = new LargeObjectManager(this);
	return largeobject;
    }
    
    // This holds a reference to the LargeObject API if already open
    private LargeObjectManager largeobject = null;
    
    /**
     * This method is used internally to return an object based around
     * postgresql's more unique data types.
     *
     * <p>It uses an internal Hashtable to get the handling class. If the
     * type is not supported, then an instance of postgresql.util.PGobject
     * is returned.
     *
     * You can use the getValue() or setValue() methods to handle the returned
     * object. Custom objects can have their own methods.
     *
     * In 6.4, this is extended to use the postgresql.util.Serialize class to
     * allow the Serialization of Java Objects into the database without using
     * Blobs. Refer to that class for details on how this new feature works.
     *
     * @return PGobject for this type, and set to value
     * @exception SQLException if value is not correct for this type
     * @see postgresql.util.Serialize
     */
    public Object getObject(String type,String value) throws SQLException
    {
	try {
	    Object o = objectTypes.get(type);
	    
	    // If o is null, then the type is unknown, so check to see if type
	    // is an actual table name. If it does, see if a Class is known that
	    // can handle it
	    if(o == null) {
		Serialize ser = new Serialize(this,type);
		objectTypes.put(type,ser);
		return ser.fetch(Integer.parseInt(value));
	    }
	    
	    // If o is not null, and it is a String, then its a class name that
	    // extends PGobject.
	    //
	    // This is used to implement the postgresql unique types (like lseg,
	    // point, etc).
	    if(o instanceof String) {
		// 6.3 style extending PG_Object
		PGobject obj = null;
		obj = (PGobject)(Class.forName((String)o).newInstance());
		obj.setType(type);
		obj.setValue(value);
		return (Object)obj;
	    } else {
		// If it's an object, it should be an instance of our Serialize class
		// If so, then call it's fetch method.
		if(o instanceof Serialize)
		    return ((Serialize)o).fetch(Integer.parseInt(value));
	    }
	} catch(SQLException sx) {
	    // rethrow the exception. Done because we capture any others next
	    sx.fillInStackTrace();
	    throw sx;
	} catch(Exception ex) {
	    throw new PSQLException("postgresql.con.creobj",type,ex);
	}
	
	// should never be reached
	return null;
    }
    
    /**
     * This stores an object into the database.
     * @param o Object to store
     * @return OID of the new rectord
     * @exception SQLException if value is not correct for this type
     * @see postgresql.util.Serialize
     */
    public int putObject(Object o) throws SQLException
    {
	try {
	    String type = o.getClass().getName();
	    Object x = objectTypes.get(type);
	    
	    // If x is null, then the type is unknown, so check to see if type
	    // is an actual table name. If it does, see if a Class is known that
	    // can handle it
	    if(x == null) {
		Serialize ser = new Serialize(this,type);
		objectTypes.put(type,ser);
		return ser.store(o);
	    }
	    
	    // If it's an object, it should be an instance of our Serialize class
	    // If so, then call it's fetch method.
	    if(x instanceof Serialize)
		return ((Serialize)x).store(o);
	    
	    // Thow an exception because the type is unknown
	    throw new PSQLException("postgresql.con.strobj");
	    
	} catch(SQLException sx) {
	    // rethrow the exception. Done because we capture any others next
	    sx.fillInStackTrace();
	    throw sx;
	} catch(Exception ex) {
	    throw new PSQLException("postgresql.con.strobjex",ex);
	}
    }
    
    /**
     * This allows client code to add a handler for one of postgresql's
     * more unique data types.
     *
     * <p><b>NOTE:</b> This is not part of JDBC, but an extension.
     *
     * <p>The best way to use this is as follows:
     *
     * <p><pre>
     * ...
     * ((postgresql.Connection)myconn).addDataType("mytype","my.class.name");
     * ...
     * </pre>
     *
     * <p>where myconn is an open Connection to postgresql.
     *
     * <p>The handling class must extend postgresql.util.PGobject
     *
     * @see postgresql.util.PGobject
     */
    public void addDataType(String type,String name)
    {
	objectTypes.put(type,name);
    }
    
    // This holds the available types
    private Hashtable objectTypes = new Hashtable();
    
    // This array contains the types that are supported as standard.
    //
    // The first entry is the types name on the database, the second
    // the full class name of the handling class.
    //
    private static final String defaultObjectTypes[][] = {
	{"box",		"postgresql.geometric.PGbox"},
	{"circle",	"postgresql.geometric.PGcircle"},
	{"line",	"postgresql.geometric.PGline"},
	{"lseg",	"postgresql.geometric.PGlseg"},
	{"path",	"postgresql.geometric.PGpath"},
	{"point",	"postgresql.geometric.PGpoint"},
	{"polygon",	"postgresql.geometric.PGpolygon"},
	{"money",	"postgresql.util.PGmoney"}
    };
    
    // This initialises the objectTypes hashtable
    private void initObjectTypes()
    {
	for(int i=0;i<defaultObjectTypes.length;i++)
	    objectTypes.put(defaultObjectTypes[i][0],defaultObjectTypes[i][1]);
    }
    
    // These are required by other common classes
    public abstract java.sql.Statement createStatement() throws SQLException;
    
    /**
     * This returns a resultset. It must be overridden, so that the correct
     * version (from jdbc1 or jdbc2) are returned.
     */
    protected abstract java.sql.ResultSet getResultSet(postgresql.Connection conn, Field[] fields, Vector tuples, String status, int updateCount) throws SQLException;
}
