package org.postgresql;

import java.sql.*;
import java.util.*;

import org.postgresql.util.PSQLException;

/*
 * The Java SQL framework allows for multiple database drivers.  Each
 * driver should supply a class that implements the Driver interface
 *
 * <p>The DriverManager will try to load as many drivers as it can find and
 * then for any given connection request, it will ask each driver in turn
 * to try to connect to the target URL.
 *
 * <p>It is strongly recommended that each Driver class should be small and
 * standalone so that the Driver class can be loaded and queried without
 * bringing in vast quantities of supporting code.
 *
 * <p>When a Driver class is loaded, it should create an instance of itself
 * and register it with the DriverManager.	This means that a user can load
 * and register a driver by doing Class.forName("foo.bah.Driver")
 *
 * @see org.postgresql.Connection
 * @see java.sql.Driver
 */
public class Driver implements java.sql.Driver
{

	protected static final int DEBUG = 0;
	protected static final int INFO = 1;
	protected static final int WARN = 2;
	protected static final int ERROR = 3;
	protected static final int FATAL = 4;

	private static int logLevel = FATAL;

	static
	{
		try
		{
			// moved the registerDriver from the constructor to here
			// because some clients call the driver themselves (I know, as
			// my early jdbc work did - and that was based on other examples).
			// Placing it here, means that the driver is registered once only.
			java.sql.DriverManager.registerDriver(new Driver());

		}
		catch (SQLException e)
		{
			e.printStackTrace();
		}
	}

	/*
	 * Construct a new driver and register it with DriverManager
	 *
	 * @exception SQLException for who knows what!
	 */
	public Driver() throws SQLException
	{
		// Set the connectClass variable so that future calls will handle the correct
		// base class
		//if (System.getProperty("java.version").startsWith("1.1")) {
		//connectClass = "postgresql.jdbc1.Connection";
		//} else {
		//connectClass = "postgresql.jdbc2.Connection";
		//}

		// Ok, when the above code was introduced in 6.5 it's intention was to allow
		// the driver to automatically detect which version of JDBC was being used
		// and to detect the version of the JVM accordingly.
		//
		// It did this by using the java.version parameter.
		//
		// However, it was quickly discovered that not all JVM's returned an easily
		// parseable version number (ie "1.2") and some don't return a value at all.
		// The latter came from a discussion on the advanced java list.
		//
		// So, to solve this, I've moved the decision out of the driver, and it's now
		// a compile time parameter.
		//
		// For this to work, the Makefile creates a pseudo class which contains the class
		// name that will actually make the connection.
	}

