package org.postgresql.util;

import java.io.*;
import java.lang.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.sql.*;

/**
 * This class uses PostgreSQL's object oriented features to store Java Objects.<p>
 *
 * It does this by mapping a Java Class name to a table in the database. Each
 * entry in this new table then represents a Serialized instance of this
 * class. As each entry has an OID (Object IDentifier), this OID can be
 * included in another table.<p>
 *
 * Serialize depends on a feature of Postgres that allows
 * a table to be used as a data type. However, Postgres support of
 * this feature is incomplete. The basic ability to create and use
 * a table as a field type in another table exists:<br>
 *    CREATE TABLE myclass( var1 TEXT, var2 INTEGER );<br>
 *    CREATE TABLE othertable( field1 TEXT, field2 myclass );<br>
 *    INSERT INTO myclass VALUES ('Hello', 1);<br>
 *    INSERT INTO othertable VALUES ('World', xxxx::myclass);<br>
 *    where xxxx is the OID of a row in myclass<br>
 * This lets othertable reference a myclass instance but
 * the support to actually make any use of the myclass data type
 * is not there. For instance, you cannot compare the myclass field
 * with ANY other data type, not even with other myclass values.
 * Casting to and from the myclass type will also not work.
 * From the limited testing done, only the literal xxxx::myclass
 * syntax appears to work.<p>
 *
 * Queries like:<br>
 *    SELECT othertable.field2.var1 FROM othertable;<br>
 * will not work but were suggested in the original Postgres
 * design documents.<p>
 * Because support is incomplete for table data types, tables
 * such as othertable that hold java instances should also
 * hold an oid field for the same java instance:<br>
 *    CREATE othertable( field1 TEXT, field2 myclass, myclassOID oid);<br>
 * This oid-type field would be set with setInt() immediately after
 * setting the myclass-type field with setObject(). The order of these
 * set calls matters since the oid is not available until after
 * setting the object when inserting a new object. With the oid,
 * queries and comparisons etc. can be done to locate a myclass.
 * Read below about how to include an int oid field in your java class
 * that is updated by setObject() when it is inserted.<p>
 *
 * The myclass table represents a java class. This table is created
 * by Serialize.create(). Serialize.create() must be called before
 * the first use of the myclass java class in PreparedStatement.setObject()
 * calls. This is a one-time initialization step.<p>
 *
 * There are a number of limitations placed on the java class to be
 * used by Serialize:
 * <ul>
 * <li>The class name must be less than 32 chars long and must be all lowercase.
 * This is due to limitations in Postgres about the size of table names.
 * The name must be all lowercase since table names in Postgres are
 * case insensitive and the relname is stored in lowercase. Unless some
 * additional table were to be maintained about the names of java classes,
 * there is no way to know how to go from a Postgres table name back to
 * a java class name with knowledge of case of the letters in the name.
 * <li>The class name must not contain the underscore '_' character since
 * any dots in a java class name are converted to an underscore in
 * its table name and vice versa going back.
 * <li>The class should only contain java primitive types and String.
 * Support for recursively "serializing" a class is not tested but
 * some code for this does exist and you may wish to take a look at it.
 * <li>Only the public fields of the class will be stored in and fetched from
 * the database. Protected and private fields are ignored.
 * <li>Must have a no-arg constructor so that Class.newInstance() may
 * instantiate the class in fetch().
 * <li>Should implement the Serializable interface. This interface
 * may be used more in future releases or in providing an alternative
 * method of storing the java classes in the database. The Serializable
 * interface allows a class instance to be written out as a binary
 * stream of data and is a standard java feature.
 * <li>The class should contain a field defined as:<br>
 * int oid = 0;<br>
 * This field is actually optional and its use by jdbc2.PreparedStatement.setObject()
 * is as follows:<br>
 * If oid does not exist in the class, the class instance is stored in a new table row
 * everytime setObject() is called on it. If oid field exists and is 0, then the class
 * instance is stored into a new row in the table and that row's oid is set in the class by setObject().
 * If oid field exists and is > 0, then the existing table row for the class instance is
 * updated. The oid field should be thought of as read-only unless you want to set it to 0
 * so that a new instance is created in the database rather than doing an update.<p>
 * </ul>
 *
 * Suggested usage:
 * <ol>
 * <li>Create your javaclass and include an int oid = 0; field.
 * <li>Run Serialize.create( conn, javaclass ) to create the table for javaclass (once).
 * <li>Create mytable in the database with fields like: jclassoid INTEGER, jclass JAVACLASS<br>
 * <li>Use a jdbc2.PreparedStatement to insert, update, or select from mytable.
 * Use setObject(2, jclass), followed by setInt(1, jclass.oid) to setup an insert.
 * <li>Use jclass.oid and jclassoid to do queries since the jclass field cannot be used
 * for anything but fetching the javaclass instance with getObject("jclass").
 * </ol>
 * Finally, Serialize is a work in progress and being a utility class, it is not supported.
 * You are "on your own" if you use it. If you use it and make any enhancements,
 * please consider joining the email lists pgsql-jdbc@postgresql.org and pgsql-patches@postgresql.org
 * and contributing your additions.
 */
