package postgresql; import java.io.*; import java.lang.*; import java.net.*; import java.util.*; import java.sql.*; import postgresql.*; /** * @version 1.0 15-APR-1997 * @author Adrian Hall * * A Connection represents a session with a specific database. Within the * context of a Connection, SQL statements are executed and results are * returned. * * A Connection's database is able to provide information describing * its tables, its supported SQL grammar, its stored procedures, the * capabilities of this connection, etc. This information is obtained * with the getMetaData method. * * Note: By default, the Connection automatically commits changes * after executing each statement. If auto-commit has been disabled, an * explicit commit must be done or database changes will not be saved. * * @see java.sql.Connection */ public class Connection implements java.sql.Connection { private PG_Stream pg_stream; 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; private int STARTUP_CODE = 7; private boolean autoCommit = true; private boolean readOnly = false; private Driver this_driver; private String this_url; private String cursor = null; // The positioned update cursor name /** * Connect to a PostgreSQL database back end. * * @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 */ public Connection(String host, int port, Properties info, String database, String url, Driver d) throws SQLException { int len = 288; // Length of a startup packet 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; try { pg_stream = new PG_Stream(host, port); } catch (IOException e) { throw new SQLException ("Connection failed: " + e.toString()); } // Now we need to construct and send a startup packet try { pg_stream.SendInteger(len, 4); len -= 4; pg_stream.SendInteger(STARTUP_CODE, 4); len -= 4; pg_stream.Send(database.getBytes(), 64); len -= 64; pg_stream.Send(PG_USER.getBytes(), len); } catch (IOException e) { throw new SQLException("Connection failed: " + e.toString()); } ExecSQL(" "); // Test connection PG_STATUS = CONNECTION_OK; } /** * SQL statements without parameters are normally executed using * Statement objects. If the same SQL statement is executed many * times, it is more efficient to use a PreparedStatement * * @return a new Statement object * @exception SQLException passed through from the constructor */ public java.sql.Statement createStatement() throws SQLException { return new Statement(this); } /** * A SQL statement with or without IN parameters can be pre-compiled * and stored in a PreparedStatement object. This object can then * be used to efficiently execute this statement multiple times. * * Note: This method is optimized for handling parametric * SQL statements that benefit from precompilation if the drivers * supports precompilation. PostgreSQL does not support precompilation. * In this case, the statement is not sent to the database until the * PreparedStatement is executed. This has no direct effect on users; * however it does affect which method throws certain SQLExceptions * * @param sql a SQL statement that may contain one or more '?' IN * parameter placeholders * @return a new PreparedStatement object containing the pre-compiled * statement. * @exception SQLException if a database access error occurs. */ public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException { return new PreparedStatement(this, sql); } /** * A SQL stored procedure call statement is handled by creating a * CallableStatement for it. The CallableStatement provides methods * for setting up its IN and OUT parameters and methods for executing * it. * * Note: This method is optimised for handling stored procedure * call statements. Some drivers may send the call statement to the * database when the prepareCall is done; others may wait until the * CallableStatement is executed. This has no direct effect on users; * however, it does affect which method throws certain SQLExceptions * * @param sql a SQL statement that may contain one or more '?' parameter * placeholders. Typically this statement is a JDBC function call * escape string. * @return a new CallableStatement object containing the pre-compiled * SQL statement * @exception SQLException if a database access error occurs */ public java.sql.CallableStatement prepareCall(String sql) throws SQLException { throw new SQLException("Callable Statements are not supported at this time"); // return new CallableStatement(this, sql); } /** * A driver may convert the JDBC sql grammar into its system's * native SQL grammar prior to sending it; nativeSQL returns the * native form of the statement that the driver would have sent. * * @param sql a SQL statement that may contain one or more '?' * parameter placeholders * @return the native form of this statement * @exception SQLException if a database access error occurs */ public String nativeSQL(String sql) throws SQLException { return sql; } /** * If a connection is in auto-commit mode, than all its SQL * statements will be executed and committed as individual * transactions. Otherwise, its SQL statements are grouped * into transactions that are terminated by either commit() * or rollback(). By default, new connections are in auto- * commit mode. The commit occurs when the statement completes * or the next execute occurs, whichever comes first. In the * case of statements returning a ResultSet, the statement * completes when the last row of the ResultSet has been retrieved * or the ResultSet has been closed. In advanced cases, a single * statement may return multiple results as well as output parameter * values. Here the commit occurs when all results and output param * values have been retrieved. * * @param autoCommit - true enables auto-commit; false disables it * @exception SQLException if a database access error occurs */ public void setAutoCommit(boolean autoCommit) throws SQLException { if (this.autoCommit == autoCommit) return; if (autoCommit) ExecSQL("end"); else ExecSQL("begin"); this.autoCommit = autoCommit; } /** * gets the current auto-commit state * * @return Current state of the auto-commit mode * @exception SQLException (why?) * @see setAutoCommit */ public boolean getAutoCommit() throws SQLException { return this.autoCommit; } /** * The method commit() makes all changes made since the previous * commit/rollback permanent and releases any database locks currently * held by the Connection. This method should only be used when * auto-commit has been disabled. (If autoCommit == true, then we * just return anyhow) * * @exception SQLException if a database access error occurs * @see setAutoCommit */ public void commit() throws SQLException { if (autoCommit) return; ExecSQL("commit"); autoCommit = true; ExecSQL("begin"); autoCommit = false; } /** * The method rollback() drops all changes made since the previous * commit/rollback and releases any database locks currently held by * the Connection. * * @exception SQLException if a database access error occurs * @see commit */ public void rollback() throws SQLException { if (autoCommit) return; ExecSQL("rollback"); autoCommit = true; ExecSQL("begin"); autoCommit = false; } /** * In some cases, it is desirable to immediately release a Connection's * database and JDBC resources instead of waiting for them to be * automatically released (cant think why off the top of my head) * * Note: A Connection is automatically closed when it is * garbage collected. Certain fatal errors also result in a closed * connection. * * @exception SQLException if a database access error occurs */ public void close() throws SQLException { if (pg_stream != null) { try { pg_stream.close(); } catch (IOException e) {} pg_stream = null; } } /** * Tests to see if a Connection is closed * * @return the status of the connection * @exception SQLException (why?) */ public boolean isClosed() throws SQLException { return (pg_stream == null); } /** * A connection's database is able to provide information describing * its tables, its supported SQL grammar, its stored procedures, the * capabilities of this connection, etc. This information is made * available through a DatabaseMetaData object. * * @return a DatabaseMetaData object for this connection * @exception SQLException if a database access error occurs */ public java.sql.DatabaseMetaData getMetaData() throws SQLException { // return new DatabaseMetaData(this); throw new SQLException("DatabaseMetaData not supported"); } /** * You can put a connection in read-only mode as a hunt to enable * database optimizations * * Note: setReadOnly cannot be called while in the middle * of a transaction * * @param readOnly - true enables read-only mode; false disables it * @exception SQLException if a database access error occurs */ public void setReadOnly (boolean readOnly) throws SQLException { this.readOnly = readOnly; } /** * Tests to see if the connection is in Read Only Mode. Note that * we cannot really put the database in read only mode, but we pretend * we can by returning the value of the readOnly flag * * @return true if the connection is read only * @exception SQLException if a database access error occurs */ public boolean isReadOnly() throws SQLException { return readOnly; } /** * A sub-space of this Connection's database may be selected by * setting a catalog name. If the driver does not support catalogs, * it will silently ignore this request * * @exception SQLException if a database access error occurs */ public void setCatalog(String catalog) throws SQLException { // No-op } /** * Return the connections current catalog name, or null if no * catalog name is set, or we dont support catalogs. * * @return the current catalog name or null * @exception SQLException if a database access error occurs */ public String getCatalog() throws SQLException { return null; } /** * You can call this method to try to change the transaction * isolation level using one of the TRANSACTION_* values. * * Note: setTransactionIsolation cannot be called while * in the middle of a transaction * * @param level one of the TRANSACTION_* isolation values with * the exception of TRANSACTION_NONE; some databases may * not support other values * @exception SQLException if a database access error occurs * @see java.sql.DatabaseMetaData#supportsTransactionIsolationLevel */ public void setTransactionIsolation(int level) throws SQLException { throw new SQLException("Transaction Isolation Levels are not implemented"); } /** * Get this Connection's current transaction isolation mode. * * @return the current TRANSACTION_* mode value * @exception SQLException if a database access error occurs */ public int getTransactionIsolation() throws SQLException { return java.sql.Connection.TRANSACTION_SERIALIZABLE; } /** * The first warning reported by calls on this Connection is * returned. * * Note: Sebsequent warnings will be changed to this * SQLWarning * * @return the first SQLWarning or null * @exception SQLException if a database access error occurs */ public SQLWarning getWarnings() throws SQLException { return null; // We handle warnings as errors } /** * After this call, getWarnings returns null until a new warning * is reported for this connection. * * @exception SQLException if a database access error occurs */ public void clearWarnings() throws SQLException { // Not handles since we handle wanrings as errors } // ********************************************************** // END OF PUBLIC INTERFACE // ********************************************************** /** * Send a query to the backend. Returns one of the ResultSet * objects. * * Note: 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 synchronized ResultSet ExecSQL(String sql) throws SQLException { Field[] fields = null; Vector tuples = new Vector(); byte[] buf = new byte[sql.length()]; int fqp = 0; boolean hfr = false; String recv_status = null, msg; SQLException final_error = null; if (sql.length() > 8192) throw new SQLException("SQL Statement too long: " + sql); try { pg_stream.SendChar('Q'); buf = sql.getBytes(); pg_stream.Send(buf); pg_stream.SendChar(0); } catch (IOException e) { throw new SQLException("I/O Error: " + e.toString()); } while (!hfr || fqp > 0) { 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 SQLException("Tuple received before MetaData"); tuples.addElement(pg_stream.ReceiveTuple(fields.length, true)); break; case 'C': // Command Status recv_status = pg_stream.ReceiveString(8192); if (fields != null) hfr = true; else { try { pg_stream.SendChar('Q'); pg_stream.SendChar(' '); pg_stream.SendChar(0); } catch (IOException e) { throw new SQLException("I/O Error: " + e.toString()); } fqp++; } break; case 'D': // Text Data Transfer if (fields == null) throw new SQLException("Tuple received before MetaData"); tuples.addElement(pg_stream.ReceiveTuple(fields.length, false)); 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 SQLException("Garbled Data"); if (fqp > 0) fqp--; if (fqp == 0) hfr = true; break; case 'N': // Error Notification msg = pg_stream.ReceiveString(4096); PrintStream log = DriverManager.getLogStream(); log.println(msg); break; case 'P': // Portal Name String pname = pg_stream.ReceiveString(8192); break; case 'T': // MetaData Field Description if (fields != null) throw new SQLException("Cannot handle multiple result groups"); fields = ReceiveFields(); break; default: throw new SQLException("Unknown Response Type: " + (char)c); } } if (final_error != null) throw final_error; return new ResultSet(this, fields, tuples, recv_status, 1); } /** * 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.ReceiveInteger(2), i; Field[] fields = new Field[nf]; for (i = 0 ; i < nf ; ++i) { String typname = pg_stream.ReceiveString(8192); int typid = pg_stream.ReceiveInteger(4); int typlen = pg_stream.ReceiveInteger(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 class handles all the Streamed I/O for a postgresql connection class PG_Stream { private Socket connection; private InputStream pg_input; private OutputStream pg_output; /** * Constructor: Connect to the PostgreSQL back end and return * a stream connection. * * @param host the hostname to connect to * @param port the port number that the postmaster is sitting on * @exception IOException if an IOException occurs below it. */ public PG_Stream(String host, int port) throws IOException { connection = new Socket(host, port); pg_input = connection.getInputStream(); pg_output = connection.getOutputStream(); } /** * Sends a single character to the back end * * @param val the character to be sent * @exception IOException if an I/O error occurs */ public void SendChar(int val) throws IOException { pg_output.write(val); } /** * Sends an integer to the back end * * @param val the integer to be sent * @param siz the length of the integer in bytes (size of structure) * @exception IOException if an I/O error occurs */ public void SendInteger(int val, int siz) throws IOException { byte[] buf = new byte[siz]; while (siz-- > 0) { buf[siz] = (byte)(val & 0xff); val >>= 8; } Send(buf); } /** * Send an array of bytes to the backend * * @param buf The array of bytes to be sent * @exception IOException if an I/O error occurs */ public void Send(byte buf[]) throws IOException { pg_output.write(buf); } /** * Send an exact array of bytes to the backend - if the length * has not been reached, send nulls until it has. * * @param buf the array of bytes to be sent * @param siz the number of bytes to be sent * @exception IOException if an I/O error occurs */ public void Send(byte buf[], int siz) throws IOException { int i; pg_output.write(buf, 0, (buf.length < siz ? buf.length : siz)); if (buf.length < siz) { for (i = buf.length ; i < siz ; ++i) { pg_output.write(0); } } } /** * Receives a single character from the backend * * @return the character received * @exception SQLException if an I/O Error returns */ public int ReceiveChar() throws SQLException { int c = 0; try { c = pg_input.read(); if (c < 0) throw new IOException("EOF"); } catch (IOException e) { throw new SQLException("Error reading from backend: " + e.toString()); } return c; } /** * Receives an integer from the backend * * @param siz length of the integer in bytes * @return the integer received from the backend * @exception SQLException if an I/O error occurs */ public int ReceiveInteger(int siz) throws SQLException { int n = 0; try { for (int i = 0 ; i < siz ; i++) { int b = pg_input.read(); if (b < 0) throw new IOException("EOF"); n = n | (b >> (8 * i)) ; } } catch (IOException e) { throw new SQLException("Error reading from backend: " + e.toString()); } return n; } /** * Receives a null-terminated string from the backend. Maximum of * maxsiz bytes - if we don't see a null, then we assume something * has gone wrong. * * @param maxsiz maximum length of string * @return string from back end * @exception SQLException if an I/O error occurs */ public String ReceiveString(int maxsiz) throws SQLException { byte[] rst = new byte[maxsiz]; int s = 0; try { while (s < maxsiz) { int c = pg_input.read(); if (c < 0) throw new IOException("EOF"); else if (c == 0) break; else rst[s++] = (byte)c; } if (s >= maxsiz) throw new IOException("Too Much Data"); } catch (IOException e) { throw new SQLException("Error reading from backend: " + e.toString()); } String v = new String(rst, 0, s); return v; } /** * Read a tuple from the back end. A tuple is a two dimensional * array of bytes * * @param nf the number of fields expected * @param bin true if the tuple is a binary tuple * @return null if the current response has no more tuples, otherwise * an array of strings * @exception SQLException if a data I/O error occurs */ public byte[][] ReceiveTuple(int nf, boolean bin) throws SQLException { int i, bim = (nf + 7)/8; byte[] bitmask = Receive(bim); byte[][] answer = new byte[nf][0]; int whichbit = 0x80; int whichbyte = 0; for (i = 0 ; i < nf ; ++i) { boolean isNull = ((bitmask[whichbyte] & whichbit) == 0); whichbit >>= 1; if (whichbit == 0) { ++whichbyte; whichbit = 0x80; } if (isNull) answer[i] = null; else { int len = ReceiveInteger(4); if (!bin) len -= 4; if (len < 0) len = 0; answer[i] = Receive(len); } } return answer; } /** * Reads in a given number of bytes from the backend * * @param siz number of bytes to read * @return array of bytes received * @exception SQLException if a data I/O error occurs */ private byte[] Receive(int siz) throws SQLException { byte[] answer = new byte[siz]; int s = 0; try { while (s < siz) { int w = pg_input.read(answer, s, siz - s); if (w < 0) throw new IOException("EOF"); s += w; } } catch (IOException e) { throw new SQLException("Error reading from backend: " + e.toString()); } return answer; } /** * Closes the connection * * @exception IOException if a IO Error occurs */ public void close() throws IOException { pg_output.close(); pg_input.close(); connection.close(); } }