	/*
	 * Try to make a database connection to the given URL.	The driver
	 * should return "null" if it realizes it is the wrong kind of
	 * driver to connect to the given URL.	This will be common, as
	 * when the JDBC driverManager is asked to connect to a given URL,
	 * it passes the URL to each loaded driver in turn.
	 *
	 * <p>The driver should raise an SQLException if it is the right driver
	 * to connect to the given URL, but has trouble connecting to the
	 * database.
	 *
	 * <p>The java.util.Properties argument can be used to pass arbitrary
	 * string tag/value pairs as connection arguments.
	 *
	 * user - (optional) The user to connect as
	 * password - (optional) The password for the user
	 * charSet - (optional) The character set to be used for converting
	 *	 to/from the database to unicode.  If multibyte is enabled on the
	 *	 server then the character set of the database is used as the default,
	 *	 otherwise the jvm character encoding is used as the default.
	 * compatible - This is used to toggle
	 *	 between different functionality as it changes across different releases
	 *	 of the jdbc driver code.  The values here are versions of the jdbc
	 *	 client and not server versions.  For example in 7.1 get/setBytes
	 *	 worked on LargeObject values, in 7.2 these methods were changed
	 *	 to work on bytea values.  This change in functionality could
	 *	 be disabled by setting the compatible level to be "7.1", in
	 *	 which case the driver will revert to the 7.1 functionality.
	 *
	 * <p>Normally, at least
	 * "user" and "password" properties should be included in the
	 * properties.	For a list of supported
	 * character encoding , see
	 * http://java.sun.com/products/jdk/1.2/docs/guide/internat/encoding.doc.html
	 * Note that you will probably want to have set up the Postgres database
	 * itself to use the same encoding, with the "-E <encoding>" argument
	 * to createdb.
	 *
	 * Our protocol takes the forms:
	 * <PRE>
	 *	jdbc:org.postgresql://host:port/database?param1=val1&...
	 * </PRE>
	 *
	 * @param url the URL of the database to connect to
	 * @param info a list of arbitrary tag/value pairs as connection
	 *	arguments
	 * @return a connection to the URL or null if it isnt us
	 * @exception SQLException if a database access error occurs
	 * @see java.sql.Driver#connect
	 */
	public java.sql.Connection connect(String url, Properties info) throws SQLException
	{
		if ((props = parseURL(url, info)) == null)
		{
			Driver.debug("Error in url" + url);
			return null;
		}
		try
		{
			Driver.debug("connect " + url);

			org.postgresql.Connection con = (org.postgresql.Connection)(Class.forName("@JDBCCONNECTCLASS@").newInstance());
			con.openConnection (host(), port(), props, database(), url, this);
			return (java.sql.Connection)con;
		}
		catch (ClassNotFoundException ex)
		{
			Driver.debug("error", ex);
			throw new PSQLException("postgresql.jvm.version", ex);
		}
		catch (PSQLException ex1)
		{
			// re-throw the exception, otherwise it will be caught next, and a
			// org.postgresql.unusual error will be returned instead.
			throw ex1;
		}
		catch (Exception ex2)
		{
			Driver.debug("error", ex2);
			throw new PSQLException("postgresql.unusual", ex2);
		}
	}

	/*
	 * Returns true if the driver thinks it can open a connection to the
	 * given URL.  Typically, drivers will return true if they understand
	 * the subprotocol specified in the URL and false if they don't.  Our
	 * protocols start with jdbc:org.postgresql:
	 *
	 * @see java.sql.Driver#acceptsURL
	 * @param url the URL of the driver
	 * @return true if this driver accepts the given URL
	 * @exception SQLException if a database-access error occurs
	 *	(Dont know why it would *shrug*)
	 */
	public boolean acceptsURL(String url) throws SQLException
	{
		if (parseURL(url, null) == null)
			return false;
		return true;
	}

	/*
	 * The getPropertyInfo method is intended to allow a generic GUI
	 * tool to discover what properties it should prompt a human for
	 * in order to get enough information to connect to a database.
	 *
	 * <p>Note that depending on the values the human has supplied so
	 * far, additional values may become necessary, so it may be necessary
	 * to iterate through several calls to getPropertyInfo
	 *
	 * @param url the Url of the database to connect to
	 * @param info a proposed list of tag/value pairs that will be sent on
	 *	connect open.
	 * @return An array of DriverPropertyInfo objects describing
	 *	possible properties.  This array may be an empty array if
	 *	no properties are required
	 * @exception SQLException if a database-access error occurs
	 * @see java.sql.Driver#getPropertyInfo
	 */
	public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException
	{
		Properties p = parseURL(url, info);

		// naughty, but its best for speed. If anyone adds a property here, then
		// this _MUST_ be increased to accomodate them.
		DriverPropertyInfo d, dpi[] = new DriverPropertyInfo[0];
		//int i=0;

		//dpi[i++] = d = new DriverPropertyInfo("auth",p.getProperty("auth","default"));
		//d.description = "determines if password authentication is used";
		//d.choices = new String[4];
		//d.choices[0]="default";	// Get value from org.postgresql.auth property, defaults to trust
		//d.choices[1]="trust"; // No password authentication
		//d.choices[2]="password";	// Password authentication
		//d.choices[3]="ident"; // Ident (RFC 1413) protocol

		return dpi;
	}

	/*
	 * Gets the drivers major version number
	 *
	 * @return the drivers major version number
	 */
	public int getMajorVersion()
	{
		return @MAJORVERSION@;
	}

