// Copyright (c) 2001 Dustin Sallings <dustin@spy.net>
package net.spy.db;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import net.spy.SpyObject;
import net.spy.db.sp.SelectPrimaryKey;
import net.spy.db.sp.UpdatePrimaryKey;
import net.spy.util.CloseUtil;
import net.spy.util.SpyConfig;
/**
* Primary key generator. This is an extensible singleton that provides
* access to a database-backed set of primary keys along with a fetch-ahead
* cache of those keys.
*
* <p>
*
* The default implementation assumes you have the following table
* (<code>primary_key</code>) in the database the configuration defines:
*
* </p>
* <p>
*
* <table border="1">
* <tr>
* <th colspan="3"><font size="+1">primary_keys</font></th>
* </tr>
* <tr>
* <th>Column Name</th>
* <th>Column Type</th>
* <th>Column Description</th>
* </tr>
* <tr>
* <td>table_name</td>
* <td>varchar</td>
* <td>The name of the table (or other resource) for which we are
* generating the given primary key. All table names in this column
* must be in lowercase as the input from the user will be
* lowercased on key retrieval.
* </td>
* </tr>
* <tr>
* <td>primary_key</td>
* <td>numeric</td>
* <td>The next primary key value issued.</td>
* </tr>
* <tr>
* <td>incr_value</td>
* <td>numeric</td>
* <td>The amount to increment the primary key each time.</td>
* </table>
*
* </p>
* <p>
*
* Other schemas may exist as long as they fit into the ``update something,
* select something'' model and they operate as described below.
*
* </p>
*/
public class GetPK extends SpyObject {
private static GetPK instance=null;
private final ConcurrentMap<String, KeyStore> caches;
/**
* Constructor for an extensible Singleton.
*/
protected GetPK() {
super();
caches=new ConcurrentHashMap<String, KeyStore>();
}
/**
* Get the instance of GetPK.
*
* @return the instance
*/
public static synchronized GetPK getInstance() {
if(instance == null) {
instance=new GetPK();
}
return instance;
}
/**
* Set the singleton instance.
* @param to the new singleton instance
*/
public static synchronized void setInstance(GetPK to) {
instance=to;
}
/**
* Get a primary key from the database described in the given config.
*
* @param conf the configuration
* @param table the table for which the key is needed
* @return the key
* @throws SQLException if there's a problem getting the key
*/
public BigDecimal getPrimaryKey(SpyConfig conf, String table)
throws SQLException {
BigDecimal rv=null;
SpyDB db=new SpyDB(conf);
try {
rv=getPrimaryKey(db, table.toLowerCase(),
makeDbTableKey(conf, table));
} finally {
CloseUtil.close(db);
}
return(rv);
}
// make the key to be used for identifying this table and connection
// source
private String makeDbTableKey (SpyConfig conf, String table) {
StringBuilder rc=new StringBuilder(512);
// shove in typical stuff
rc.append(conf.get("dbSource"));
rc.append(";");
rc.append(conf.get("dbConnectionSource"));
rc.append(";");
rc.append(conf.get("dbDriverName"));
rc.append(";");
rc.append(conf.get("dbUser"));
rc.append(";");
rc.append(conf.get("dbPass"));
rc.append(";");
rc.append(conf.get("dbPoolName"));
rc.append(";");
// just a little conf helper in case we ever require this
rc.append(conf.get("dbPkKey"));
rc.append(";");
// and add the table
rc.append(table);
return(rc.toString());
}
// Get the key (usually from the cache)
private BigDecimal getPrimaryKey(SpyDB db, String table, String key)
throws SQLException {
BigDecimal rv=null;
try {
KeyStore ks=caches.get(key);
// If we didn't get the key store, go get it now
if(ks == null) {
getKeysFromDB(db, table, key);
ks=caches.get(key);
if(ks==null) {
throw new SQLException("Couldn't get initial keys for "
+ table);
}
}
rv=ks.nextKey();
} catch(OverDrawnException ode) {
// Overdrawn, need to fetch the cache.
caches.remove(key);
getKeysFromDB(db, table, key);
// Get the new key store
KeyStore ks=caches.get(key);
try {
rv=ks.nextKey();
} catch(OverDrawnException ode2) {
throw new AssertionError(
"Primary keys not available after load.");
}
}
return(rv);
}
/**
* Get the DBSP required for updating the primary key table.
*
* <p>
*
* A subclass may override this to change the behavior of the first
* part of the ``fetch from db'' stage. The DBSP returned will take
* exactly one parameter: <code>table_name</code> and will be called
* via executeUpdate. The update must update exactly <i>one</i> row.
* Any more or fewer will cause the process to fail and an exception
* will be thrown.
*
* </p>
* <p>
*
* For an example implementation, please see {@link UpdatePrimaryKey}
* (this is the default).
*
* </p>
*
* @param conn the connection to use (already in a transaction)
* @return the required DBSP
* @throws SQLException if there's a problem getting the DBSP
*/
protected DBSP getUpdateDBSP(Connection conn) throws SQLException {
return(new UpdatePrimaryKey(conn));
}
/**
* Get the DBSP required for selecting primary key information back out
* of the primary key table.
*
* <p>
*
* A subclass may override this method to change the behavior of the
* select statement that finds the range of results for a table. The
* DBSP returned will take exactly one parameter:
* <code>table_name</code> and return a result set containing at least
* the following two columns:
*
* <ul>
* <li><code>first_key</code> - the first key in the range</li>
* <li><code>last_key</code> - the last key in the range</li>
* </ul>
*
* The ResultSet must contain exactly <i>one</i> row. Any more or
* fewer will cause the process to fail and an exception will be
* thrown.
*
* </p>
* <p>
*
* For an example implementation, please see {@link SelectPrimaryKey}
* (this is the default).
*
* <p>
*
* @param conn the connection to use (already in a transaction)
* @return the required DBSP
* @throws SQLException if there's a problem getting the DBSP
*/
protected DBSP getSelectDBSP(Connection conn) throws SQLException {
return(new SelectPrimaryKey(conn));
}
// get keys from a database
private void getKeysFromDB(SpyDB db, String table, String key)
throws SQLException {
Connection conn=null;
boolean complete=false;
try {
conn=db.getConn();
conn.setAutoCommit(false);
// Update the table first
DBSP dbsp=getUpdateDBSP(conn);
dbsp.set("table_name", table);
int changed=0;
try {
changed=dbsp.executeUpdate();
} finally {
dbsp.close();
}
// Make sure one row got updated
if(changed==0) {
throw new SQLException("Incorrect row count for " + table
+ " (got " + changed + ") - "
+ "This usually means the primary key table does not have "
+ table + " or there is a case mismatch.");
} else if(changed>1) {
throw new SQLException(
"Did not update the correct number of rows for "
+ table + " (got " + changed + ")");
}
// Now, fetch it again
BigDecimal start=null;
BigDecimal end=null;
dbsp=getSelectDBSP(conn);
ResultSet rs=null;
try {
dbsp.set("table_name", table);
rs=dbsp.executeQuery();
if(!rs.next()) {
throw new SQLException(
"No results returned for primary key");
}
// Get the beginning and ending of the range
start=rs.getBigDecimal("first_key");
end=rs.getBigDecimal("last_key");
if(rs.next()) {
throw new SQLException(
"Too many results returned for primary key");
}
} finally {
// clean up
if(rs!=null) {
rs.close();
}
dbsp.close();
}
KeyStore ks=new KeyStore(start, end);
getLogger().debug("Got a new keystore for %s: %s", table, ks);
// Note that this may blindly remove an existing keystore. That's
// considered OK as it'll just be an unexpected burn.
caches.put(key, ks);
complete=true;
} finally {
if(conn!=null) {
if(complete) {
try {
conn.commit();
} catch(SQLException e) {
getLogger().error("Problem committing", e);
}
} else {
try {
conn.rollback();
} catch(SQLException e) {
getLogger().error("Problem rolling back", e);
}
}
// Set autocommit back
try {
conn.setAutoCommit(true);
} catch(SQLException e) {
getLogger().error("Problem resetting autocommit", e);
}
} // got a connection
} // finally block
} // getKeysFromDB
}