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.
*
* 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.
*
* 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:
* CREATE TABLE myclass( var1 TEXT, var2 INTEGER );
* CREATE TABLE othertable( field1 TEXT, field2 myclass );
* INSERT INTO myclass VALUES ('Hello', 1);
* INSERT INTO othertable VALUES ('World', xxxx::myclass);
* where xxxx is the OID of a row in myclass
* 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.
*
* Queries like:
* SELECT othertable.field2.var1 FROM othertable;
* will not work but were suggested in the original Postgres
* design documents.
* 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:
* CREATE othertable( field1 TEXT, field2 myclass, myclassOID oid);
* 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.
*
* 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.
*
* There are a number of limitations placed on the java class to be
* used by Serialize:
*
* - 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.
*
- 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.
*
- 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.
*
- Only the public fields of the class will be stored in and fetched from
* the database. Protected and private fields are ignored.
*
- Must have a no-arg constructor so that Class.newInstance() may
* instantiate the class in fetch().
*
- 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.
*
- The class should contain a field defined as:
* int oid = 0;
* This field is actually optional and its use by jdbc2.PreparedStatement.setObject()
* is as follows:
* 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.
*
*
* Suggested usage:
*
* - Create your javaclass and include an int oid = 0; field.
*
- Run Serialize.create( conn, javaclass ) to create the table for javaclass (once).
*
- Create mytable in the database with fields like: jclassoid INTEGER, jclass JAVACLASS
* - 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.
*
- 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").
*
* 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
*
* 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 0;
}
}
StringBuffer sb = new StringBuffer(update?"update "+tableName+" set":"insert into "+tableName+" ");
char sep=update?' ':'(';
for(int i=0;i -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
*
* Because of this, a Class name may not have _ in the name.
* 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
* .
*
* @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('_','.');
}
}