Serious database services rethink. It's cleaner, more loosely coupled and

more extensible now. Whee!


git-svn-id: https://samskivert.googlecode.com/svn/trunk@323 6335cc39-0255-0410-8fd6-9bcaacd3b74c
This commit is contained in:
mdb
2001-09-20 01:53:20 +00:00
parent 418655feab
commit 0d5051e407
14 changed files with 974 additions and 449 deletions
@@ -0,0 +1,60 @@
//
// $Id: PersistenceException.java,v 1.1 2001/09/20 01:53:19 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import org.apache.commons.util.exception.NestableException;
/**
* A persistence exception can be thrown when an error occurs in the
* underlying persistence code. By encapsulating errors, one retains the
* ability to make changes to the implementation structure without
* affecting the interface to persistence services presented to the
* application.
*/
public class PersistenceException extends NestableException
{
/**
* Constructs a persistence exception with the specified error
* message.
*/
public PersistenceException (String message)
{
super(message);
}
/**
* Constructs a persistence exception with the specified error message
* and the chained causing event.
*/
public PersistenceException (String message, Exception cause)
{
super(message, cause);
}
/**
* Constructs a persistence exception with the specified chained
* causing event.
*/
public PersistenceException (Exception cause)
{
super(cause);
}
}
@@ -0,0 +1,84 @@
//
// $Id: ConnectionProvider.java,v 1.1 2001/09/20 01:53:19 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
/**
* As the repository aims to interface with whatever database pooling
* services a project cares to use, it obtains all of its database
* connections through a connection provider. The connection provider
* provides connections based on a database identifier (a string) which
* identifies a particular connection to a particular database. A user of
* these services would then coordinate the database identifier (or
* identifiers) used to invoke a database operation with the database
* connections that are returned by the connection provider that they
* provided to the repository at construct time.
*/
public interface ConnectionProvider
{
/**
* Obtains a database connection based on the supplied database
* identifier. The repository expects to have exclusive use of this
* connection instance until it releases it. This connection will be
* released subsequently with a call to {@link #releaseConnection} or
* {@link #connectionFailed} depending on the circumstances of the
* release. <code>close()</code> <em>will not</em> be called on the
* connection. It is up to the connection provider to close the
* connection when it is released if appropriate.
*
* @param ident the database connection identifier.
*
* @return an active JDBC connection (which may have come from a
* connection pool).
*
* @exception PersistenceException thrown if a problem occurs trying
* to open the requested connection.
*/
public Connection getConnection (String ident)
throws PersistenceException;
/**
* Releases a database connection when it is no longer needed by the
* repository.
*
* @param ident the database identifier used when obtaining this
* connection.
* @param conn the connection to release (back into the pool or to be
* closed if pooling is not going on).
*/
public void releaseConnection (String ident, Connection conn);
/**
* Called by the repository if a failure occurred on the connection.
* It is expected that the connection will be disposed of and
* subsequent calls to <code>getConnection</code> will return a
* freshly established connection to the database.
*
* @param ident the database identifier used when obtaining this
* connection.
* @param conn the connection that failed.
* @param error the error thrown by the connection (which may be used
* to determine if the connection should be closed or can be reused).
*/
public void connectionFailed (String ident, Connection conn,
SQLException error);
}
@@ -0,0 +1,74 @@
//
// $Id: DatabaseLiaison.java,v 1.1 2001/09/20 01:53:19 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Despite good intentions, JDBC and SQL do not provide a unified
* interface to all databases. There remain idiosyncrasies that must be
* worked around when making code interact with different database
* servers. The database liaison encapsulates the code needed to
* straighten out the curves and curve out the straights.
*/
public interface DatabaseLiaison
{
/**
* Indicates whether this particular RDBMS/JDBC driver combination
* supports transactions.
*
* @return true if transactions are supported, false if not.
*/
public boolean supportsTransactions ();
/**
* Determines whether or not the supplied SQL exception was caused by
* a duplicate row being inserted into a table with a unique key.
*
* @return true if the exception was caused by the insertion of a
* duplicate row, false if not.
*/
public boolean isDuplicateRowException (SQLException sqe);
/**
* Determines whether or not the supplied SQL exception is a transient
* failure, meaning one that is not related to the SQL being executed,
* but instead to the environment at the time of execution, like the
* connection to the database having been lost.
*
* @return true if the exception was thrown due to a transient
* failure, false if not.
*/
public boolean isTransientException (SQLException sqe);
/**
* Returns the value of an <code>AUTO_INCREMENT</code> column for the
* last row insertion. This is MySQL specific, but exists here for now
* until we sort out a general purpose mechanism for assigning unique
* ids.
*
* @return the most recent ID generated by an insert into an
* <code>AUTO_INCREMENT</code> table or -1 if it could not be
* obtained.
*/
public int lastInsertedId (Connection conn) throws SQLException;
}
@@ -0,0 +1,73 @@
//
// $Id: JORARepository.java,v 1.1 2001/09/20 01:53:19 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import java.util.Properties;
import com.samskivert.jdbc.jora.*;
/**
* The JORA repository simplifies the process of building persistence
* services that make use of the JORA object relational mapping package.
*
* @see com.samskivert.jdbc.jora.Session
*/
public abstract class JORARepository extends SimpleRepository
{
/**
* Creates and initializes a JORA repository which will access the
* database identified by the supplied database identifier.
*
* @param provider the connection provider which will be used to
* obtain our database connection.
* @param dbident the identifier of the database that will be accessed
* by this repository.
*/
public JORARepository (ConnectionProvider provider, String dbident)
{
super(provider, dbident);
// create our JORA session
_session = new Session((Connection)null);
// create our tables
createTables(_session);
}
/**
* After the database session is begun, this function will be called
* to give the repository implementation the opportunity to create its
* table objects.
*
* @param session the session instance to use when creating your table
* instances.
*/
protected abstract void createTables (Session session);
protected void gotConnection (Connection conn)
{
// let our session know about this connection
_session.setConnection(conn);
}
protected Session _session;
}
@@ -0,0 +1,39 @@
//
// $Id: LiaisonRegistry.java,v 1.1 2001/09/20 01:53:19 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
/**
* The liaison registry provides access to the appropriate database
* liaison implementation for a particular database connection.
*/
public class LiaisonRegistry
{
/**
* Fetch the appropriate database liaison for the supplied database
* connection.
*/
public static DatabaseLiaison getLiaison (Connection conn)
{
return null;
}
}
@@ -0,0 +1,64 @@
//
// $Id: MySQLLiaison.java,v 1.1 2001/09/20 01:53:19 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
/**
* A database liaison for the MySQL database.
*/
public class MySQLLiaison
{
// documentation inherited
public boolean supportsTransactions ()
{
return false;
}
// documentation inherited
public boolean isDuplicateRowException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null && msg.indexOf("Duplicate entry") != -1);
}
// documentation inherited
public boolean isTransientException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null &&
(msg.indexOf("Lost connection") != -1 ||
msg.indexOf("Broken pipe") != -1));
}
// documentation inherited
public int lastInsertedId (Connection conn) throws SQLException
{
// we have to do this by hand. alas all is not roses.
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()");
if (rs.next()) {
return rs.getInt(1);
} else {
return -1;
}
}
}
@@ -1,92 +0,0 @@
//
// $Id: MySQLRepository.java,v 1.8 2001/08/11 22:43:28 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import java.util.Properties;
/**
* The MySQL repository class provides functionality useful to repository
* implementations that make use of MySQL as their underlying database.
*/
public abstract class MySQLRepository extends Repository
{
/**
* Constructs a MySQL repository implementation with the supplied
* configuration properties.
*/
public MySQLRepository (Properties props)
throws SQLException
{
super(props);
}
/**
* @return the most recent ID generated by an insert into an
* AUTO_INCREMENT table.
*/
protected int lastInsertedId ()
throws SQLException
{
// make sure we've got a connection
ensureConnection();
// we have to do this by hand. alas all is not roses.
Statement stmt = _session.connection.createStatement();
ResultSet rs = stmt.executeQuery("select LAST_INSERT_ID()");
if (rs.next()) {
return rs.getInt(1);
} else {
return -1;
}
}
/**
* Determines whether or not the supplied SQL exception originated
* from a duplicate row error.
*
* @return true if the exception was thrown because a duplicate row
* was inserted into a table that does not allow such things, false if
* the exception is not related to duplicate rows.
*/
protected boolean isDuplicateRowException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null && msg.indexOf("Duplicate entry") != -1);
}
/**
* Determines whether or not the supplied SQL exception is a transient
* failure, meaning one that is not related to the SQL being executed,
* but instead to the environment at the time of execution, like the
* connection to the database having been lost.
*
* @return true if the exception was thrown due to a transient
* failure, false if not.
*/
protected boolean isTransientException (SQLException sqe)
{
String msg = sqe.getMessage();
return (msg != null &&
(msg.indexOf("Lost connection") != -1 ||
msg.indexOf("Broken pipe") != -1));
}
}
@@ -1,5 +1,5 @@
//
// $Id: Repository.java,v 1.9 2001/08/11 22:43:28 mdb Exp $
// $Id: Repository.java,v 1.10 2001/09/20 01:53:19 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 Michael Bayne
@@ -20,295 +20,98 @@
package com.samskivert.jdbc;
import java.sql.*;
import java.util.List;
import java.util.Properties;
import com.samskivert.Log;
import com.samskivert.util.*;
import com.samskivert.jdbc.jora.*;
import java.sql.Connection;
import java.sql.SQLException;
/**
* The repository class provides basic functionality upon which to build
* an interface to a repository of information stored in a database (a
* table or set of tables).
* table or set of tables) that is accessed via JDBC.
*
* <p> These services are based on the JORA Java/RDBMS interoperability
* <p> It is expected that the repository class will encapsulate all
* database access for a particular table or set of tables. The interface
* provided to the rest of the application will involve only the
* application object model. For example:
*
* <pre>
* public class PeopleRepository extends SimpleRepository
* {
* public Person getPerson (int personid);
* public Person[] getPeopleByFirstName (String firstName);
* public void updatePerson (Person person);
* }
* </pre>
*
* <p> It is probably also desirable to catch SQL exceptions and wrap them
* in the <code>PersistenceException</code> class that is provided by this
* package.
*
* <p> The repository comes in a few flavors depending on the needs of the
* persistence services being developed:
*
* <ul>
* <li>{@link SimpleRepository}: The simple repository is used by services
* that need access to a single JDBC connection to perform their database
* operations.</li>
*
* <li>{@link JORARepository}: JORA repository is used by services that
* wish to make use of the JORA Java/RDBMS interoperability package.</li>
*
* <li> Because the repository provides a unified interface to a
* particular persistence service, it is conceivable that it would need to
* talk to multiple databases to provide those services. Presently, the
* repository only supports a single connection, but if the need arose,
* implementing a repository flavor that supported multiple connections
* would be the proper solution.</li>
* </ul>
*/
public abstract class Repository
public class Repository
{
/**
* Creates and initializes the repository. A properties object should
* be supplied with the following fields:
* Creates and initializes the repository.
*
* <pre>
* driver=[jdbc driver class]
* url=[jdbc driver url]
* username=[jdbc username]
* password=[jdbc password]
* </pre>
*
* <p> The connection to the database will not be established until
* the first operation is executed. It is important that database
* operations be invoked via <code>execute()</code> rather than
* directly so that the repository can manage the database session
* properly (reestablishing the session on transient failures and so
* on). If a derived class wishes to perform operations directly, it
* should call <code>ensureConnection()</code> before performing any
* database activity.
*
* @param props a properties object containing the configuration
* parameters for the repository.
* @param provider the connection provider which will be used to
* obtain our database connection.
*/
public Repository (Properties props)
throws SQLException
public Repository (ConnectionProvider provider)
{
// extract our configuration parameters
_dclass =
requireProp(props, "driver", "No driver class specified.");
_url =
requireProp(props, "url", "No driver url specified.");
_username =
requireProp(props, "username", "No driver username specified.");
_password =
requireProp(props, "password", "No driver password specified.");
// we attempt to reestablish the connection if there's a transient
// failure (like the database is down or our old connection gets
// closed by the database), but we don't want to get too crazy
// about connecting to the database automatically, so we limit our
// attempts to twice every thirty seconds
_throttle = new Throttle(2, 30);
}
/** Establishes a connection to the database. */
protected synchronized void ensureConnection ()
throws SQLException
{
// nothing doing if we've already got a connection
if (_session != null) {
return;
}
// make sure we don't try to reestablish the connection
// overzealously in times of lengthy database outage
if (_throttle.throttleOp()) {
throw new SQLException("Connection attempt throttled.");
}
try {
// create our JORA session
_session = new Session(_dclass);
// connect the session to the database. the only reason
// session.open() fails is class not found
if (!_session.open(_url, _username, _password)) {
throw new SQLException("Unable to load JDBC driver class: " +
_dclass);
}
// set auto-commit to false
if (supportsTransactions()) {
_session.connection.setAutoCommit(false);
}
// create our table objects
createTables();
} catch (SQLException sqe) {
// if we have any problems establishing a connection, close
// and clear out our session reference to prevent attempted
// use of a half initialized session
try {
shutdown();
} catch (SQLException sqe2) {
Log.warning("Error shutting down half-initialized " +
"connection [primary_failure=" + sqe +
", secondary_failure=" + sqe2 + "].");
}
throw sqe;
}
_provider = provider;
}
/**
* Shuts down the existing connection to the database and
* reestablishes the connection.
*/
public void reestablishConnection ()
throws SQLException
{
// first close the old connection
shutdown();
// then open a new one
ensureConnection();
}
/**
* After the database session is begun, this function will be called
* to give the repository implementation the opportunity to create its
* table objects. If the database session fails, a new session will be
* established and this function will be called again for the new
* session.
*/
protected abstract void createTables () throws SQLException;
protected static String requireProp (Properties props,
String name, String errmsg)
throws SQLException
{
String value = props.getProperty(name);
if (StringUtil.blank(value)) {
throw new SQLException(errmsg);
}
return value;
}
/**
* A repository should be shutdown before the calling code disposes of
* it. This allows the repository to cleanly terminate the underlying
* database services.
*/
public synchronized void shutdown ()
throws SQLException
{
if (_session != null) {
_session.close();
_session = null;
}
}
/**
* Used by <code>execute</code>.
*
* @see #execute
* Database operations should be encapsulated in instances of this
* class and then provided to the repository for invocation. This
* allows the repository to manage transaction commits for you as well
* as for it to automatically retry an operation if the connection
* failed for some transient reason.
*/
protected interface Operation
{
public Object invoke () throws SQLException;
/**
* Invokes code that performs one or more database operations, all
* of which will be encapsulated in a single transaction (which
* can be retried in the event of a transient failure).
*
* @param conn the database connection on which the operations
* will be performed.
* @param liaison a database liaison for the supplied connection
* which can be used to determine things for which there is no
* standard way to determine via JDBC.
*
* @exception SQLException if thrown, this will be wrapped in a
* {@link PersistenceException} before being passed up to the
* operation invoker.
* @exception PersistenceException can be thrown if something goes
* awry when executing the operation. Note that the operation will
* not be retried if a persistence exception is thrown. Such
* exceptions are assumed to be application specific and not
* indicative of a basic JDBC failure. The transaction
* <em>will</em> be rolled back in such cases, however.
*/
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws SQLException, PersistenceException;
}
/**
* Executes the supplied operation. In the event of a transient
* failure, the repository will attempt to reestablish the database
* connection and try the operation again.
*
* @return whatever value is returned by the invoked operation.
*/
protected Object execute (Operation op)
throws SQLException
{
return execute(op, true);
}
/**
* Executes the supplied operation followed by a call to commit() on
* the session unless an SQLException or runtime error occurs, in
* which case a call to rollback() is executed on the session.
*
* @param retryOnTransientFailure if true and the operation fails due
* to a transient failure (like losing the connection to the database
* or deadlock detection), the connection to the database will be
* reestablished and the operation attempted once more.
*
* @return whatever value is returned by the invoked operation.
*/
protected Object execute (Operation op, boolean retryOnTransientFailure)
throws SQLException
{
Object rv = null;
// make sure our connection is established
ensureConnection();
try {
// invoke the operation
rv = op.invoke();
// commit the session
if (supportsTransactions()) {
_session.commit();
}
// return the operation result
return rv;
} catch (SQLException sqe) {
// back out our changes if something got hosed
if (supportsTransactions()) {
try {
_session.rollback();
} catch (SQLException rbe) {
Log.warning("Unable to roll back operation.");
Log.logStackTrace(rbe);
}
}
// if this is a transient failure and we've been requested to
// retry such failures, close and reopen the database
// connection and try one more time
if (retryOnTransientFailure && isTransientException(sqe)) {
Log.info("Transient failure executing operation, " +
"retrying [error=" + sqe + "].");
// to reset the connection, we simply close it. it will be
// reopened on our subsequent call to execute()
shutdown();
return execute(op, false);
} else {
Log.info("Non-transient exception, we're hosed " +
"[error=" + sqe + "].");
throw sqe;
}
} catch (RuntimeException rte) {
// back out our changes if something got hosed
if (supportsTransactions()) {
try {
_session.rollback();
} catch (SQLException rbe) {
Log.warning("Unable to roll back operation.");
Log.logStackTrace(rbe);
}
}
throw rte;
}
}
/**
* Specializations of this class for particular RDBMS/JDBC driver
* combinations should override this method and indicate whether or
* not transactions are supported.
*/
protected boolean supportsTransactions ()
{
return false;
}
/**
* Determines whether or not the supplied SQL exception is a transient
* failure, meaning one that is not related to the SQL being executed,
* but instead to the environment at the time of execution, like the
* connection to the database having been lost.
*
* <p> Specializations of the repository class for particular
* database/JDBC driver combos should override this function and match
* the appropriate exceptions.
*
* @return true if the exception was thrown due to a transient
* failure, false if not.
*/
protected boolean isTransientException (SQLException sqe)
{
return false;
}
protected Session _session;
protected String _dclass;
protected String _url;
protected String _username;
protected String _password;
// we use this to prevent too many database connection attempts
protected Throttle _throttle;
/** Our database connection provider. */
protected ConnectionProvider _provider;
}
@@ -0,0 +1,177 @@
//
// $Id: SimpleRepository.java,v 1.1 2001/09/20 01:53:20 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.sql.*;
import java.util.Properties;
import com.samskivert.Log;
/**
* The simple repository should be used for a repository that only needs
* access to a single JDBC connection instance to perform its persistence
* services.
*/
public class SimpleRepository extends Repository
{
/**
* Creates and initializes a simple repository which will access the
* database identified by the supplied database identifier.
*
* @param provider the connection provider which will be used to
* obtain our database connection.
* @param dbident the identifier of the database that will be accessed
* by this repository.
*/
public SimpleRepository (ConnectionProvider provider, String dbident)
{
super(provider);
_dbident = dbident;
}
/**
* Executes the supplied operation. In the event of a transient
* failure, the repository will attempt to reestablish the database
* connection and try the operation again.
*
* @return whatever value is returned by the invoked operation.
*/
protected Object execute (Operation op)
throws PersistenceException
{
return execute(op, true);
}
/**
* Executes the supplied operation followed by a call to
* <code>commit()</code> on the connection unless a
* <code>PersistenceException</code> or runtime error occurs, in which
* case a call to <code>rollback()</code> is executed on the
* connection.
*
* @param retryOnTransientFailure if true and the operation fails due
* to a transient failure (like losing the connection to the database
* or deadlock detection), the connection to the database will be
* reestablished (if necessary) and the operation attempted once more.
*
* @return whatever value is returned by the invoked operation.
*/
protected Object execute (Operation op, boolean retryOnTransientFailure)
throws PersistenceException
{
// obtain our database connection
Connection conn = null;
try {
conn = _provider.getConnection(_dbident);
conn.setAutoCommit(false);
gotConnection(conn);
} catch (SQLException sqe) {
String err = "Unable to obtain connection.";
throw new PersistenceException(err, sqe);
}
// obtain a liaison for this connection
DatabaseLiaison liaison = LiaisonRegistry.getLiaison(conn);
Object rv = null;
try {
// invoke the operation
rv = op.invoke(conn, liaison);
// commit the transaction
if (liaison.supportsTransactions()) {
conn.commit();
}
// return the operation result
return rv;
} catch (SQLException sqe) {
// back out our changes if something got hosed
if (liaison.supportsTransactions()) {
try {
conn.rollback();
} catch (SQLException rbe) {
Log.warning("Unable to roll back operation.");
Log.logStackTrace(rbe);
}
}
// let the connection provider know that the connection failed
_provider.connectionFailed(_dbident, conn, sqe);
// clear out the reference so that we don't release it later
conn = null;
// if this is a transient failure and we've been requested to
// retry such failures, try one more time
if (retryOnTransientFailure &&
liaison.isTransientException(sqe)) {
Log.info("Transient failure executing operation, " +
"retrying [error=" + sqe + "].");
return execute(op, false);
}
String err = "Operation invocation failed.";
throw new PersistenceException(err, sqe);
} catch (PersistenceException pe) {
// back out our changes if something got hosed
if (liaison.supportsTransactions()) {
try {
conn.rollback();
} catch (SQLException rbe) {
Log.warning("Unable to roll back operation.");
Log.logStackTrace(rbe);
}
}
throw pe;
} catch (RuntimeException rte) {
// back out our changes if something got hosed
if (conn != null && liaison.supportsTransactions()) {
try {
conn.rollback();
} catch (SQLException rbe) {
Log.warning("Unable to roll back operation.");
Log.logStackTrace(rbe);
}
}
throw rte;
} finally {
if (conn != null) {
// release the database connection
_provider.releaseConnection(_dbident, conn);
}
}
}
/**
* Called when we fetch a connection from the provider. This gives
* derived classes an opportunity to configure whatever internals they
* might be using with the connection that was fetched.
*/
protected void gotConnection (Connection conn)
{
}
protected String _dbident;
}
@@ -0,0 +1,191 @@
//
// $Id: StaticConnectionProvider.java,v 1.1 2001/09/20 01:53:20 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 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
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc;
import java.io.IOException;
import java.sql.*;
import java.util.*;
import com.samskivert.Log;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.PropertiesUtil;
import com.samskivert.util.StringUtil;
/**
* The static connection provider generates JDBC connections based on
* configuration information provided via a properties file. It does no
* connection pooling and always returns the same connection for a
* particular identifier (unless that connection need be closed because of
* a connection failure, in which case it opens a new one the next time
* the connection is requested).
*
* <p> The configuration properties file should contain the following
* information:
*
* <pre>
* IDENT.driver=[jdbc driver class]
* IDENT.url=[jdbc driver url]
* IDENT.username=[jdbc username]
* IDENT.password=[jdbc password]
*
* [...]
* </pre>
*
* Where <code>IDENT</code> is the database identifier for a particular
* database connection. When a particular database identifier is
* requested, the configuration information will be fetched from the
* properties.
*/
public class StaticConnectionProvider implements ConnectionProvider
{
/**
* Constructs a static connection provider which will load its
* configuration from a properties file accessible via the classpath
* of the running application and identified by the specified path.
*
* @param propPath the path (relative to the classpath) to the
* properties file that will be used for configuration information.
*
* @exception IOException thrown if an error occurs locating or
* loading the specified properties file.
*/
public StaticConnectionProvider (String propPath)
throws IOException
{
this(ConfigUtil.loadProperties(propPath));
}
/**
* Constructs a static connection provider which will fetch its
* configuration information from the specified properties object.
*
* @param props the configuration for this connection provider.
*/
public StaticConnectionProvider (Properties props)
{
_props = props;
}
/**
* Closes all of the open database connections in preparation for
* shutting down.
*/
public void shutdown ()
{
// close all of the connections
Iterator iter = _conns.keySet().iterator();
while (iter.hasNext()) {
String ident = (String)iter.next();
Connection conn = (Connection)_conns.get(ident);
try {
conn.close();
} catch (SQLException sqe) {
Log.warning("Error shutting down connection " +
"[ident=" + ident + ", err=" + sqe + "].");
}
}
// clear out the connection table
_conns.clear();
}
// documentation inherited
public Connection getConnection (String ident)
throws PersistenceException
{
Connection conn = (Connection)_conns.get(ident);
// open the connection if we haven't already
if (conn == null) {
Properties props =
PropertiesUtil.getSubProperties(_props, ident);
// get the JDBC configuration info
String err = "No driver class specified [ident=" + ident + "].";
String driver = requireProp(props, "driver", err);
err = "No driver URL specified [ident=" + ident + "].";
String url = requireProp(props, "url", err);
err = "No driver username specified [ident=" + ident + "].";
String username = requireProp(props, "username", err);
err = "No driver password specified [ident=" + ident + "].";
String password = requireProp(props, "password", err);
// load up the driver class
try {
Class.forName(driver);
} catch (Exception e) {
err = "Error loading driver [ident=" + ident +
", class=" + driver + "].";
throw new PersistenceException(err, e);
}
// create the connection
try {
conn = DriverManager.getConnection(url, username, password);
} catch (SQLException sqe) {
err = "Error creating database connection " +
"[ident=" + ident + "].";
throw new PersistenceException(err, sqe);
}
// cache the connection
_conns.put(ident, conn);
}
return conn;
}
// documentation inherited
public void releaseConnection (String ident, Connection conn)
{
// nothing to do here, all is well
}
// documentation inherited
public void connectionFailed (String ident, Connection conn,
SQLException error)
{
// attempt to close the connection
try {
conn.close();
} catch (SQLException sqe) {
Log.warning("Error closing failed connection [ident=" + ident +
", error=" + sqe + "].");
}
// and remove it from the cache
_conns.remove(ident);
}
protected static String requireProp (Properties props,
String name, String errmsg)
throws PersistenceException
{
String value = props.getProperty(name);
if (StringUtil.blank(value)) {
throw new PersistenceException(errmsg);
}
return value;
}
protected Properties _props;
protected HashMap _conns = new HashMap();
}
@@ -13,6 +13,8 @@ package com.samskivert.jdbc.jora;
import java.util.*;
import java.sql.*;
import com.samskivert.Log;
/**
* This class is reposnsible for establishing connection with database
* and handling database errors.
@@ -31,10 +33,47 @@ public class Session {
connectionID = 0;
}
/** Construct a session with a pre-existing connection instance. In
* this case, {@link #open} should not be called on this session.
*
* @param conn the connection to use.
*/
public Session(Connection connection)
{
connectionID = 0;
preparedStmtHash = new Hashtable();
setConnection(connection);
}
/** Session consructor for ODBC bridge driver
*/
public Session() { this("sun.jdbc.odbc.JdbcOdbcDriver"); }
/** Sets the connection that should be used by this session.
*/
public void setConnection(Connection conn)
{
// only up the connection id if this is a new connection
if (connection != conn) {
// clear out our prepared statement hash because we've got a
// new connection
Enumeration items = preparedStmtHash.elements();
while (items.hasMoreElements()) {
try {
((PreparedStatement)items.nextElement()).close();
} catch (SQLException sqe) {
Log.warning("Error closing cached prepared statement " +
"[error=" + sqe + "].");
}
}
preparedStmtHash.clear();
// switch to our new connection
connection = conn;
connectionID += 1;
}
}
/** Handler of database session errors. Programmer should override
* this method in derived class in order to provide application
* dependent error handling.
@@ -1,5 +1,5 @@
//
// $Id: UserExistsException.java,v 1.3 2001/08/12 01:34:31 mdb Exp $
// $Id: UserExistsException.java,v 1.4 2001/09/20 01:53:20 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 Michael Bayne
@@ -20,11 +20,13 @@
package com.samskivert.servlet.user;
import com.samskivert.jdbc.PersistenceException;
/**
* Thrown during user account creation when a user with the requested
* username already exists.
*/
public class UserExistsException extends Exception
public class UserExistsException extends PersistenceException
{
public UserExistsException (String message)
{
@@ -1,5 +1,5 @@
//
// $Id: UserManager.java,v 1.8 2001/08/11 22:43:28 mdb Exp $
// $Id: UserManager.java,v 1.9 2001/09/20 01:53:20 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 Michael Bayne
@@ -21,11 +21,12 @@
package com.samskivert.servlet.user;
import java.net.URLEncoder;
import java.sql.SQLException;
import java.util.Properties;
import javax.servlet.http.*;
import com.samskivert.Log;
import com.samskivert.jdbc.ConnectionProvider;
import com.samskivert.jdbc.PersistenceException;
import com.samskivert.servlet.RedirectException;
import com.samskivert.servlet.util.RequestUtils;
import com.samskivert.util.*;
@@ -39,8 +40,8 @@ public class UserManager
{
/**
* A user manager creates a user repository through which to load and
* save user records. The properties needed to configure the user
* repository must be provided to the user manager at construct time.
* save user records. A connection provider must be supplied via which
* the user repository can obtain its database connection.
*
* <p> Presently the user manager requires the following configuration
* information:
@@ -59,16 +60,20 @@ public class UserManager
* they are authenticated.
* </ul>
*
* @param config the user manager configuration properties.
* @param provider a database connection provider that will be used by
* the user repository to obtain it's database connection.
*
* @see UserRepository#UserRepository
*/
public UserManager (Properties props)
throws SQLException
public UserManager (Properties config, ConnectionProvider provider)
throws PersistenceException
{
// open up the user repository
_repository = new UserRepository(props);
_repository = new UserRepository(provider);
// fetch the login URL from the properties
_loginURL = props.getProperty("login_url");
_loginURL = config.getProperty("login_url");
if (_loginURL == null) {
Log.warning("No login_url supplied in user manager config. " +
"Authentication won't work.");
@@ -80,8 +85,8 @@ public class UserManager
{
try {
_repository.pruneSessions();
} catch (SQLException sqe) {
Log.warning("Error pruning session table: " + sqe);
} catch (PersistenceException pe) {
Log.warning("Error pruning session table: " + pe);
}
}
};
@@ -91,13 +96,6 @@ public class UserManager
public void shutdown ()
{
// shut down the user repository
try {
_repository.shutdown();
} catch (SQLException sqe) {
Log.warning("Error shutting down user repository: " + sqe);
}
// cancel our session table pruning thread
IntervalManager.remove(_prunerid);
}
@@ -119,7 +117,7 @@ public class UserManager
* bogus.
*/
public User loadUser (HttpServletRequest req)
throws SQLException
throws PersistenceException
{
String authcode = getAuthCode(req);
if (authcode != null) {
@@ -139,7 +137,7 @@ public class UserManager
* @return the user associated with the request.
*/
public User requireUser (HttpServletRequest req)
throws SQLException, RedirectException
throws PersistenceException, RedirectException
{
User user = loadUser(req);
// if no user was loaded, we need to redirect these fine people to
@@ -172,7 +170,8 @@ public class UserManager
*/
public User login (String username, String password, boolean persist,
HttpServletResponse rsp)
throws SQLException, NoSuchUserException, InvalidPasswordException
throws PersistenceException, NoSuchUserException,
InvalidPasswordException
{
// load up the requested user
User user = _repository.loadUser(username);
@@ -1,5 +1,5 @@
//
// $Id: UserRepository.java,v 1.16 2001/09/15 17:22:11 mdb Exp $
// $Id: UserRepository.java,v 1.17 2001/09/20 01:53:20 mdb Exp $
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001 Michael Bayne
@@ -28,8 +28,7 @@ import java.util.Properties;
import org.apache.regexp.*;
import com.samskivert.Log;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.MySQLRepository;
import com.samskivert.jdbc.*;
import com.samskivert.jdbc.jora.*;
import com.samskivert.util.HashIntMap;
@@ -38,33 +37,32 @@ import com.samskivert.util.HashIntMap;
* user repository encapsulates the creating, loading and management of
* users and sessions.
*/
public class UserRepository extends MySQLRepository
public class UserRepository extends JORARepository
{
/**
* Creates the repository and opens the user database. A properties
* object should be supplied with the following fields:
*
* <pre>
* driver=[jdbc driver class]
* url=[jdbc driver url]
* username=[jdbc username]
* password=[jdbc password]
* </pre>
*
* @param props a properties object containing the configuration
* parameters for the repository.
* The database identifier used to obtain a connection from our
* connection provider. The value is <code>userdb</code> which you'll
* probably need to know to provide the proper configuration to your
* connection provider.
*/
public UserRepository (Properties props)
throws SQLException
public static final String USER_REPOSITORY_IDENT = "userdb";
/**
* Creates the repository and opens the user database. The database
* identifier used to fetch our database connection is documented by
* {@link #USER_REPOSITORY_IDENT}.
*
* @param provider the database connection provider.
*/
public UserRepository (ConnectionProvider provider)
{
super(props);
super(provider, USER_REPOSITORY_IDENT);
}
protected void createTables ()
throws SQLException
protected void createTables (Session session)
{
// create our table object
_utable = new Table(User.class.getName(), "users", _session,
_utable = new Table(User.class.getName(), "users", session,
"userid");
}
@@ -83,7 +81,7 @@ public class UserRepository extends MySQLRepository
*/
public int createUser (String username, String password,
String realname, String email)
throws InvalidUsernameException, UserExistsException, SQLException
throws InvalidUsernameException, UserExistsException, PersistenceException
{
// check minimum length
if (username.length() < MINIMUM_USERNAME_LENGTH) {
@@ -103,25 +101,26 @@ public class UserRepository extends MySQLRepository
user.email = email;
user.created = new Date(System.currentTimeMillis());
try {
execute(new Operation () {
public Object invoke () throws SQLException
{
execute(new Operation () {
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
try {
_utable.insert(user);
// update the userid now that it's known
user.userid = lastInsertedId();
user.userid = liaison.lastInsertedId(conn);
// nothing to return
return null;
}
});
} catch (SQLException sqe) {
if (isDuplicateRowException(sqe)) {
throw new UserExistsException("error.user_exists");
} else {
throw sqe;
}
}
} catch (SQLException sqe) {
if (liaison.isDuplicateRowException(sqe)) {
throw new UserExistsException("error.user_exists");
} else {
throw sqe;
}
}
}
});
return user.userid;
}
@@ -133,10 +132,11 @@ public class UserRepository extends MySQLRepository
* that id exists.
*/
public User loadUser (final String username)
throws SQLException
throws PersistenceException
{
return (User)execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
// look up the user
String query = "where username = '" + username + "'";
@@ -161,10 +161,11 @@ public class UserRepository extends MySQLRepository
* that id exists.
*/
public User loadUser (final int userid)
throws SQLException
throws PersistenceException
{
return (User)execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
// look up the user
Cursor ec = _utable.select("where userid = " + userid);
@@ -188,10 +189,11 @@ public class UserRepository extends MySQLRepository
* no session exists with the supplied identifier.
*/
public User loadUserBySession (final String sessionKey)
throws SQLException
throws PersistenceException
{
return (User)execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
String query = "where authcode = '" + sessionKey +
"' AND sessions.userid = users.userid";
@@ -214,10 +216,11 @@ public class UserRepository extends MySQLRepository
* Updates a user that was previously fetched from the repository.
*/
public void updateUser (final User user)
throws SQLException
throws PersistenceException
{
execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_utable.update(user);
// nothing to return
@@ -230,10 +233,11 @@ public class UserRepository extends MySQLRepository
* Removes the user from the repository.
*/
public void deleteUser (final User user)
throws SQLException
throws PersistenceException
{
execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_utable.delete(user);
// nothing to return
@@ -251,7 +255,7 @@ public class UserRepository extends MySQLRepository
* sessions expire after one month.
*/
public String createNewSession (final User user, boolean persist)
throws SQLException
throws PersistenceException
{
// generate a random session identifier
final String authcode = UserUtil.genAuthCode(user);
@@ -263,7 +267,8 @@ public class UserRepository extends MySQLRepository
// insert the session into the database
execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_session.execute("insert into sessions " +
"(authcode, userid, expires) values('" +
@@ -282,10 +287,11 @@ public class UserRepository extends MySQLRepository
* Prunes any expired sessions from the sessions table.
*/
public void pruneSessions ()
throws SQLException
throws PersistenceException
{
execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
_session.execute("delete from sessions where " +
"expires <= CURRENT_DATE()");
@@ -301,7 +307,7 @@ public class UserRepository extends MySQLRepository
* slot in the array will contain a null.
*/
public String[] loadUserNames (int[] userids)
throws SQLException
throws PersistenceException
{
return loadNames(userids, "username");
}
@@ -312,13 +318,13 @@ public class UserRepository extends MySQLRepository
* slot in the array will contain a null.
*/
public String[] loadRealNames (int[] userids)
throws SQLException
throws PersistenceException
{
return loadNames(userids, "realname");
}
protected String[] loadNames (int[] userids, final String column)
throws SQLException
throws PersistenceException
{
// if userids is zero length, we've got no work to do
if (userids.length == 0) {
@@ -338,7 +344,8 @@ public class UserRepository extends MySQLRepository
// do the query
execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = _session.connection.createStatement();
try {
@@ -377,13 +384,14 @@ public class UserRepository extends MySQLRepository
* already using it to link up.
*/
public String[] loadAllRealNames ()
throws SQLException
throws PersistenceException
{
final ArrayList names = new ArrayList();
// do the query
execute(new Operation () {
public Object invoke () throws SQLException
public Object invoke (Connection conn, DatabaseLiaison liaison)
throws PersistenceException, SQLException
{
Statement stmt = _session.connection.createStatement();
try {
@@ -411,13 +419,17 @@ public class UserRepository extends MySQLRepository
public static void main (String[] args)
{
Properties props = new Properties();
props.put("driver", "org.gjt.mm.mysql.Driver");
props.put("url", "jdbc:mysql://localhost:3306/samskivert");
props.put("username", "www");
props.put("password", "Il0ve2PL@Y");
props.put(USER_REPOSITORY_IDENT + ".driver",
"org.gjt.mm.mysql.Driver");
props.put(USER_REPOSITORY_IDENT + ".url",
"jdbc:mysql://localhost:3306/samskivert");
props.put(USER_REPOSITORY_IDENT + ".username", "www");
props.put(USER_REPOSITORY_IDENT + ".password", "Il0ve2PL@Y");
try {
UserRepository rep = new UserRepository(props);
StaticConnectionProvider scp =
new StaticConnectionProvider(props);
UserRepository rep = new UserRepository(scp);
System.out.println(rep.loadUser("mdb"));
System.out.println(rep.loadUserBySession("auth"));
@@ -427,7 +439,7 @@ public class UserRepository extends MySQLRepository
rep.createUser("mdb", "foobar", "Michael Bayne",
"mdb@samskivert.com");
rep.shutdown();
scp.shutdown();
} catch (Throwable t) {
t.printStackTrace(System.err);