diff --git a/src/java/com/samskivert/Log.java b/src/java/com/samskivert/Log.java
index 43aa9f6b..600ce178 100644
--- a/src/java/com/samskivert/Log.java
+++ b/src/java/com/samskivert/Log.java
@@ -1,8 +1,8 @@
//
-// $Id: Log.java,v 1.3 2001/08/12 01:34:30 mdb Exp $
+// $Id$
//
// samskivert library - useful routines for java programs
-// Copyright (C) 2001 Michael Bayne
+// Copyright (C) 2001-2006 Michael Bayne
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
@@ -20,35 +20,21 @@
package com.samskivert;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
/**
- * A placeholder class that contains a reference to the log object used by
- * the samskivert package. This is a useful pattern to use when using the
- * samskivert logging facilities. One creates a top-level class like this
- * one that instantiates a log object with an name that identifies log
- * messages from that package and then provides static methods that
- * generate log messages using that instance. Then, classes in that
- * package need only import the log wrapper class and can easily use it to
- * generate log messages. For example:
- *
- *
- * import com.samskivert.Log;
- * // ...
- * Log.warning("All hell is breaking loose!");
- * // ...
- *
- *
- * @see com.samskivert.util.Log
+ * Contains a reference to the log object used by the samskivert package.
*/
public class Log
{
- /** The static log instance configured for use by this package. */
- public static com.samskivert.util.Log log =
- new com.samskivert.util.Log("samskivert");
+ /** We dispatch our log messages through this logger. */
+ public static Logger log = Logger.getLogger("com.samskivert");
/** Convenience function. */
public static void debug (String message)
{
- log.debug(message);
+ log.fine(message);
}
/** Convenience function. */
@@ -66,6 +52,6 @@ public class Log
/** Convenience function. */
public static void logStackTrace (Throwable t)
{
- log.logStackTrace(com.samskivert.util.Log.WARNING, t);
+ log.log(Level.WARNING, t.getMessage(), t);
}
}
diff --git a/src/java/com/samskivert/jdbc/JDBCUtil.java b/src/java/com/samskivert/jdbc/JDBCUtil.java
index 6ec6bb9b..739a59fd 100644
--- a/src/java/com/samskivert/jdbc/JDBCUtil.java
+++ b/src/java/com/samskivert/jdbc/JDBCUtil.java
@@ -293,7 +293,7 @@ public class JDBCUtil
Statement stmt = conn.createStatement();
try {
stmt.executeUpdate("create table " + table + "(" +
- StringUtil.join(definition, ",") + ") " +
+ StringUtil.join(definition, ", ") + ") " +
postamble);
} finally {
close(stmt);
diff --git a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java
index 150c93dd..0c80b4ad 100644
--- a/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java
+++ b/src/java/com/samskivert/jdbc/depot/DepotMarshaller.java
@@ -26,20 +26,32 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.logging.Level;
import javax.persistence.Id;
+import javax.persistence.Transient;
+import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.util.StringUtil;
+import static com.samskivert.Log.log;
+
/**
* Handles the marshalling and unmarshalling of persistent instances to JDBC
* primitives ({@link PreparedStatement} and {@link ResultSet}).
*/
public class DepotMarshaller
{
+ /** The name of the private static field that must be defined for all
+ * persistent object classes which is used to handle schema migration. If
+ * automatic schema migration is not desired, define this field and set its
+ * value to -1. */
+ public static final String SCHEMA_VERSION_FIELD = "SCHEMA_VERSION";
+
/**
* Creates a marshaller for the specified persistent object class.
*/
@@ -54,11 +66,23 @@ public class DepotMarshaller
// introspect on the class and create marshallers for persistent fields
ArrayList fields = new ArrayList();
for (Field field : _pclass.getFields()) {
- // the field must be public, non-static and non-transient
int mods = field.getModifiers();
+
+ // check for a static constant schema version
+ if ((mods & Modifier.STATIC) != 0 &&
+ field.getName().equals(SCHEMA_VERSION_FIELD)) {
+ try {
+ _schemaVersion = (Integer)field.get(null);
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Failed to read schema version " +
+ "[class=" + _pclass + "].", e);
+ }
+ }
+
+ // the field must be public, non-static and non-transient
if (((mods & Modifier.PUBLIC) == 0) ||
((mods & Modifier.STATIC) != 0) ||
- ((mods & Modifier.TRANSIENT) != 0)) {
+ field.getAnnotation(Transient.class) != null) {
continue;
}
@@ -76,6 +100,20 @@ public class DepotMarshaller
// generate our full list of fields/columns for use in queries
_allFields = fields.toArray(new String[fields.size()]);
_fullColumnList = StringUtil.join(_allFields, ",");
+
+ // create the SQL used to create and migrate our table
+ _columnDefinitions = new String[_allFields.length];
+ for (int ii = 0; ii < _allFields.length; ii++) {
+ _columnDefinitions[ii] =
+ _fields.get(_allFields[ii]).getColumnDefinition();
+ }
+ _postamble = ""; // TODO: add annotations for the postamble
+
+ // if we did not find a schema version field, complain
+ if (_schemaVersion < 0) {
+ log.warning("Unable to read " + _pclass.getName() + "." +
+ SCHEMA_VERSION_FIELD + ". Schema migration disabled.");
+ }
}
/**
@@ -116,6 +154,44 @@ public class DepotMarshaller
return new DepotRepository.Key(_primaryKey.getColumnName(), value);
}
+ /**
+ * Initializes the table used by this marshaller. If the table does not
+ * exist, it will be created. If the schema version specified by the
+ * persistent object is newer than the database schema, it will be
+ * migrated.
+ */
+ public void init (Connection conn)
+ throws SQLException
+ {
+ // check to see if our schema version table exists, create it if not
+ JDBCUtil.createTableIfMissing(conn, SCHEMA_VERSION_TABLE,
+ new String[] { "persistentClass VARCHAR(255) NOT NULL",
+ "version INTEGER NOT NULL" },
+ "");
+
+ // now create the table for our persistent class if it does not exist
+ if (!JDBCUtil.tableExists(conn, getTableName())) {
+ JDBCUtil.createTableIfMissing(
+ conn, getTableName(), _columnDefinitions, _postamble);
+ // TODO: insert current version into version table
+ return;
+ }
+
+ // if schema versioning is disabled, stop now
+ if (_schemaVersion < 0) {
+ return;
+ }
+
+ // otherwise, then make sure the versions match and do some migration
+ // if they don't
+ Statement stmt = conn.createStatement();
+ try {
+ // TODO: magical migration; execute registered migration actions
+ } finally {
+ stmt.close();
+ }
+ }
+
/**
* Creates a query for instances of this persistent object type using the
* supplied key.
@@ -195,6 +271,49 @@ public class DepotMarshaller
}
}
+ /**
+ * Fills in the primary key just assigned to the supplied persistence
+ * object by the execution of the results of {@link #createInsert}. The
+ * structure of primary key assignment will probably have to change when we
+ * support other databases.
+ */
+ public void assignPrimaryKey (Connection conn, Object po)
+ throws SQLException
+ {
+ // no primary key, no problem!
+ if (_primaryKey == null) {
+ return;
+ }
+
+ // if the primary key is non-numeric, we can't auto-assign it
+ Class> ftype = _primaryKey.getField().getType();
+ if (!ftype.equals(Byte.TYPE) && !ftype.equals(Byte.class) &&
+ !ftype.equals(Short.TYPE) && !ftype.equals(Short.class) &&
+ !ftype.equals(Integer.TYPE) && !ftype.equals(Integer.class) &&
+ !ftype.equals(Long.TYPE) && !ftype.equals(Long.class)) {
+ return;
+ }
+
+ // load up the last inserted ID mysql style; eventually this will be
+ // fancier and pluggable
+ Statement stmt = null;
+ try {
+ stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()");
+ if (rs.next()) {
+ _primaryKey.getField().set(po, rs.getInt(1));
+ }
+
+ } catch (IllegalAccessException iae) {
+ String errmsg = "Failed to assign primary key " +
+ "[type=" + _pclass + "]";
+ throw (SQLException)new SQLException(errmsg).initCause(iae);
+
+ } finally {
+ JDBCUtil.close(stmt);
+ }
+ }
+
/**
* Creates a statement that will update the supplied persistent object
* using the supplied key.
@@ -320,12 +439,37 @@ public class DepotMarshaller
return pstmt;
}
+ /** The persistent object class that we manage. */
protected Class _pclass;
+
+ /** A field marshaller for each persistent field in our object. */
protected HashMap _fields =
new HashMap();
- protected String _tableName;
+ /** The field marshaller for our persistent object's primary key or null if
+ * it did not define a primary key. */
protected FieldMarshaller _primaryKey;
+
+ /** The name of our persistent object table. */
+ protected String _tableName;
+
+ /** The persisent fields of our object, in definition order, separated by
+ * commas for easy use in a select statement. */
protected String _fullColumnList;
+
+ /** The persisent fields of our object, in definition order. */
protected String[] _allFields;
+
+ /** The version of our persistent object schema as specified in the class
+ * definition. */
+ protected int _schemaVersion = -1;
+
+ /** Used when creating and migrating our table schema. */
+ protected String[] _columnDefinitions;
+
+ /** Used when creating and migrating our table schema. */
+ protected String _postamble;
+
+ /** The name of the table we use to track schema versions. */
+ protected static final String SCHEMA_VERSION_TABLE = "DepotSchemaVersion";
}
diff --git a/src/java/com/samskivert/jdbc/depot/DepotRepository.java b/src/java/com/samskivert/jdbc/depot/DepotRepository.java
index d326869b..bb7f5206 100644
--- a/src/java/com/samskivert/jdbc/depot/DepotRepository.java
+++ b/src/java/com/samskivert/jdbc/depot/DepotRepository.java
@@ -59,9 +59,7 @@ public class DepotRepository
{
final DepotMarshaller marsh = getMarshaller(type);
return invoke(new Query(key) {
- public T invoke (Connection conn)
- throws SQLException, PersistenceException
- {
+ public T invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createQuery(conn, _key);
try {
T result = null;
@@ -89,9 +87,7 @@ public class DepotRepository
{
final DepotMarshaller marsh = getMarshaller(type);
return invoke(new Query>(key) {
- public ArrayList invoke (Connection conn)
- throws SQLException, PersistenceException
- {
+ public ArrayList invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createQuery(conn, _key);
try {
ArrayList results = new ArrayList();
@@ -131,13 +127,11 @@ public class DepotRepository
{
final DepotMarshaller marsh = getMarshaller(record.getClass());
return invoke(new Modifier(marsh.getPrimaryKey(record)) {
- public int invoke (Connection conn)
- throws SQLException
- {
+ public int invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createInsert(conn, record);
try {
int mods = stmt.executeUpdate();
- // TODO: assign primary key
+ marsh.assignPrimaryKey(conn, record);
return mods;
} finally {
stmt.close();
@@ -157,9 +151,7 @@ public class DepotRepository
{
final DepotMarshaller marsh = getMarshaller(record.getClass());
return invoke(new Modifier(marsh.getPrimaryKey(record)) {
- public int invoke (Connection conn)
- throws SQLException
- {
+ public int invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createUpdate(conn, record, _key);
try {
return stmt.executeUpdate();
@@ -181,9 +173,7 @@ public class DepotRepository
{
final DepotMarshaller marsh = getMarshaller(record.getClass());
return invoke(new Modifier(marsh.getPrimaryKey(record)) {
- public int invoke (Connection conn)
- throws SQLException
- {
+ public int invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createUpdate(
conn, record, _key, modifiedFields);
try {
@@ -239,9 +229,7 @@ public class DepotRepository
final DepotMarshaller marsh = getMarshaller(type);
return invoke(new Modifier(key) {
- public int invoke (Connection conn)
- throws SQLException
- {
+ public int invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createPartialUpdate(
conn, _key, fields, values);
try {
@@ -313,9 +301,7 @@ public class DepotRepository
final DepotMarshaller marsh = getMarshaller(type);
return invoke(new Modifier(key) {
- public int invoke (Connection conn)
- throws SQLException
- {
+ public int invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createLiteralUpdate(
conn, _key, fields, values);
try {
@@ -341,9 +327,7 @@ public class DepotRepository
{
final DepotMarshaller marsh = getMarshaller(record.getClass());
return invoke(new Modifier(marsh.getPrimaryKey(record)) {
- public int invoke (Connection conn)
- throws SQLException
- {
+ public int invoke (Connection conn) throws SQLException {
PreparedStatement stmt = null;
try {
// if our primary key is null or is the integer 0, assume
@@ -360,7 +344,9 @@ public class DepotRepository
// if the update modified zero rows or the primary key was
// obviously unset, do an insertion
stmt = marsh.createInsert(conn, record);
- return stmt.executeUpdate();
+ int mods = stmt.executeUpdate();
+ marsh.assignPrimaryKey(conn, record);
+ return mods;
} finally {
stmt.close();
@@ -407,9 +393,7 @@ public class DepotRepository
{
final DepotMarshaller marsh = getMarshaller(type);
return invoke(new Modifier(key) {
- public int invoke (Connection conn)
- throws SQLException
- {
+ public int invoke (Connection conn) throws SQLException {
PreparedStatement stmt = marsh.createDelete(conn, _key);
try {
return stmt.executeUpdate();
@@ -421,11 +405,21 @@ public class DepotRepository
}
protected DepotMarshaller getMarshaller (Class type)
+ throws PersistenceException
{
@SuppressWarnings("unchecked")DepotMarshaller marshaller =
(DepotMarshaller)_marshallers.get(type);
if (marshaller == null) {
_marshallers.put(type, marshaller = new DepotMarshaller(type));
+ // initialize the marshaller which may create or migrate the table
+ // for its underlying persistent object
+ final DepotMarshaller fm = marshaller;
+ invoke(new Modifier(null) {
+ public int invoke (Connection conn) throws SQLException {
+ fm.init(conn);
+ return 0;
+ }
+ });
}
return marshaller;
}
@@ -530,8 +524,7 @@ public class DepotRepository
return _key;
}
- public abstract T invoke (Connection conn)
- throws SQLException, PersistenceException;
+ public abstract T invoke (Connection conn) throws SQLException;
protected Query (Key key)
{
@@ -543,8 +536,7 @@ public class DepotRepository
protected static abstract class CollectionQuery
{
- public abstract T invoke (Connection conn)
- throws SQLException, PersistenceException;
+ public abstract T invoke (Connection conn) throws SQLException;
}
protected static abstract class Modifier
@@ -554,8 +546,7 @@ public class DepotRepository
return _key;
}
- public abstract int invoke (Connection conn)
- throws SQLException, PersistenceException;
+ public abstract int invoke (Connection conn) throws SQLException;
protected Modifier (Key key)
{
diff --git a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java
index b34c8d01..17fa10af 100644
--- a/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java
+++ b/src/java/com/samskivert/jdbc/depot/FieldMarshaller.java
@@ -29,6 +29,13 @@ import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
+import javax.persistence.Column;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+
+import com.samskivert.util.StringUtil;
+
/**
* Handles the marshalling and unmarshalling of a particular field of a
* persistent object.
@@ -91,6 +98,30 @@ public abstract class FieldMarshaller
return marshaller;
}
+ /**
+ * Returns the {@link Field} handled by this marshaller.
+ */
+ public Field getField ()
+ {
+ return _field;
+ }
+
+ /**
+ * Returns the name of the table column used to store this field.
+ */
+ public String getColumnName ()
+ {
+ return _columnName;
+ }
+
+ /**
+ * Returns the SQL used to define this field's column.
+ */
+ public String getColumnDefinition ()
+ {
+ return _columnDefinition;
+ }
+
/**
* Reads the value of our field from the persistent object and sets that
* value into the specified column of the supplied prepared statement.
@@ -106,24 +137,82 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException;
/**
- * Returns the name of the table column used to store this field.
+ * Returns the type used in the SQL column definition for this field.
*/
- public String getColumnName ()
- {
- return _field.getName();
- }
-
- /**
- * Returns the {@link Field} handled by this marshaller.
- */
- public Field getField ()
- {
- return _field;
- }
+ public abstract String getColumnType ();
protected void init (Field field)
{
_field = field;
+ _columnName = field.getName();
+
+ // read our column metadata from the annotation (if it exists);
+ // annoyingly we can't create a Column instance to read the defaults so
+ // we have to duplicate them here
+ int length = 255;
+ boolean nullable = true;
+ boolean unique = false;
+ Column column = _field.getAnnotation(Column.class);
+ if (column != null) {
+ nullable = column.nullable();
+ unique = column.unique();
+ length = column.length();
+ if (!StringUtil.isBlank(column.name())) {
+ _columnName = column.name();
+ }
+ }
+
+ // create our SQL column definition
+ StringBuilder builder = new StringBuilder();
+ if (column != null && !StringUtil.isBlank(column.columnDefinition())) {
+ builder.append(column.columnDefinition());
+
+ } else {
+ builder.append(getColumnName());
+ String type = getColumnType();
+ builder.append(" ").append(type);
+
+ // if this is a VARCHAR field, add the length
+ if (type.equals("VARCHAR")) {
+ builder.append("(").append(length).append(")");
+ }
+
+ // TODO: handle precision and scale
+
+ // handle nullability and uniqueness
+ if (!nullable) {
+ builder.append(" NOT NULL");
+ }
+ if (unique) {
+ builder.append(" UNIQUE");
+ }
+ }
+
+ // handle primary keyness
+ if (field.getAnnotation(Id.class) != null) {
+ builder.append(" PRIMARY KEY");
+
+ // figure out how we're going to generate our primary key values
+ GeneratedValue gv = field.getAnnotation(GeneratedValue.class);
+ GenerationType strat = GenerationType.AUTO;
+ if (gv != null) {
+ strat = gv.strategy();
+ }
+ switch (strat) {
+ case AUTO:
+ case IDENTITY:
+ builder.append(" AUTO_INCREMENT");
+ break;
+ case SEQUENCE: // TODO
+ throw new IllegalArgumentException(
+ "TABLE key generation strategy not yet supported.");
+ case TABLE: // TODO
+ throw new IllegalArgumentException(
+ "TABLE key generation strategy not yet supported.");
+ }
+ }
+
+ _columnDefinition = builder.toString();
}
protected static class PBooleanMarshaller extends FieldMarshaller {
@@ -135,6 +224,9 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.setBoolean(po, rs.getBoolean(getColumnName()));
}
+ public String getColumnType () {
+ return "TINYINT";
+ }
}
protected static class PByteMarshaller extends FieldMarshaller {
@@ -146,6 +238,9 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.setByte(po, rs.getByte(getColumnName()));
}
+ public String getColumnType () {
+ return "TINYINT";
+ }
}
protected static class PShortMarshaller extends FieldMarshaller {
@@ -157,6 +252,9 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.setShort(po, rs.getShort(getColumnName()));
}
+ public String getColumnType () {
+ return "SMALLINT";
+ }
}
protected static class PIntMarshaller extends FieldMarshaller {
@@ -168,6 +266,9 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.setInt(po, rs.getInt(getColumnName()));
}
+ public String getColumnType () {
+ return "INTEGER";
+ }
}
protected static class PLongMarshaller extends FieldMarshaller {
@@ -179,6 +280,9 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.setLong(po, rs.getLong(getColumnName()));
}
+ public String getColumnType () {
+ return "BIGINT";
+ }
}
protected static class PFloatMarshaller extends FieldMarshaller {
@@ -190,6 +294,9 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.setFloat(po, rs.getFloat(getColumnName()));
}
+ public String getColumnType () {
+ return "FLOAT";
+ }
}
protected static class PDoubleMarshaller extends FieldMarshaller {
@@ -201,6 +308,9 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.setDouble(po, rs.getDouble(getColumnName()));
}
+ public String getColumnType () {
+ return "DOUBLE";
+ }
}
protected static class ObjectMarshaller extends FieldMarshaller {
@@ -212,7 +322,39 @@ public abstract class FieldMarshaller
throws SQLException, IllegalAccessException {
_field.set(po, rs.getObject(getColumnName()));
}
+ public String getColumnType () {
+ Class> ftype = _field.getType();
+ if (ftype.equals(Byte.class)) {
+ return "TINYINT";
+ } else if (ftype.equals(Short.class)) {
+ return "SMALLINT";
+ } else if (ftype.equals(Integer.class)) {
+ return "INTEGER";
+ } else if (ftype.equals(Long.class)) {
+ return "BIGINT";
+ } else if (ftype.equals(Float.class)) {
+ return "FLOAT";
+ } else if (ftype.equals(Double.class)) {
+ return "DOUBLE";
+ } else if (ftype.equals(String.class)) {
+ return "VARCHAR";
+ } else if (ftype.equals(Date.class)) {
+ return "DATE";
+ } else if (ftype.equals(Time.class)) {
+ return "DATETIME";
+ } else if (ftype.equals(Timestamp.class)) {
+ return "TIMESTAMP";
+ } else if (ftype.equals(Blob.class)) {
+ return "BLOB";
+ } else if (ftype.equals(Clob.class)) {
+ return "CLOB";
+ } else {
+ throw new IllegalArgumentException(
+ "Don't know how to create SQL for " + ftype + ".");
+ }
+ }
}
protected Field _field;
+ protected String _columnName, _columnDefinition;
}
diff --git a/tests/src/java/com/samskivert/jdbc/depot/TestRecord.java b/tests/src/java/com/samskivert/jdbc/depot/TestRecord.java
index f3ac6f3b..7c14edfc 100644
--- a/tests/src/java/com/samskivert/jdbc/depot/TestRecord.java
+++ b/tests/src/java/com/samskivert/jdbc/depot/TestRecord.java
@@ -15,15 +15,21 @@ import com.samskivert.util.StringUtil;
*/
public class TestRecord
{
+ public static final int SCHEMA_VERSION = 1;
+
@Id
public int recordId;
+ @Column(nullable=false)
public String name;
+ @Column(nullable=false)
public int age;
+ @Column(nullable=false)
public Date created;
+ @Column(nullable=false)
public Timestamp lastModified;
public String toString ()