	/*
	 * Get the drivers minor version number
	 *
	 * @return the drivers minor version number
	 */
	public int getMinorVersion()
	{
		return @MINORVERSION@;
	}

	/*
	 * Returns the VERSION variable from Makefile.global
	 */
	public static String getVersion()
	{
		return "@VERSION@";
	}

	/*
	 * Report whether the driver is a genuine JDBC compliant driver.  A
	 * driver may only report "true" here if it passes the JDBC compliance
	 * tests, otherwise it is required to return false.  JDBC compliance
	 * requires full support for the JDBC API and full support for SQL 92
	 * Entry Level.
	 *
	 * <p>For PostgreSQL, this is not yet possible, as we are not SQL92
	 * compliant (yet).
	 */
	public boolean jdbcCompliant()
	{
		return false;
	}

	private Properties props;

	static private String[] protocols = { "jdbc", "postgresql" };

	/*
	 * Constructs a new DriverURL, splitting the specified URL into its
	 * component parts
	 * @param url JDBC URL to parse
	 * @param defaults Default properties
	 * @return Properties with elements added from the url
	 * @exception SQLException
	 */
	Properties parseURL(String url, Properties defaults) throws SQLException
	{
		int state = -1;
		Properties urlProps = new Properties(defaults);
		String key = "";
		String value = "";

		StringTokenizer st = new StringTokenizer(url, ":/;=&?", true);
		for (int count = 0; (st.hasMoreTokens()); count++)
		{
			String token = st.nextToken();

			// PM June 29 1997
			// Added this, to help me understand how this works.
			// Unless you want each token to be processed, leave this commented out
			// but don't delete it.
			//DriverManager.println("wellFormedURL: state="+state+" count="+count+" token='"+token+"'");

			// PM Aug 2 1997 - Modified to allow multiple backends
			if (count <= 3)
			{
				if ((count % 2) == 1 && token.equals(":"))
					;
				else if ((count % 2) == 0)
				{
					boolean found = (count == 0) ? true : false;
					for (int tmp = 0;tmp < protocols.length;tmp++)
					{
						if (token.equals(protocols[tmp]))
						{
							// PM June 29 1997 Added this property to enable the driver
							// to handle multiple backend protocols.
							if (count == 2 && tmp > 0)
							{
								urlProps.put("Protocol", token);
								found = true;
							}
						}
					}

					if (found == false)
						return null;
				}
				else
					return null;
			}
			else if (count > 3)
			{
				if (count == 4 && token.equals("/"))
					state = 0;
				else if (count == 4)
				{
					urlProps.put("PGDBNAME", token);
					state = -2;
				}
				else if (count == 5 && state == 0 && token.equals("/"))
					state = 1;
				else if (count == 5 && state == 0)
					return null;
				else if (count == 6 && state == 1)
					urlProps.put("PGHOST", token);
				else if (count == 7 && token.equals(":"))
					state = 2;
				else if (count == 8 && state == 2)
				{
					try
					{
						Integer portNumber = Integer.decode(token);
						urlProps.put("PGPORT", portNumber.toString());
					}
					catch (Exception e)
					{
						return null;
					}
				}
				else if ((count == 7 || count == 9) &&
						 (state == 1 || state == 2) && token.equals("/"))
					state = -1;
				else if (state == -1)
				{
					urlProps.put("PGDBNAME", token);
					state = -2;
				}
				else if (state <= -2 && (count % 2) == 1)
				{
					// PM Aug 2 1997 - added tests for ? and &
					if (token.equals(";") || token.equals("?") || token.equals("&") )
						state = -3;
					else if (token.equals("="))
						state = -5;
				}
				else if (state <= -2 && (count % 2) == 0)
				{
					if (state == -3)
						key = token;
					else if (state == -5)
					{
						value = token;
						//DriverManager.println("put("+key+","+value+")");
						urlProps.put(key, value);
						state = -2;
					}
				}
			}
		}

		// PM June 29 1997
		// This now outputs the properties only if we are logging
		// PM Sep 13 1999 Commented out, as it throws a Deprecation warning
		// when compiled under JDK1.2.
		//if (DriverManager.getLogStream() != null)
		//	urlProps.list(DriverManager.getLogStream());

		return urlProps;

	}