public class Serialize
{
  // This is the connection that the instance refers to
  protected org.postgresql.Connection conn;

  // This is the table name
  protected String tableName;

  // This is the class name
  protected String className;

  // This is the Class for this serialzed object
  protected Class ourClass;

  /**
   * This creates an instance that can be used to serialize or deserialize
   * a Java object from a PostgreSQL table.
   */
  public Serialize(org.postgresql.Connection c,String type) throws SQLException
  {
    try {
      conn = c;
		DriverManager.println("Serialize: initializing instance for type: " + type);
      tableName = toPostgreSQL(type);
      className = type;
      ourClass = Class.forName(className);
    } catch(ClassNotFoundException cnfe) {
		DriverManager.println("Serialize: " + className + " java class not found");
      throw new PSQLException("postgresql.serial.noclass",type);
    }

    // Second check, the type must be a table
    boolean status = false;
    ResultSet rs = conn.ExecSQL("select typname from pg_type,pg_class where typname=relname and typname='" + tableName + "'");
    if(rs!=null) {
      if(rs.next()) {
			status = true;
			DriverManager.println("Serialize: " + tableName + " table found");
		}
      rs.close();
	}
    // This should never occur, as org.postgresql has it's own internal checks
	if(!status) {
		DriverManager.println("Serialize: " + tableName + " table not found");
      throw new PSQLException("postgresql.serial.table",type);
	}
    // Finally cache the fields within the table
  }

  /**
   * Constructor when Object is passed in
   */
  public Serialize(org.postgresql.Connection c,Object o) throws SQLException
  {
    this(c, o.getClass().getName());
  }

  /**
   * Constructor when Class is passed in
   */
  public Serialize(org.postgresql.Connection c, Class cls) throws SQLException
  {
    this(c, cls.getName());
  }

  /**
   * This fetches an object from a table, given it's OID
   * @param oid The oid of the object
   * @return Object relating to oid
   * @exception SQLException on error
   */
	public Object fetch(int oid) throws SQLException
	{
		try {
			DriverManager.println("Serialize.fetch: " + "attempting to instantiate object of type: " + ourClass.getName() );
      	Object obj = ourClass.newInstance();
			DriverManager.println("Serialize.fetch: " + "instantiated object of type: " + ourClass.getName() );

      	// NB: we use java.lang.reflect here to prevent confusion with
      	// the org.postgresql.Field

      	// used getFields to get only public fields. We have no way to set values
      	// for other declarations. Maybe look for setFieldName() methods?
      	java.lang.reflect.Field f[] = ourClass.getFields();
      	boolean hasOID=false;
      	int oidFIELD=-1;

      	StringBuffer sb = new StringBuffer("select");
      	char sep=' ';
      	// build a select for the fields. Look for the oid field to use in the where
			for(int i=0;i<f.length;i++) {
				String n = f[i].getName();
				if(n.equals("oid")) {
		  			hasOID=true;
		  			oidFIELD=i;
				}
				sb.append(sep);
				sb.append(n);
				sep=',';
      	}
      	sb.append(" from ");
      	sb.append(tableName);
      	sb.append(" where oid=");
      	sb.append(oid);

      	DriverManager.println("Serialize.fetch: " + sb.toString());
      	ResultSet rs = conn.ExecSQL(sb.toString());

      	if(rs!=null) {
				if(rs.next()) {
					for(int i=0;i<f.length;i++) {
            		if( !Modifier.isFinal(f[i].getModifiers()) ) {
							if( f[i].getType().getName().equals("short") )
								f[i].setShort(obj, rs.getShort(i+1));
							else if( f[i].getType().getName().equals("char") )
								f[i].setChar(obj, rs.getString(i+1).toCharArray()[0]);
							else if( f[i].getType().getName().equals("byte"))
                			f[i].setByte(obj, rs.getByte(i+1));
							else if( f[i].getType().getName().equals("boolean") ) {
								// booleans come out of pgsql as a t or an f
								if( rs.getString(i+1).equals("t") ) f[i].setBoolean(obj, true);
								else f[i].setBoolean(obj, false);
							} else f[i].set(obj,rs.getObject(i+1));
						}
					}
				}
				rs.close();
			} else throw new PSQLException("postgresql.unexpected");

      	return obj;

		} catch(IllegalAccessException iae) {
			throw new SQLException(iae.toString());
		} catch(InstantiationException ie) {
			throw new SQLException(ie.toString());
		}
	}

