/*
* This file is part of the WfMOpen project.
* Copyright (C) 2001-2003 Danet GmbH (www.danet.de), GS-AN.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id: JDBCPersistentMap.java 2893 2009-01-20 17:02:33Z drmlipp $
*
* $Log$
* Revision 1.4 2006/09/29 12:32:13 drmlipp
* Consistently using WfMOpen as projct name now.
*
* Revision 1.3 2005/04/22 15:10:49 drmlipp
* Merged changes from 1.3 branch up to 1.3p15.
*
* Revision 1.1.1.3.6.5 2005/04/14 11:45:07 drmlipp
* More (minor) optimizations.
*
* Revision 1.1.1.3.6.4 2005/04/13 16:14:06 drmlipp
* Optimized db access.
*
* Revision 1.1.1.3.6.3 2005/04/11 14:33:11 drmlipp
* Improved message.
*
* Revision 1.2 2005/04/08 11:28:03 drmlipp
* Merged changes from 1.3 branch up to 1.3p6.
*
* Revision 1.1.1.3.6.2 2005/03/18 21:59:00 drmlipp
* Fixed db access optimization.
*
* Revision 1.1.1.3.6.1 2005/03/18 15:02:00 drmlipp
* Optimized (reduced number of DB accesses).
*
* Revision 1.1.1.3 2004/08/18 15:17:35 drmlipp
* Update to 1.2
*
* Revision 1.37 2004/02/09 14:57:49 lipp
* Added support for map id of type Long.
*
* Revision 1.36 2003/11/27 16:26:09 lipp
* Added specific exception type to allow reconstruction of SQLException
* when using JDBCPersistence.
*
* Revision 1.35 2003/11/03 12:41:23 lipp
* Added null vs. "" support for Oracle.
*
* Revision 1.34 2003/06/27 08:51:46 lipp
* Fixed copyright/license information.
*
* Revision 1.33 2003/05/23 12:11:13 lipp
* Fixed driver/db characteristics caching.
*
* Revision 1.32 2003/04/11 13:21:04 lipp
* Fixed NullPointerException
*
* Revision 1.31 2003/03/28 10:09:20 lipp
* Intriduced logging using commons-logging.
*
* Revision 1.30 2003/03/06 15:23:03 lipp
* Some special handling for oracle.
*
* Revision 1.29 2003/03/06 13:47:45 schlue
* Handling of null values fixed.
*
* Revision 1.28 2003/03/05 13:18:32 schlue
* Fixed column names for map and item removed (now dynamic).
*
* Revision 1.27 2003/03/05 09:42:39 lipp
* Temporary fix for UniversalPrepStmt.
*
* Revision 1.26 2003/02/21 16:38:51 lipp
* Introduced UniversalPrepStmt.
*
* Revision 1.25 2003/02/19 12:23:14 lipp
* Adapted parameter sequence for setBinary to usual style.
*
* Revision 1.24 2003/02/18 15:20:38 schlue
* svalue handling fixed.
*
* Revision 1.23 2003/02/05 15:34:22 lipp
* Removed unnecessary exception.
*
* Revision 1.22 2002/11/19 15:20:31 lipp
* Removed not needed semicolon.
*
* Revision 1.21 2002/10/08 11:57:03 schlue
* Code fixed for changing a connection with non-saved modifications.
* Problems with serialization fixed.
* Formal corrections acc. to style checker.
*
* Revision 1.20 2002/08/30 07:24:25 schlue
* isModified added.
*
* Revision 1.19 2002/08/26 20:23:13 lipp
* Lots of method renames.
*
* Revision 1.18 2002/08/26 14:17:07 lipp
* JavaDoc fixes.
*
* Revision 1.17 2002/08/26 09:43:31 schlue
* Code optimizations.
*
* Revision 1.16 2002/08/23 13:17:09 schlue
* Modifications and fixes acc. to code review.
*
* Revision 1.15 2002/08/22 17:17:26 schlue
* Testing of threshold for svalue against DB schema implemented.
* New utility operation for determining column length added.
* Optional oracle test environment.
*
* Revision 1.14 2002/08/22 10:58:53 schlue
* Utility method to determine column length added.
*
* Revision 1.13 2002/08/22 08:36:07 schlue
* Max key length added.
* Garbage collection optimzed.
*
* Revision 1.12 2002/08/20 09:34:34 lipp
* Removed static.
*
* Revision 1.11 2002/08/20 08:47:49 schlue
* Default value for maximum key length added.
* Advanced error/warning logging.
*
* Revision 1.10 2002/08/19 19:23:40 lipp
* Various minor improvements.
*
* Revision 1.9 2002/08/19 15:15:04 lipp
* Added logger.
*
* Revision 1.8 2002/08/19 13:38:50 schlue
* Determination of maximum key length added.
* clear() operation optimized.
*
* Revision 1.7 2002/08/17 19:00:57 lipp
* Made map id changeable.
*
* Revision 1.6 2002/08/15 16:57:20 schlue
* Performance optimization: Optional usage of statement batches.
*
* Revision 1.5 2002/04/03 12:53:04 lipp
* JavaDoc fixes.
*
* Revision 1.4 2001/12/10 09:15:17 schlue
* javadoc warnings corrected
*
* Revision 1.3 2001/12/07 14:39:54 schlue
* Modifications acc. to review
*
* Revision 1.2 2001/12/07 12:33:17 schlue
* Implementation of persistent map finished
*
* Revision 1.1 2001/12/04 15:37:51 lipp
* New package for persistent maps.
*
*/
package de.danet.an.util.persistentmaps;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import de.danet.an.util.JDBCUtil;
import de.danet.an.util.PersistentMap;
import de.danet.an.util.UniversalPrepStmt;
import de.danet.an.util.persistentmaps.JDBCPersistentMap.DAO.EntryId;
/**
* This class provides an implementation of
* {@link de.danet.an.util.PersistentMap <code>PersistentMap</code>} that
* uses JDBC to implement the persistence.<P>
*
* The class uses an abstract SQL table model. The table consists of
* a column for the map id, the entry key and two value
* entries.<P>
*
* All columns except the second value entry are of type string,
* with lengths as appropriate for the
* application. The second value field is of type BLOB (binary large
* object) and is used if either the value to be stored is not
* of type {@link java.lang.String <code>String</code>} or longer
* than a given limit.
*
* The map id enables storing of several
* jdbc persistent maps in a single table. It is passed as paramter
* to all constructors of this class. Only a single instance for a
* map id may be created as creating two instances may
* corrupt caching.<P>
*
* The SQL statements used to retrieve and save data are configurable.
* Thus any table or view may be used as a base for a jdbc persistent map.<P>
*
* <b>Note that this implementation is not synchronized.</b> If multiple
* threads access this map concurrently, and at least one of the threads
* modifies the map structurally, it must be synchronized externally.
* (A structural modification is any operation that adds or deletes one or more
* mappings; merely changing the value associated with a key that an instance
* already contains is not a structural modification.) This is typically
* accomplished by synchronizing on some object that naturally encapsulates the
* map.
* This can be done at creation time, to prevent accidental unsynchronized
* access to the map:
* <code>JDBCPersistentMap pm = new JDBCPersistentMap(...);</code>
* <code>Map m = Collections.synchronizedMap(pm);</code>
* Since only the methods defined by Map are synchronized by this
* mechanism, all supplemented methods (like 'load' or 'store' have to be
* synchronized on the map object 'm', created above, e.g.:
* <code>synchronized(m) {</code>
* <code> pm.load(); // Must be in the synchronized block</code>
* <code>}</code><P>
*
* The class is serializable. Note, however, that the database connection
* is transient.
*/
public class JDBCPersistentMap extends HashMap
implements PersistentMap, Serializable {
private static final org.apache.commons.logging.Log logger
= org.apache.commons.logging.LogFactory.getLog(JDBCPersistentMap.class);
/**
* The map id used by this map.
*/
private Object id = null;
/**
* The database table name used for this map.
*/
private String dbTableName = null;
/**
* The column name of the "map" key attribute.
* Modifications are triggered by changes to the update statement.
*/
private String mapColumnName = "MAPID";
/**
* The column name of the "item" key attribute.
* Modifications are triggered by changes to the update statement.
*/
private String itemColumnName = "ITEM";
/**
* The SQL statement used to retrieve data.
*/
private String loadStatement = null;
/**
* The SQL statement used to insert data.
*/
private String insertStatement = null;
/**
* The SQL statement used to update data.
*/
private String updateStatement = null;
/**
* The SQL statement used to delete data.
*/
private String deleteStatement = null;
/**
* The SQL statement used to delete all data of a map.
*/
private String deleteAllStatement = null;
/**
* The maximum length of the string value in the database.
*/
private int sValueMax = 256;
/**
* The associated data source.
*/
private transient DataSource dataSource = null;
/**
* The database URL of the current configuration.
*/
private transient String configDbUrl = null;
/**
* Indicated that the dataSource has changed;
*/
private transient boolean dataSourceChanged = true;
/**
* The database connection used for store and load.
*/
private transient Connection dbConnection = null;
/**
* The databse properties.
*/
private transient JDBCUtil.DBProperties dbProps = null;
/** Indicates that map id has never been used before. */
private boolean isNewMap = false;
/**
* Flag, indicating if the current map has already been loaded.
*/
private boolean hasBeenLoaded = false;
/**
* Flag, indicating if the current map has been cleared.
*/
private boolean hasBeenCleared = false;
/**
* Flag, indicating if there are any non-saved modifications.
*/
private boolean isModified = false;
/**
* List of inserted keys to map entries.
*/
private Set insertList = new HashSet();
/**
* List of updated keys to map entries.
*/
private Set updateList = new HashSet();
/**
* List of deleted map entries.
*/
private Set deleteList = new HashSet();
/**
* Prepared statement for loading map entries.
*/
private transient PreparedStatement loadPStmt = null;
/**
* Prepared statement for inserting map entries.
*/
private transient UniversalPrepStmt insertPStmt = null;
/**
* Prepared statement for updating map entries.
*/
private transient UniversalPrepStmt updatePStmt = null;
/**
* Prepared statement for deleting map entries.
*/
private transient PreparedStatement deletePStmt = null;
/**
* Prepared statement for deleting all map entries.
*/
private transient PreparedStatement deleteAllPStmt = null;
/**
* Class for storing string/binary element of a map value.
*/
private class MapValue {
String svalue = null;
Object bvalue = null;
}
/**
* Returns a new JDBC persistent map. The map is formed by all entries
* in the underlying table with the given map id. The
* table used by the default SQL statements is
* <code>DEFAULTPERSISTENCEMAP</code>.
*
* @param dataSource the data source that will be used to obtain
* connections.
* @param mapId the map id.
* @throws SQLException if a database related error occurs
*/
public JDBCPersistentMap (DataSource dataSource, String mapId)
throws SQLException {
this (dataSource, mapId, "DEFAULTPERSISTENCEMAP");
}
/**
* Returns a new JDBC persistent map. The map is formed by all entries
* in the underlying table with the given map id. The
* table used by the default SQL statements is
* <code>DEFAULTPERSISTENCEMAP</code>.
*
* @param dataSource the data source that will be used to obtain
* connections.
* @param mapId the map id.
* @throws SQLException if a database related error occurs
*/
public JDBCPersistentMap (DataSource dataSource, Long mapId)
throws SQLException {
this (dataSource, mapId, "DEFAULTPERSISTENCEMAP");
}
/**
* Returns a new JDBC persistent map. The map is formed by all entries
* in the underlying table with the given persistence group.
*
* @param dataSource the data source that will be used to obtain
* connections.
* @param mapId the map id.
* @param dbtable the table used in the default SQL statements.
* @throws SQLException if a database related error occurs
*/
public JDBCPersistentMap
(DataSource dataSource, String mapId, String dbtable)
throws SQLException {
init (dataSource, mapId, dbtable);
}
/**
* Returns a new JDBC persistent map. The map is formed by all entries
* in the underlying table with the given persistence group.
*
* @param dataSource the data source that will be used to obtain
* connections.
* @param mapId the map id.
* @param dbtable the table used in the default SQL statements.
* @throws SQLException if a database related error occurs
*/
public JDBCPersistentMap
(DataSource dataSource, Long mapId, String dbtable)
throws SQLException {
init (dataSource, mapId, dbtable);
}
private void init (DataSource ds, Object mapId, String dbtable)
throws SQLException {
setDataSource (ds);
id = mapId;
dbTableName = dbtable;
loadStatement = "SELECT ITEM, SVALUE, BVALUE FROM "
+ dbtable + " WHERE MAPID = ?";
insertStatement = "INSERT INTO " + dbtable
+ " (MAPID, ITEM, SVALUE, BVALUE) VALUES (?, ?, ?, ?)";
updateStatement = "UPDATE " + dbtable
+ " SET SVALUE = ?, BVALUE = ? WHERE MAPID = ? AND ITEM = ?";
deleteStatement = "DELETE FROM " + dbtable
+ " WHERE MAPID = ? AND ITEM = ?";
deleteAllStatement = "DELETE FROM " + dbtable
+ " WHERE MAPID = ?";
}
/**
* Set the data source for this map. This must be called after
* deserialization. Calling this method resets the connection and
* must therefore be followed by a call to {@link #setConnection
* <code>setConnection</code>}.
* @param dataSource the data source that will be used to obtain
* connections.
* @throws SQLException if a database related error occurs
*/
public void setDataSource (DataSource dataSource)
throws SQLException {
setConnection (null);
this.dataSource = dataSource;
dataSourceChanged = true;
}
/**
* Sets the map id for this map. Clears the cached modifications
* as a side effect if the map id differs from the current map id.
*
* @param mapId the new map id.
* @see #getMapId
*/
public void setMapId (String mapId) {
if (mapId.equals (id)) {
return;
}
clearModifications ();
id = mapId;
}
/**
* Sets the map id for this map. Clears the cached modifications
* as a side effect if the map id differs from the current map id.
*
* @param mapId the new map id.
* @see #getMapId
*/
public void setMapId (Long mapId) {
if (mapId.equals (id)) {
return;
}
clearModifications ();
id = mapId;
}
private void clearModifications () {
super.clear ();
insertList.clear();
updateList.clear();
deleteList.clear();
hasBeenLoaded = false;
hasBeenCleared = false;
isModified = false;
isNewMap = false;
}
/**
* May be called after constructor or <code>setMapId</code> and
* before the first call to <code>store()</code> to indicate that
* the map is newly created. This allows the persistent map
* implementation to optimize the storage bahaviour.
*/
public void setNewMap () {
isNewMap = true;
}
/**
* Returns the map id of this map.
* @return the map id.
* @see #setMapId
*/
public Object getMapId () {
return id;
}
/**
* Sets the SQL statement used to retrieve all entries for this map
* from the database table. The default is
* <pre>SELECT ITEM, SVALUE, BVALUE FROM <i>table</i> WHERE MAPID = ?</pre>
* where <code><i>table</i></code> defaults to
* <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
* {@link #JDBCPersistentMap(String,String) extended constructor}.
* <P><b>NOTE: The statement must declare semantically corresponding
* columns for all the selection and restriction values described in
* the example above!</b></P>
*
* @param statement the load statement.
* @see #getLoadStatement
*/
public void setLoadStatement(String statement) {
loadStatement = statement;
try {
loadPStmt = (UniversalPrepStmt)
closePreparedStatement(loadPStmt);
} catch (SQLException e) {
logger.warn ("Problem while closing prepared statement: "
+ e.getMessage ());
}
}
/**
* Returns the SQL statement used to retrieve all entries for this
* map from the database table.
*
* @return the SQL statement.
* @see #setLoadStatement
*/
public String getLoadStatement () {
return loadStatement;
}
/**
* Sets the SQL statement used to insert an entry for this map
* in the database table. The default is
* <pre>INSERT INTO <i>table</i> (MAPID, ITEM, SVALUE, BVALUE)
* VALUES (?, ?, ?, ?)</pre>
* where <code><i>table</i></code> defaults to
* <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
* {@link #JDBCPersistentMap(String,String) extended constructor}.
* <P><b>NOTE: The statement must declare semantically corresponding
* columns for all the values described in the example above!</b></P>
*
* @param statement the insert statement.
* @see #getInsertStatement
*/
public void setInsertStatement(String statement) {
insertStatement = statement;
try {
insertPStmt = (UniversalPrepStmt)
closePreparedStatement(insertPStmt);
} catch (SQLException e) {
logger.warn ("Problem while closing prepared statement: "
+ e.getMessage ());
}
}
/**
* Returns the SQL statement used to insert an entry for this
* map into the database table.
*
* @return the SQL statement.
* @see #setInsertStatement
*/
public String getInsertStatement () {
return insertStatement;
}
/**
* Sets the SQL statement used to update an entry for this map in
* the database table. The default is <pre>UPDATE <i>table</i> SET
* SVALUE = ?, BVALUE = ? WHERE MAPID=? AND ITEM=?</pre> where
* <code><i>table</i></code> defaults to
* <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling
* the {@link #JDBCPersistentMap(String,String) extended
* constructor}. <P><b>NOTE: The statement must declare
* semantically corresponding columns for all the data and
* restriction values described in the example above! The names of
* the key columns in the restriction phrase have to be in the
* same order as above (first: map-ident, second: item-ident)</b></P>
*
* @param statement the update statement.
* @see #getUpdateStatement
*/
public void setUpdateStatement(String statement) {
int startWhere = statement.toLowerCase().indexOf("where");
if ( startWhere == -1 ) {
throw new IllegalArgumentException("No WHERE clause in statement.");
}
updateStatement = statement;
try {
updatePStmt = (UniversalPrepStmt)
closePreparedStatement(updatePStmt);
} catch (SQLException e) {
logger.warn ("Problem while closing prepared statement: "
+ e.getMessage ());
}
// Determine key columns names
mapColumnName = null;
itemColumnName = null;
String whereClause = updateStatement.substring(startWhere);
StringTokenizer wtok = new StringTokenizer(whereClause);
int tokCnt = 0;
while (wtok.hasMoreTokens()) {
String tok = wtok.nextToken();
if (tokCnt == 1) {
mapColumnName = tok;
} else if (tokCnt == 5) {
itemColumnName = tok;
}
tokCnt++;
}
if (mapColumnName == null) {
throw new IllegalArgumentException
("Cannot find map name column in WHERE clause of \""
+ statement + "\"");
}
if (itemColumnName == null) {
throw new IllegalArgumentException
("Cannot find item name column in WHERE clause of \""
+ statement + "\"");
}
}
/**
* Returns the SQL statement used to update an entry for this
* map in the database table.
*
* @return the SQL statement.
* @see #setUpdateStatement
*/
public String getUpdateStatement () {
return updateStatement;
}
/**
* Sets the SQL statement used to delete an entry for this map
* in the database table. The default is
* <pre>DELETE FROM <i>table</i> WHERE MAPID=? AND ITEM=?</pre>
* where <code><i>table</i></code> defaults to
* <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
* {@link #JDBCPersistentMap(String,String) extended constructor}.
* <P><b>NOTE: The statement must declare semantically corresponding
* columns for all the restriction values described in the example
* above!</b></P>
*
* @param statement the delete statement.
* @see #getDeleteStatement
*/
public void setDeleteStatement(String statement) {
deleteStatement = statement;
deletePStmt = null;
}
/**
* Returns the SQL statement used to delete an entry for this
* map in the database table.
*
* @return the SQL statement.
* @see #setDeleteStatement
*/
public String getDeleteStatement () {
return deleteStatement;
}
/**
* Sets the SQL statement used to delete all entries for this map
* in the database table. The default is
* <pre>DELETE FROM <i>table</i> WHERE MAPID=?</pre>
* where <code><i>table</i></code> defaults to
* <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
* {@link #JDBCPersistentMap(String,String) extended constructor}.
* <P><b>NOTE: The statement must declare semantically corresponding
* columns for all the restriction values described in the example
* above!</b></P>
*
* @param statement the delete statement.
* @see #getDeleteAllStatement
*/
public void setDeleteAllStatement(String statement) {
deleteAllStatement = statement;
deleteAllPStmt = null;
}
/**
* Returns the SQL statement used to delete all entries for this
* map in the database table.
*
* @return the SQL statement.
* @see #setDeleteAllStatement
*/
public String getDeleteAllStatement () {
return deleteAllStatement;
}
/**
* Sets the maximum length for string value in the
* <code>SVALUE</code> column of the database table. The default
* is <code>256</code>. If the given length value is greater than
* the column length in the database scheme (a DB connection that
* supports column information assumed), the length value is restricted
* to the DB column length.
* @param limit the maximum length for values in the <code>SVALUE</code>
* column.
* @see #getSValueMax
*/
public void setSValueMax (int limit) {
if (limit <= 0) {
throw new IllegalArgumentException
("limit must be greater than zero");
}
if (dbConnection == null) {
sValueMax = limit;
return;
}
try {
int sLength = JDBCUtil.getTableColumnSize
(dbConnection, dbTableName, "SVALUE", limit);
if (limit > sLength) {
logger.warn
("Illegal limit (" + limit + ") for maximum string value. "
+ "Using column length from database schema ("
+ sLength + ").");
sValueMax = sLength;
} else {
sValueMax = limit;
}
} catch (SQLException exc) {
logger.error (exc.getMessage(), exc);
}
}
/**
* Returns the maximum length for string value in the <code>SVALUE</code>
* column of the database table.
*
* @return the current maximum length
* @see #setSValueMax
*/
public int getSValueMax() {
return sValueMax;
}
/**
* Sets the database connection used in {@link #load <code>load</code>}
* and {@link #store <code>store</code>}.
* If connection is closed or changes, all prepared statements are
* released.
* If a valid connection with a different database url (compared with
* the previous valid connection) is set, the svalue threshold is
* validated if it hasn't been checked before (@see #setSValueMax).
* @param con the Connection.
* @throws SQLException from the underlying JDBC calls
* @see #getConnection
*/
public void setConnection (Connection con) throws SQLException {
boolean sameConnection
= ((con == null) && (dbConnection == null))
|| ((con != null) && (dbConnection != null)
&& (dbConnection.equals(con)));
if ((con == null) || !sameConnection) {
// Close all prepared statements
loadPStmt = closePreparedStatement(loadPStmt);
insertPStmt = (UniversalPrepStmt)
closePreparedStatement(insertPStmt);
updatePStmt = (UniversalPrepStmt)
closePreparedStatement(updatePStmt);
deletePStmt = closePreparedStatement(deletePStmt);
deleteAllPStmt = closePreparedStatement(deleteAllPStmt);
}
if ((dataSource == null || dataSourceChanged) && con != null) {
dbProps = JDBCUtil.dbProperties (dataSource, con);
String newDbUrl = null;
boolean sameDb = false;
if (dataSource == null) {
newDbUrl = con.getMetaData().getURL();
sameDb = (configDbUrl != null && newDbUrl.equals (configDbUrl));
configDbUrl = newDbUrl;
}
if (dataSourceChanged || !sameDb) {
// get table column size for "string" column
int sLength = JDBCUtil.getTableColumnSize
(con, dbTableName, "SVALUE", sValueMax);
if (sValueMax > sLength) {
logger.warn
("Illegal limit for maximum string value. "
+ "Using column length from database schema.");
sValueMax = sLength;
}
// Simulate that all current values have just
// been inserted, so that they will be stored with
// the naxt call to "store"
insertList.clear();
updateList.clear();
deleteList.clear();
insertList.addAll(this.keySet());
hasBeenLoaded = false;
}
dataSourceChanged = false;
}
dbConnection = con;
}
/**
* Returns the database connection set by
* {@link #setConnection <code>setConnection</code>}.
*
* @return the current connection
* @see #setConnection
*/
public Connection getConnection () {
return dbConnection;
}
/**
* Retrieves the information about the maximum string length of key
* entries.
* @return the maximum string length of key entries
* @throws IOException problems while storing the map
*/
public int maxKeyLength () throws IOException {
if (dbConnection == null) {
throw new IllegalStateException ("No connection set");
}
final int defaultMaxKeyLength = 50;
try {
// Request keylength value from database schema
int kLength = JDBCUtil.getTableColumnSize
(dbConnection, dbTableName, mapColumnName, defaultMaxKeyLength);
return kLength;
} catch (SQLException exc) {
throw new PersistentMapSQLException(exc);
}
}
/**
* Loads the map from persistent store.
* The map is cleared before loading.
*
* @throws IOException problems while loading the map
*/
public void load() throws IOException {
if (dbConnection == null) {
throw new IllegalStateException ("No connection set");
}
ResultSet result = null;
try {
if (loadPStmt == null ) {
loadPStmt = new UniversalPrepStmt
(dataSource, dbConnection, loadStatement);
}
setMapId (loadPStmt, 1);
result = loadPStmt.executeQuery();
// Remove current entries and then add loaded values
super.clear();
insertList.clear();
updateList.clear();
deleteList.clear();
while (result.next()) {
String key = result.getString(1);
Object value = result.getString(2);
if (value == null) {
try {
value = JDBCUtil.getBinary(result,3);
} catch (ClassNotFoundException exc) {
throw new IOException(exc.getMessage());
}
}
super.put(key, value);
}
result.close();
} catch (SQLException exc) {
throw new PersistentMapSQLException(exc);
}
hasBeenLoaded = true;
isModified = false;
}
/**
* Stores the map in persistent store.
* If the set has not been loaded before, all entries with the current
* map id are deleted from the persistent store first.
* Only modified (new, changed, deleted) map entries are changed in the
* peristent store.
*
* @throws IOException problems while storing the map
*/
public void store() throws IOException {
if (dbConnection == null) {
throw new IllegalStateException ("No connection set");
}
if (!hasBeenLoaded || hasBeenCleared) {
if (!isNewMap) {
/* First delete all existing values for that id in the
* persistent store */
try {
if (deleteAllPStmt == null) {
deleteAllPStmt = new UniversalPrepStmt
(dataSource, dbConnection, deleteAllStatement);
}
setMapId (deleteAllPStmt, 1);
int hits = deleteAllPStmt.executeUpdate();
} catch (SQLException exc) {
throw new PersistentMapSQLException(exc);
}
}
hasBeenLoaded = true;
hasBeenCleared = false;
}
isNewMap = false;
// Iterate through all the modification tracking lists
doDelete();
doInsert();
doUpdate();
isModified = false;
}
/**
* Removes all mappings from this map.
* This implementation ensures consistency between the map and the
* persistent store.
*
*/
public void clear() {
super.clear();
insertList.clear();
updateList.clear();
deleteList.clear();
hasBeenCleared = true;
isModified = false;
}
/**
* Associates the specified value with the specified key in this map.
* The key has to have a non-null value.
* If the map previously contained a mapping for this key, the old value
* is replaced.
* This implementation ensures consistency between the map and the
* persistent store.
*
* @param key key with which the specified value is to be associated.
* @param value value to be associated with the specified key.
* @return previous value associated with specified key, or null if
* there was no mapping for key.
*/
public Object put(Object key, Object value) {
if (key == null || !(key instanceof String)) {
throw new IllegalArgumentException
("Key must be non-null and of type String");
}
Object oldValue = null;
if (containsKey(key)) {
// Existing entry
oldValue = super.put(key, value);
// Changing an inserted entry leaves it in state "inserted"
if (!insertList.contains(key)
&& ((oldValue == null && value != null)
|| (oldValue != null && value == null)
|| (oldValue != null && !oldValue.equals(value)))) {
updateList.add(key);
}
} else {
// New (or prior deleted) entry
// Adding a prior deleted entry brings it to state "updated"
super.put(key, value);
// The modification lists maybe null during readObject of
// a JDBCPersistantMap (for the restauration of the map entries)
if (insertList != null) {
if (deleteList.contains(key)) {
deleteList.remove(key);
updateList.add(key);
} else {
insertList.add(key);
}
}
}
isModified = true;
return oldValue;
}
/**
* Removes the mapping for this key from this map if present.
* This implementation ensures consistency between the map and the
* persistent store.
*
* @param key key with which the specified value is to be associated.
* @return previous value associated with specified key, or null if
* there was no mapping for key.
*/
public Object remove(Object key) {
if (key == null || !containsKey(key)) {
return null;
}
// If it has been inserted just before, remove all traces
if (insertList.contains(key)) {
insertList.remove(key);
} else {
// otherwise mark as removed
// It doesn't matter any more if entry has been updated before
updateList.remove(key);
deleteList.add(key);
}
isModified = true;
return super.remove(key);
}
/**
* Copies all of the mappings from the specified map to this map.
* These mappings will replace any mappings that this map had for any of
* the keys currently in the specified map.
* This implementation ensures consistency between the map and the
* persistent store.
*
* @param t Mappings to be stored in this map.
*/
public void putAll(Map t) {
for (Iterator it = t.keySet().iterator(); it.hasNext();) {
Object key = it.next();
put(key, t.get(key));
}
}
/**
* Indicates whether modifications need to be saved currently.
* @return true, if there are non-saved modifications, otherwise false.
*/
public boolean isModified () {
return isModified;
}
/**
* Performs an update of the entry with the given key in the persistent
* store.
*
* @param withBatch flag, indicating if statement batches should be used
* @throws IOException problems while updating the entry
* @throws IllegalStateException if no connection has been set
*/
private void doUpdate() throws IOException {
try {
if (updateList.size() == 0) {
return;
}
if (updatePStmt == null) {
updatePStmt = new UniversalPrepStmt
(dataSource, dbConnection, updateStatement);
}
Iterator keys = updateList.iterator();
while (keys.hasNext()) {
final String key = (String)keys.next();
final MapValue value = getValue(key);
updatePStmt.setString(1, value.svalue);
updatePStmt.setBinary(2, value.bvalue);
setMapId (updatePStmt, 3);
updatePStmt.setString(4, key);
if (dbProps.supportsBatchUpdates()) {
updatePStmt.addBatch();
} else {
int hits = updatePStmt.executeUpdate();
if (hits != 1) {
throw new IOException("Data store not consistent "
+ "(hits on update = "
+ hits + ")!");
}
}
}
if (dbProps.supportsBatchUpdates()) {
int[] result = updatePStmt.executeBatch();
if (result.length != updateList.size()) {
throw new IOException("Incorrect number of commands "
+ "while executing update batch.");
} else {
for (int i = 0; i < result.length; i++) {
if ((result[i] != 1) && (result[i] != -2)) {
throw new IOException("Data store not consistent "
+ "(hits on update = "
+ result[i] + ")!");
}
}
}
}
} catch (SQLException exc) {
throw new PersistentMapSQLException(exc);
}
updateList.clear();
}
/**
* Deletes entry of the current map with the given key in the
* persistent store.
*
* @throws IOException problems while deleting the entry
* @throws IllegalStateException if no connection has been set
*/
private void doDelete() throws IOException {
try {
if (deleteList.size() == 0) {
return;
}
if (deletePStmt == null) {
deletePStmt = new UniversalPrepStmt
(dataSource, dbConnection, deleteStatement);
}
Iterator keys = deleteList.iterator();
while (keys.hasNext()) {
final String key = (String)keys.next();
setMapId (deletePStmt, 1);
deletePStmt.setString(2, key);
if (dbProps.supportsBatchUpdates()) {
deletePStmt.addBatch();
} else {
int hits = deletePStmt.executeUpdate();
if (hits != 1) {
throw new IOException("Data store not consistent "
+ "(hits on delete = "
+ hits + ")!");
}
}
}
if (dbProps.supportsBatchUpdates()) {
int[] result = deletePStmt.executeBatch();
if (result.length != deleteList.size()) {
throw new IOException("Incorrect number of commands "
+ "while executing delete batch.");
} else {
for (int i = 0; i < result.length; i++) {
if ((result[i] != 1) && (result[i] != -2)) {
throw new IOException("Data store not consistent "
+ "(hits on delete = "
+ result[i] + ")!");
}
}
}
}
} catch (SQLException exc) {
throw new PersistentMapSQLException(exc);
}
deleteList.clear();
}
/**
* Inserts entry of the current map with the given key in the
* persistent store.
*
* @throws IOException problems while inserting the entry
* @throws IllegalStateException if no connection has been set
*/
private void doInsert() throws IOException {
try {
if (insertList.size() == 0) {
return;
}
if (insertPStmt == null) {
insertPStmt = new UniversalPrepStmt
(dataSource, dbConnection, insertStatement,
new String[] { mapColumnName, itemColumnName});
}
Iterator keys = insertList.iterator();
while (keys.hasNext()) {
final String key = (String)keys.next();
final MapValue value = getValue(key);
setMapId (insertPStmt, 1);
insertPStmt.setString(2, key);
insertPStmt.setString(3, value.svalue);
insertPStmt.setBinary(4, value.bvalue);
if (dbProps.supportsBatchUpdates()) {
insertPStmt.addBatch();
} else {
int hits = insertPStmt.executeUpdate();
if (hits != 1) {
throw new IOException("Data store not consistent "
+ "(hits on insert = "
+ hits + ")!");
}
}
}
if (dbProps.supportsBatchUpdates()) {
int[] result = insertPStmt.executeBatch();
if (result.length != insertList.size()) {
throw new IOException("Incorrect number of commands "
+ "while executing insert batch.");
} else {
for (int i = 0; i < result.length; i++) {
if ((result[i] != 1) && (result[i] != -2)) {
throw new IOException("Data store not consistent "
+ "(hits on insert = "
+ result[i] + ")!");
}
}
}
}
} catch (SQLException exc) {
throw new PersistentMapSQLException(exc);
}
insertList.clear();
}
/**
* Verify if the string will be stored as string, not as object.
* @param value the string
* @return <code>true</code> if the string will be stored as "svalue"
*/
protected boolean isSValue (String value) {
return (value.length() <= sValueMax)
&& (!dbProps.isOracle() || ((String)value).length() > 0);
}
/**
* Returns a map value for a given key.
*
* @param key key to retrieve value for.
* @return value for given key.
*/
private MapValue getValue(String key) {
MapValue mapValue = new MapValue();
Object value = get(key);
if ((value instanceof String) && isSValue ((String)value)) {
mapValue.svalue = (String)value;
} else {
mapValue.bvalue = value;
}
return mapValue;
}
/**
* Sets the map id in a prepared statement.
* @param stmt the statement
* @param offset the offset
*/
private void setMapId (PreparedStatement stmt, int offset)
throws SQLException {
if (id instanceof String) {
stmt.setString(offset, (String)id);
} else {
stmt.setLong(offset, ((Long)id).longValue());
}
}
/**
* Closes an existing prepared statement.
*
* @param pStmt the prepared statement to be closed
* @throws SQLException if no connection has been set
* @return always null.
*/
private PreparedStatement closePreparedStatement(PreparedStatement pStmt)
throws SQLException {
if (pStmt != null) {
pStmt.close();
}
return null;
}
public static class DAO {
public static class EntryId implements Serializable {
private long mapId;
private String item;
/**
* @return Returns the mapId.
*/
public long getMapId() {
return mapId;
}
/**
* @param mapId The mapId to set.
*/
public void setMapId(long mapId) {
this.mapId = mapId;
}
/**
* @return Returns the item.
*/
public String getItem() {
return item;
}
/**
* @param item The item to set.
*/
public void setItem(String item) {
this.item = item;
}
/* (non-Javadoc)
* Comment copied from interface or superclass.
*/
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((item == null) ? 0 : item.hashCode());
result = prime * result + (int) (mapId ^ (mapId >>> 32));
return result;
}
/* (non-Javadoc)
* Comment copied from interface or superclass.
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof EntryId)) {
return false;
}
EntryId other = (EntryId) obj;
if (item == null) {
if (other.item != null) {
return false;
}
} else if (!item.equals(other.item)) {
return false;
}
if (mapId != other.mapId) {
return false;
}
return true;
}
}
private long mapId;
private String item;
private String svalue;
private Serializable bvalue;
/**
* Create a new instance with all attributes initialized
* to defaults or the given values.
*
*/
public DAO() {
super();
}
/**
* @return Returns the mapId.
*/
public long getMapId() {
return mapId;
}
/**
* @param mapId The mapId to set.
*/
public void setMapId(long mapId) {
this.mapId = mapId;
}
/**
* @return Returns the item.
*/
public String getItem() {
return item;
}
/**
* @param item The item to set.
*/
public void setItem(String item) {
this.item = item;
}
/**
* @return Returns the svalue.
*/
public String getSvalue() {
return svalue;
}
/**
* @param svalue The svalue to set.
*/
public void setSvalue(String svalue) {
this.svalue = svalue;
}
/**
* @return Returns the bvalue.
*/
public Serializable getBvalue() {
return bvalue;
}
/**
* @param bvalue The bvalue to set.
*/
public void setBvalue(Serializable bvalue) {
this.bvalue = bvalue;
}
}
}