package org.postgresql.util; import org.postgresql.Driver; 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: *
*
* This method was deprecated in 7.2 because the value of an OID * can be larger than a java signed int. * @deprecated Replaced by storeObject() in 7.2 */ public int store(Object o) throws SQLException { return (int) storeObject(o); } /* * This stores an object into a table, returning it's OID.
* * 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 * @since 7.2 */ public long storeObject(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(')'); } if (Driver.logDebug) Driver.debug("Serialize.store: " + sb.toString() ); ResultSet rs = ((org.postgresql.jdbc1.AbstractJdbc1Connection)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 long newOID = ((org.postgresql.jdbc1.AbstractJdbc1ResultSet)rs).getLastOID(); rs.close(); // update the java object's oid field if it has the oid field if (hasOID) f[oidFIELD].setLong(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(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(Connection conn, Class c) throws SQLException { if (c.isInterface()) throw new PSQLException("postgresql.serial.interface"); // See if the table exists String tableName = toPostgreSQL(conn,c.getName()); String sql; if (conn.getMetaData().supportsSchemasInTableDefinitions()) { sql = "SELECT 1 FROM pg_catalog.pg_class WHERE relkind='r' AND relname='" + tableName + "' AND pg_table_is_visible(oid) "; } else { sql = "SELECT 1 FROM pg_class WHERE relkind='r' AND relname='"+tableName+"'"; } ResultSet rs = conn.createStatement().executeQuery(sql); if ( rs.next() ) { if (Driver.logDebug) Driver.debug("Serialize.create: table " + tableName + " exists, skipping"); rs.close(); return ; } // else table not found, so create it if (Driver.logDebug) Driver.debug("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(conn, type); sb.append(toPostgreSQL(conn,n)); } } } } sb.append(")"); // Now create the table if (Driver.logDebug) Driver.debug("Serialize.create: " + sb ); conn.createStatement().executeUpdate(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 * _
* * 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 the maximum table name length. * * @param con The database connection * @param name Class name * @return PostgreSQL table name * @exception SQLException on error * @since 7.3 */ public static String toPostgreSQL(Connection con, String name) throws SQLException { DatabaseMetaData dbmd = con.getMetaData(); int maxNameLength = dbmd.getMaxTableNameLength(); return toPostgreSQL(maxNameLength,name); } /** * Convert a Java Class Name to an org.postgresql table name, by replacing . * with _
* * @deprecated Replaced by toPostgresql(connection, name) in 7.3 */ public static String toPostgreSQL(String name) throws SQLException { return toPostgreSQL(31,name); } private static String toPostgreSQL(int maxNameLength, String name) throws SQLException { name = name.toLowerCase(); if (name.indexOf("_") > -1) throw new PSQLException("postgresql.serial.underscore"); // Postgres table names can only be so many characters long. // 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() > maxNameLength ) { name = name.substring(name.lastIndexOf(".") + 1); if ( name.length() > maxNameLength ) 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('_', '.'); } }