	/*
	 * @return the hostname portion of the URL
	 */
	public String host()
	{
		return props.getProperty("PGHOST", "localhost");
	}

	/*
	 * @return the port number portion of the URL or -1 if no port was specified
	 */
	public int port()
	{
		return Integer.parseInt(props.getProperty("PGPORT", "@DEF_PGPORT@"));
	}

	/*
	 * @return the database name of the URL
	 */
	public String database()
	{
		return props.getProperty("PGDBNAME");
	}

	/*
	 * @return the value of any property specified in the URL or properties
	 * passed to connect(), or null if not found.
	 */
	public String property(String name)
	{
		return props.getProperty(name);
	}

	/*
	 * This method was added in v6.5, and simply throws an SQLException
	 * for an unimplemented method. I decided to do it this way while
	 * implementing the JDBC2 extensions to JDBC, as it should help keep the
	 * overall driver size down.
	 */
	public static SQLException notImplemented()
	{
		return new PSQLException("postgresql.unimplemented");
	}
	/*
	 * logging message at the debug level
	 * messages will be printed if the logging level is less or equal to DEBUG
	 */
	public static void debug(String msg)
	{
		if (logLevel <= DEBUG)
		{
			DriverManager.println(msg);
		}
	}
	/*
	 * logging message at the debug level
	 * messages will be printed if the logging level is less or equal to DEBUG
	 */
	public static void debug(String msg, Exception ex)
	{
		if (logLevel <= DEBUG)
		{
			DriverManager.println(msg + ex != null ? ex.getMessage() : "null Exception");
		}
	}
	/*
	 * logging message at info level
	 * messages will be printed if the logging level is less or equal to INFO
	 */
	public static void info(String msg)
	{
		if (logLevel <= INFO)
		{
			DriverManager.println(msg);
		}
	}
	/*
	 * logging message at info level
	 * messages will be printed if the logging level is less or equal to INFO
	 */
	public static void info(String msg, Exception ex)
	{
		if (logLevel <= INFO)
		{
			DriverManager.println(msg + ex != null ? ex.getMessage() : "null Exception");
		}
	}
	/*
	 * logging message at warn level
	 * messages will be printed if the logging level is less or equal to WARN
	 */
	public static void warn(String msg)
	{
		if (logLevel <= WARN)
		{
			DriverManager.println(msg);
		}
	}
	/*
	 * logging message at warn level
	 * messages will be printed if the logging level is less or equal to WARN
	 */
	public static void warn(String msg, Exception ex)
	{
		if (logLevel <= WARN)
		{
			DriverManager.println(msg + ex != null ? ex.getMessage() : "null Exception");
		}
	}
	/*
	 * logging message at error level
	 * messages will be printed if the logging level is less or equal to ERROR
	 */
	public static void error(String msg)
	{
		if (logLevel <= ERROR)
		{
			DriverManager.println(msg);
		}
	}
	/*
	 * logging message at error level
	 * messages will be printed if the logging level is less or equal to ERROR
	 */
	public static void error(String msg, Exception ex)
	{
		if (logLevel <= ERROR)
		{
			DriverManager.println(msg + ex != null ? ex.getMessage() : "null Exception");
		}
	}
	/*
	 * logging message at fatal level
	 * messages will be printed if the logging level is less or equal to FATAL
	 */
	public static void fatal(String msg)
	{
		if (logLevel <= FATAL)
		{
			DriverManager.println(msg);
		}
	}
	/*
	 * logging message at fatal level
	 * messages will be printed if the logging level is less or equal to FATAL
	 */
	public static void fatal(String msg, Exception ex)
	{
		if (logLevel <= FATAL)
		{
			DriverManager.println(msg + ex != null ? ex.getMessage() : "null Exception");
		}
	}
}