  /**
   * This stores an object into a table, returning it's OID.<p>
   *
   * If the object has an int called OID, and it is > 0, then
   * that value is used for the OID, and the table will be updated.
   * If the value of OID is 0, then a new row will be created, and the
   * value of OID will be set in the object. This enables an object's
   * value in the database to be updateable.
   *
   * If the object has no int called OID, then the object is stored. However
   * if the object is later retrieved, amended and stored again, it's new
   * state will be appended to the table, and will not overwrite the old
   * entries.
   *
   * @param o Object to store (must implement Serializable)
   * @return oid of stored object
   * @exception SQLException on error
   */
	public int store(Object o) throws SQLException
	{
		try {
      	// NB: we use java.lang.reflect here to prevent confusion with
			// the org.postgresql.Field

			// don't save private fields since we would not be able to fetch them
			java.lang.reflect.Field f[] = ourClass.getFields();

			boolean hasOID=false;
			int oidFIELD=-1;
			boolean update=false;

			// Find out if we have an oid value
			for(int i=0;i<f.length;i++) {
				String n = f[i].getName();
				if(n.equals("oid")) {
					hasOID=true;
					oidFIELD=i;
					// Do update if oid != 0
					update = f[i].getInt(o) > 0;
				}
			}

			StringBuffer sb = new StringBuffer(update?"update "+tableName+" set":"insert into "+tableName+" ");
			char sep=update?' ':'(';
			for(int i=0;i<f.length;i++) {
				String n = f[i].getName();
				// oid cannot be updated!
				if( n.equals("oid") ) continue;
				sb.append(sep);
				sep=',';
				sb.append(n);
				if(update) {
					sb.append('=');
					// handle unset values
					if (f[i].get(o) == null)
						sb.append("null");
					else if(
					   f[i].getType().getName().equals("java.lang.String")
					|| f[i].getType().getName().equals("char") ) {
						sb.append('\'');
						// don't allow single qoutes or newlines in the string
						sb.append(fixString(f[i].get(o).toString()));
						sb.append('\'');
					} else sb.append(f[i].get(o).toString());
				}
			}

			if(update) sb.append(" where oid = " + f[oidFIELD].getInt(o) );

			if(!update) {
				sb.append(") values ");
				sep='(';
				for(int i=0;i<f.length;i++) {
					String n = f[i].getName();
					// oid cannot be set!
					if( n.equals("oid") ) continue;
					sb.append(sep);
					sep=',';
					// handle unset values
					if (f[i].get(o) == null) sb.append("null");
					else if(
					   f[i].getType().getName().equals("java.lang.String")
					|| f[i].getType().getName().equals("char")) {
						sb.append('\'');
						// don't allow single quotes or newlines in the string
						sb.append(fixString(f[i].get(o).toString()));
						sb.append('\'');
					} else sb.append(f[i].get(o).toString());
				}
				sb.append(')');
			}

			DriverManager.println("Serialize.store: " + sb.toString() );
			org.postgresql.ResultSet rs = (org.postgresql.ResultSet) conn.ExecSQL(sb.toString());

			// fetch the OID for returning
			if(update) {
				// object has oid already, so return it
				if(rs!=null) rs.close();
				return f[oidFIELD].getInt(o);
			} else {
				// new record inserted has new oid; rs should be not null
				int newOID = ((org.postgresql.ResultSet)rs).getInsertedOID();
				rs.close();
				// update the java object's oid field if it has the oid field
				if(hasOID) f[oidFIELD].setInt(o,newOID);
				// new object stored, return newly inserted oid
				return newOID;
			}

		} catch(IllegalAccessException iae) {
			throw new SQLException(iae.toString());
    	}
	}

	/**
	* Escape literal single quote and backslashes embedded in strings/chars.
	* Otherwise, postgres will bomb on the single quote and remove the
	* the backslashes.
	*/
	private String fixString(String s) {
		int idx = -1;

		// handle null
     if (s == null)
       return "";

     // if the string has single quotes in it escape them as ''
     if ((idx = s.indexOf("'")) > -1) {
       StringBuffer buf = new StringBuffer();
       StringTokenizer tok = new StringTokenizer(s, "'");
       // handle quote as 1St charater
       if (idx > 0) buf.append(tok.nextToken());

       while(tok.hasMoreTokens())
         buf.append("''").append(tok.nextToken());

       s = buf.toString();
     }

     // if the string has backslashes in it escape them them as \\
     if ((idx = s.indexOf("\\")) > -1) {
       StringBuffer buf = new StringBuffer();
       StringTokenizer tok = new StringTokenizer(s, "\\");
       if (idx > 0) buf.append(tok.nextToken());

       while(tok.hasMoreTokens())
         buf.append("\\\\").append(tok.nextToken());

       s = buf.toString();
     }

     return s;
   }

  /**
   * This method is not used by the driver, but it creates a table, given
   * a Serializable Java Object. It should be used before serializing any
   * objects.
   * @param c Connection to database
   * @param o Object to base table on
   * @exception SQLException on error
   */
  public static void create(org.postgresql.Connection con,Object o) throws SQLException
  {
    create(con,o.getClass());
  }

  /**
   * This method is not used by the driver, but it creates a table, given
   * a Serializable Java Object. It should be used before serializing any
   * objects.
   * @param c Connection to database
   * @param o Class to base table on
   * @exception SQLException on error
   */
	public static void create(org.postgresql.Connection con,Class c) throws SQLException
	{
		if(c.isInterface()) throw new PSQLException("postgresql.serial.interface");

		// See if the table exists
		String tableName = toPostgreSQL(c.getName());

		ResultSet rs = con.ExecSQL("select relname from pg_class where relname = '"+tableName+"'");
		if( rs.next() ) {
 			DriverManager.println("Serialize.create: table "+tableName+" exists, skipping");
			rs.close();
			return;
		}

		// else table not found, so create it
		DriverManager.println("Serialize.create: table " + tableName + " not found, creating" );
     	// No entries returned, so the table doesn't exist

     	StringBuffer sb = new StringBuffer("create table ");
     	sb.append(tableName);
     	char sep='(';

		// java.lang.reflect.Field[] fields = c.getDeclaredFields();
		// Only store public fields, another limitation!
     	java.lang.reflect.Field[] fields = c.getFields();
     	for(int i=0;i<fields.length;i++) {
			Class type = fields[i].getType();
			// oid is a special field
			if(!fields[i].getName().equals("oid")) {
				sb.append(sep);
				sb.append(fields[i].getName());
				sb.append(' ');
				sep=',';

				if(type.isArray()) {
					// array handling
				} else {
					// convert the java type to org.postgresql, recursing if a class
					// is found
					String n = type.getName();
					int j=0;
					for(;j<tp.length && !tp[j][0].equals(n);j++);
					if(j<tp.length) sb.append(tp[j][1]);
					else {
						create(con, type);
						sb.append(toPostgreSQL(n));
					}
				}
			}
		}
		sb.append(")");

		// Now create the table
		DriverManager.println("Serialize.create: " + sb );
		con.ExecSQL(sb.toString());
	}

  // This is used to translate between Java primitives and PostgreSQL types.
  private static final String tp[][] = {
//    {"boolean",			"int1"},
    {"boolean",			"bool"},
    {"double",			"float8"},
    {"float",			"float4"},
    {"int",			"int4"},
//    {"long",			"int4"},
    {"long",			"int8"},
    {"short",			"int2"},
    {"java.lang.String",	"text"},
    {"java.lang.Integer",	"int4"},
    {"java.lang.Float",		"float4"},
    {"java.lang.Double",	"float8"},
    {"java.lang.Short",		"int2"},
    {"char",                    "char"},
    {"byte",                    "int2"}
  };

  /**
   * This converts a Java Class name to a org.postgresql table, by replacing . with
   * _<p>
   *
   * Because of this, a Class name may not have _ in the name.<p>
   * Another limitation, is that the entire class name (including packages)
   * cannot be longer than 32 characters (a limit forced by PostgreSQL).
   *
   * @param name Class name
   * @return PostgreSQL table name
   * @exception SQLException on error
   */
  public static String toPostgreSQL(String name) throws SQLException
  {
    name = name.toLowerCase();

    if(name.indexOf("_")>-1)
      throw new PSQLException("postgresql.serial.underscore");

    // Postgres table names can only be 32 character long.
	 // Reserve 1 char, so allow only up to 31 chars.
    // If the full class name with package is too long
    // then just use the class name. If the class name is
    // too long throw an exception.
	 //
    if( name.length() > 31 ) {
      name = name.substring(name.lastIndexOf(".") + 1);
      if( name.length() >31 )
        throw new PSQLException("postgresql.serial.namelength",name,new Integer(name.length()));
    }
    return name.replace('.','_');
  }


  /**
   * This converts a org.postgresql table to a Java Class name, by replacing _ with
   * .<p>
   *
   * @param name PostgreSQL table name
   * @return Class name
   * @exception SQLException on error
   */
  public static String toClassName(String name) throws SQLException
  {
    name = name.toLowerCase();
    return name.replace('_','.');
  }

}
