0001: /*
0002: * This file is part of the WfMOpen project.
0003: * Copyright (C) 2001-2003 Danet GmbH (www.danet.de), GS-AN.
0004: * All rights reserved.
0005: *
0006: * This program is free software; you can redistribute it and/or modify
0007: * it under the terms of the GNU General Public License as published by
0008: * the Free Software Foundation; either version 2 of the License, or
0009: * (at your option) any later version.
0010: *
0011: * This program is distributed in the hope that it will be useful,
0012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0014: * GNU General Public License for more details.
0015: *
0016: * You should have received a copy of the GNU General Public License
0017: * along with this program; if not, write to the Free Software
0018: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
0019: *
0020: * $Id: JDBCPersistentMap.java,v 1.5 2007/03/27 21:59:44 mlipp Exp $
0021: *
0022: * $Log: JDBCPersistentMap.java,v $
0023: * Revision 1.5 2007/03/27 21:59:44 mlipp
0024: * Fixed lots of checkstyle warnings.
0025: *
0026: * Revision 1.4 2006/09/29 12:32:13 drmlipp
0027: * Consistently using WfMOpen as projct name now.
0028: *
0029: * Revision 1.3 2005/04/22 15:10:49 drmlipp
0030: * Merged changes from 1.3 branch up to 1.3p15.
0031: *
0032: * Revision 1.1.1.3.6.5 2005/04/14 11:45:07 drmlipp
0033: * More (minor) optimizations.
0034: *
0035: * Revision 1.1.1.3.6.4 2005/04/13 16:14:06 drmlipp
0036: * Optimized db access.
0037: *
0038: * Revision 1.1.1.3.6.3 2005/04/11 14:33:11 drmlipp
0039: * Improved message.
0040: *
0041: * Revision 1.2 2005/04/08 11:28:03 drmlipp
0042: * Merged changes from 1.3 branch up to 1.3p6.
0043: *
0044: * Revision 1.1.1.3.6.2 2005/03/18 21:59:00 drmlipp
0045: * Fixed db access optimization.
0046: *
0047: * Revision 1.1.1.3.6.1 2005/03/18 15:02:00 drmlipp
0048: * Optimized (reduced number of DB accesses).
0049: *
0050: * Revision 1.1.1.3 2004/08/18 15:17:35 drmlipp
0051: * Update to 1.2
0052: *
0053: * Revision 1.37 2004/02/09 14:57:49 lipp
0054: * Added support for map id of type Long.
0055: *
0056: * Revision 1.36 2003/11/27 16:26:09 lipp
0057: * Added specific exception type to allow reconstruction of SQLException
0058: * when using JDBCPersistence.
0059: *
0060: * Revision 1.35 2003/11/03 12:41:23 lipp
0061: * Added null vs. "" support for Oracle.
0062: *
0063: * Revision 1.34 2003/06/27 08:51:46 lipp
0064: * Fixed copyright/license information.
0065: *
0066: * Revision 1.33 2003/05/23 12:11:13 lipp
0067: * Fixed driver/db characteristics caching.
0068: *
0069: * Revision 1.32 2003/04/11 13:21:04 lipp
0070: * Fixed NullPointerException
0071: *
0072: * Revision 1.31 2003/03/28 10:09:20 lipp
0073: * Intriduced logging using commons-logging.
0074: *
0075: * Revision 1.30 2003/03/06 15:23:03 lipp
0076: * Some special handling for oracle.
0077: *
0078: * Revision 1.29 2003/03/06 13:47:45 schlue
0079: * Handling of null values fixed.
0080: *
0081: * Revision 1.28 2003/03/05 13:18:32 schlue
0082: * Fixed column names for map and item removed (now dynamic).
0083: *
0084: * Revision 1.27 2003/03/05 09:42:39 lipp
0085: * Temporary fix for UniversalPrepStmt.
0086: *
0087: * Revision 1.26 2003/02/21 16:38:51 lipp
0088: * Introduced UniversalPrepStmt.
0089: *
0090: * Revision 1.25 2003/02/19 12:23:14 lipp
0091: * Adapted parameter sequence for setBinary to usual style.
0092: *
0093: * Revision 1.24 2003/02/18 15:20:38 schlue
0094: * svalue handling fixed.
0095: *
0096: * Revision 1.23 2003/02/05 15:34:22 lipp
0097: * Removed unnecessary exception.
0098: *
0099: * Revision 1.22 2002/11/19 15:20:31 lipp
0100: * Removed not needed semicolon.
0101: *
0102: * Revision 1.21 2002/10/08 11:57:03 schlue
0103: * Code fixed for changing a connection with non-saved modifications.
0104: * Problems with serialization fixed.
0105: * Formal corrections acc. to style checker.
0106: *
0107: * Revision 1.20 2002/08/30 07:24:25 schlue
0108: * isModified added.
0109: *
0110: * Revision 1.19 2002/08/26 20:23:13 lipp
0111: * Lots of method renames.
0112: *
0113: * Revision 1.18 2002/08/26 14:17:07 lipp
0114: * JavaDoc fixes.
0115: *
0116: * Revision 1.17 2002/08/26 09:43:31 schlue
0117: * Code optimizations.
0118: *
0119: * Revision 1.16 2002/08/23 13:17:09 schlue
0120: * Modifications and fixes acc. to code review.
0121: *
0122: * Revision 1.15 2002/08/22 17:17:26 schlue
0123: * Testing of threshold for svalue against DB schema implemented.
0124: * New utility operation for determining column length added.
0125: * Optional oracle test environment.
0126: *
0127: * Revision 1.14 2002/08/22 10:58:53 schlue
0128: * Utility method to determine column length added.
0129: *
0130: * Revision 1.13 2002/08/22 08:36:07 schlue
0131: * Max key length added.
0132: * Garbage collection optimzed.
0133: *
0134: * Revision 1.12 2002/08/20 09:34:34 lipp
0135: * Removed static.
0136: *
0137: * Revision 1.11 2002/08/20 08:47:49 schlue
0138: * Default value for maximum key length added.
0139: * Advanced error/warning logging.
0140: *
0141: * Revision 1.10 2002/08/19 19:23:40 lipp
0142: * Various minor improvements.
0143: *
0144: * Revision 1.9 2002/08/19 15:15:04 lipp
0145: * Added logger.
0146: *
0147: * Revision 1.8 2002/08/19 13:38:50 schlue
0148: * Determination of maximum key length added.
0149: * clear() operation optimized.
0150: *
0151: * Revision 1.7 2002/08/17 19:00:57 lipp
0152: * Made map id changeable.
0153: *
0154: * Revision 1.6 2002/08/15 16:57:20 schlue
0155: * Performance optimization: Optional usage of statement batches.
0156: *
0157: * Revision 1.5 2002/04/03 12:53:04 lipp
0158: * JavaDoc fixes.
0159: *
0160: * Revision 1.4 2001/12/10 09:15:17 schlue
0161: * javadoc warnings corrected
0162: *
0163: * Revision 1.3 2001/12/07 14:39:54 schlue
0164: * Modifications acc. to review
0165: *
0166: * Revision 1.2 2001/12/07 12:33:17 schlue
0167: * Implementation of persistent map finished
0168: *
0169: * Revision 1.1 2001/12/04 15:37:51 lipp
0170: * New package for persistent maps.
0171: *
0172: */
0173:
0174: package de.danet.an.util.persistentmaps;
0175:
0176: import java.io.IOException;
0177: import java.io.Serializable;
0178:
0179: import java.util.HashMap;
0180: import java.util.HashSet;
0181: import java.util.Iterator;
0182: import java.util.Map;
0183: import java.util.Set;
0184: import java.util.StringTokenizer;
0185:
0186: import java.sql.Connection;
0187: import java.sql.PreparedStatement;
0188: import java.sql.ResultSet;
0189: import java.sql.SQLException;
0190:
0191: import javax.sql.DataSource;
0192:
0193: import de.danet.an.util.JDBCUtil;
0194: import de.danet.an.util.PersistentMap;
0195: import de.danet.an.util.UniversalPrepStmt;
0196:
0197: /**
0198: * This class provides an implementation of
0199: * {@link de.danet.an.util.PersistentMap <code>PersistentMap</code>} that
0200: * uses JDBC to implement the persistence.<P>
0201: *
0202: * The class uses an abstract SQL table model. The table consists of
0203: * a column for the map id, the entry key and two value
0204: * entries.<P>
0205: *
0206: * All columns except the second value entry are of type string,
0207: * with lengths as appropriate for the
0208: * application. The second value field is of type BLOB (binary large
0209: * object) and is used if either the value to be stored is not
0210: * of type {@link java.lang.String <code>String</code>} or longer
0211: * than a given limit.
0212: *
0213: * The map id enables storing of several
0214: * jdbc persistent maps in a single table. It is passed as paramter
0215: * to all constructors of this class. Only a single instance for a
0216: * map id may be created as creating two instances may
0217: * corrupt caching.<P>
0218: *
0219: * The SQL statements used to retrieve and save data are configurable.
0220: * Thus any table or view may be used as a base for a jdbc persistent map.<P>
0221: *
0222: * <b>Note that this implementation is not synchronized.</b> If multiple
0223: * threads access this map concurrently, and at least one of the threads
0224: * modifies the map structurally, it must be synchronized externally.
0225: * (A structural modification is any operation that adds or deletes one or more
0226: * mappings; merely changing the value associated with a key that an instance
0227: * already contains is not a structural modification.) This is typically
0228: * accomplished by synchronizing on some object that naturally encapsulates the
0229: * map.
0230: * This can be done at creation time, to prevent accidental unsynchronized
0231: * access to the map:
0232: * <code>JDBCPersistentMap pm = new JDBCPersistentMap(...);</code>
0233: * <code>Map m = Collections.synchronizedMap(pm);</code>
0234: * Since only the methods defined by Map are synchronized by this
0235: * mechanism, all supplemented methods (like 'load' or 'store' have to be
0236: * synchronized on the map object 'm', created above, e.g.:
0237: * <code>synchronized(m) {</code>
0238: * <code> pm.load(); // Must be in the synchronized block</code>
0239: * <code>}</code><P>
0240: *
0241: * The class is serializable. Note, however, that the database connection
0242: * is transient.
0243: */
0244: public class JDBCPersistentMap extends HashMap implements
0245: PersistentMap, Serializable {
0246: private static final org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory
0247: .getLog(JDBCPersistentMap.class);
0248:
0249: /**
0250: * The map id used by this map.
0251: */
0252: private Object id = null;
0253:
0254: /**
0255: * The database table name used for this map.
0256: */
0257: private String dbTableName = null;
0258:
0259: /**
0260: * The column name of the "map" key attribute.
0261: * Modifications are triggered by changes to the update statement.
0262: */
0263: private String mapColumnName = "MAPID";
0264:
0265: /**
0266: * The column name of the "item" key attribute.
0267: * Modifications are triggered by changes to the update statement.
0268: */
0269: private String itemColumnName = "ITEM";
0270:
0271: /**
0272: * The SQL statement used to retrieve data.
0273: */
0274: private String loadStatement = null;
0275:
0276: /**
0277: * The SQL statement used to insert data.
0278: */
0279: private String insertStatement = null;
0280:
0281: /**
0282: * The SQL statement used to update data.
0283: */
0284: private String updateStatement = null;
0285:
0286: /**
0287: * The SQL statement used to delete data.
0288: */
0289: private String deleteStatement = null;
0290:
0291: /**
0292: * The SQL statement used to delete all data of a map.
0293: */
0294: private String deleteAllStatement = null;
0295:
0296: /**
0297: * The maximum length of the string value in the database.
0298: */
0299: private int sValueMax = 256;
0300:
0301: /**
0302: * The associated data source.
0303: */
0304: private transient DataSource dataSource = null;
0305:
0306: /**
0307: * The database URL of the current configuration.
0308: */
0309: private transient String configDbUrl = null;
0310:
0311: /**
0312: * Indicated that the dataSource has changed;
0313: */
0314: private transient boolean dataSourceChanged = true;
0315:
0316: /**
0317: * The database connection used for store and load.
0318: */
0319: private transient Connection dbConnection = null;
0320:
0321: /**
0322: * The databse properties.
0323: */
0324: private transient JDBCUtil.DBProperties dbProps = null;
0325:
0326: /** Indicates that map id has never been used before. */
0327: private boolean isNewMap = false;
0328:
0329: /**
0330: * Flag, indicating if the current map has already been loaded.
0331: */
0332: private boolean hasBeenLoaded = false;
0333:
0334: /**
0335: * Flag, indicating if the current map has been cleared.
0336: */
0337: private boolean hasBeenCleared = false;
0338:
0339: /**
0340: * Flag, indicating if there are any non-saved modifications.
0341: */
0342: private boolean isModified = false;
0343:
0344: /**
0345: * List of inserted keys to map entries.
0346: */
0347: private Set insertList = new HashSet();
0348:
0349: /**
0350: * List of updated keys to map entries.
0351: */
0352: private Set updateList = new HashSet();
0353:
0354: /**
0355: * List of deleted map entries.
0356: */
0357: private Set deleteList = new HashSet();
0358:
0359: /**
0360: * Prepared statement for loading map entries.
0361: */
0362: private transient PreparedStatement loadPStmt = null;
0363:
0364: /**
0365: * Prepared statement for inserting map entries.
0366: */
0367: private transient UniversalPrepStmt insertPStmt = null;
0368:
0369: /**
0370: * Prepared statement for updating map entries.
0371: */
0372: private transient UniversalPrepStmt updatePStmt = null;
0373:
0374: /**
0375: * Prepared statement for deleting map entries.
0376: */
0377: private transient PreparedStatement deletePStmt = null;
0378:
0379: /**
0380: * Prepared statement for deleting all map entries.
0381: */
0382: private transient PreparedStatement deleteAllPStmt = null;
0383:
0384: /**
0385: * Class for storing string/binary element of a map value.
0386: */
0387: private class MapValue {
0388: String svalue = null;
0389: Object bvalue = null;
0390: }
0391:
0392: /**
0393: * Returns a new JDBC persistent map. The map is formed by all entries
0394: * in the underlying table with the given map id. The
0395: * table used by the default SQL statements is
0396: * <code>DEFAULTPERSISTENCEMAP</code>.
0397: *
0398: * @param dataSource the data source that will be used to obtain
0399: * connections.
0400: * @param mapId the map id.
0401: * @throws SQLException if a database related error occurs
0402: */
0403: public JDBCPersistentMap(DataSource dataSource, String mapId)
0404: throws SQLException {
0405: this (dataSource, mapId, "DEFAULTPERSISTENCEMAP");
0406: }
0407:
0408: /**
0409: * Returns a new JDBC persistent map. The map is formed by all entries
0410: * in the underlying table with the given map id. The
0411: * table used by the default SQL statements is
0412: * <code>DEFAULTPERSISTENCEMAP</code>.
0413: *
0414: * @param dataSource the data source that will be used to obtain
0415: * connections.
0416: * @param mapId the map id.
0417: * @throws SQLException if a database related error occurs
0418: */
0419: public JDBCPersistentMap(DataSource dataSource, Long mapId)
0420: throws SQLException {
0421: this (dataSource, mapId, "DEFAULTPERSISTENCEMAP");
0422: }
0423:
0424: /**
0425: * Returns a new JDBC persistent map. The map is formed by all entries
0426: * in the underlying table with the given persistence group.
0427: *
0428: * @param dataSource the data source that will be used to obtain
0429: * connections.
0430: * @param mapId the map id.
0431: * @param dbtable the table used in the default SQL statements.
0432: * @throws SQLException if a database related error occurs
0433: */
0434: public JDBCPersistentMap(DataSource dataSource, String mapId,
0435: String dbtable) throws SQLException {
0436: init(dataSource, mapId, dbtable);
0437: }
0438:
0439: /**
0440: * Returns a new JDBC persistent map. The map is formed by all entries
0441: * in the underlying table with the given persistence group.
0442: *
0443: * @param dataSource the data source that will be used to obtain
0444: * connections.
0445: * @param mapId the map id.
0446: * @param dbtable the table used in the default SQL statements.
0447: * @throws SQLException if a database related error occurs
0448: */
0449: public JDBCPersistentMap(DataSource dataSource, Long mapId,
0450: String dbtable) throws SQLException {
0451: init(dataSource, mapId, dbtable);
0452: }
0453:
0454: private void init(DataSource ds, Object mapId, String dbtable)
0455: throws SQLException {
0456: setDataSource(ds);
0457: id = mapId;
0458: dbTableName = dbtable;
0459: loadStatement = "SELECT ITEM, SVALUE, BVALUE FROM " + dbtable
0460: + " WHERE MAPID = ?";
0461: insertStatement = "INSERT INTO " + dbtable
0462: + " (MAPID, ITEM, SVALUE, BVALUE) VALUES (?, ?, ?, ?)";
0463: updateStatement = "UPDATE "
0464: + dbtable
0465: + " SET SVALUE = ?, BVALUE = ? WHERE MAPID = ? AND ITEM = ?";
0466: deleteStatement = "DELETE FROM " + dbtable
0467: + " WHERE MAPID = ? AND ITEM = ?";
0468: deleteAllStatement = "DELETE FROM " + dbtable
0469: + " WHERE MAPID = ?";
0470: }
0471:
0472: /**
0473: * Set the data source for this map. This must be called after
0474: * deserialization. Calling this method resets the connection and
0475: * must therefore be followed by a call to {@link #setConnection
0476: * <code>setConnection</code>}.
0477: * @param dataSource the data source that will be used to obtain
0478: * connections.
0479: * @throws SQLException if a database related error occurs
0480: */
0481: public void setDataSource(DataSource dataSource)
0482: throws SQLException {
0483: setConnection(null);
0484: this .dataSource = dataSource;
0485: dataSourceChanged = true;
0486: }
0487:
0488: /**
0489: * Sets the map id for this map. Clears the cached modifications
0490: * as a side effect if the map id differs from the current map id.
0491: *
0492: * @param mapId the new map id.
0493: * @see #getMapId
0494: */
0495: public void setMapId(String mapId) {
0496: if (mapId.equals(id)) {
0497: return;
0498: }
0499: clearModifications();
0500: id = mapId;
0501: }
0502:
0503: /**
0504: * Sets the map id for this map. Clears the cached modifications
0505: * as a side effect if the map id differs from the current map id.
0506: *
0507: * @param mapId the new map id.
0508: * @see #getMapId
0509: */
0510: public void setMapId(Long mapId) {
0511: if (mapId.equals(id)) {
0512: return;
0513: }
0514: clearModifications();
0515: id = mapId;
0516: }
0517:
0518: private void clearModifications() {
0519: super .clear();
0520: insertList.clear();
0521: updateList.clear();
0522: deleteList.clear();
0523: hasBeenLoaded = false;
0524: hasBeenCleared = false;
0525: isModified = false;
0526: isNewMap = false;
0527: }
0528:
0529: /**
0530: * May be called after constructor or <code>setMapId</code> and
0531: * before the first call to <code>store()</code> to indicate that
0532: * the map is newly created. This allows the persistent map
0533: * implementation to optimize the storage bahaviour.
0534: */
0535: public void setNewMap() {
0536: isNewMap = true;
0537: }
0538:
0539: /**
0540: * Returns the map id of this map.
0541: * @return the map id.
0542: * @see #setMapId
0543: */
0544: public Object getMapId() {
0545: return id;
0546: }
0547:
0548: /**
0549: * Sets the SQL statement used to retrieve all entries for this map
0550: * from the database table. The default is
0551: * <pre>SELECT ITEM, SVALUE, BVALUE FROM <i>table</i> WHERE MAPID = ?</pre>
0552: * where <code><i>table</i></code> defaults to
0553: * <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
0554: * {@link #JDBCPersistentMap(String,String) extended constructor}.
0555: * <P><b>NOTE: The statement must declare semantically corresponding
0556: * columns for all the selection and restriction values described in
0557: * the example above!</b></P>
0558: *
0559: * @param statement the load statement.
0560: * @see #getLoadStatement
0561: */
0562: public void setLoadStatement(String statement) {
0563: loadStatement = statement;
0564: try {
0565: loadPStmt = (UniversalPrepStmt) closePreparedStatement(loadPStmt);
0566: } catch (SQLException e) {
0567: logger.warn("Problem while closing prepared statement: "
0568: + e.getMessage());
0569: }
0570: }
0571:
0572: /**
0573: * Returns the SQL statement used to retrieve all entries for this
0574: * map from the database table.
0575: *
0576: * @return the SQL statement.
0577: * @see #setLoadStatement
0578: */
0579: public String getLoadStatement() {
0580: return loadStatement;
0581: }
0582:
0583: /**
0584: * Sets the SQL statement used to insert an entry for this map
0585: * in the database table. The default is
0586: * <pre>INSERT INTO <i>table</i> (MAPID, ITEM, SVALUE, BVALUE)
0587: * VALUES (?, ?, ?, ?)</pre>
0588: * where <code><i>table</i></code> defaults to
0589: * <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
0590: * {@link #JDBCPersistentMap(String,String) extended constructor}.
0591: * <P><b>NOTE: The statement must declare semantically corresponding
0592: * columns for all the values described in the example above!</b></P>
0593: *
0594: * @param statement the insert statement.
0595: * @see #getInsertStatement
0596: */
0597: public void setInsertStatement(String statement) {
0598: insertStatement = statement;
0599: try {
0600: insertPStmt = (UniversalPrepStmt) closePreparedStatement(insertPStmt);
0601: } catch (SQLException e) {
0602: logger.warn("Problem while closing prepared statement: "
0603: + e.getMessage());
0604: }
0605: }
0606:
0607: /**
0608: * Returns the SQL statement used to insert an entry for this
0609: * map into the database table.
0610: *
0611: * @return the SQL statement.
0612: * @see #setInsertStatement
0613: */
0614: public String getInsertStatement() {
0615: return insertStatement;
0616: }
0617:
0618: /**
0619: * Sets the SQL statement used to update an entry for this map in
0620: * the database table. The default is <pre>UPDATE <i>table</i> SET
0621: * SVALUE = ?, BVALUE = ? WHERE MAPID=? AND ITEM=?</pre> where
0622: * <code><i>table</i></code> defaults to
0623: * <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling
0624: * the {@link #JDBCPersistentMap(String,String) extended
0625: * constructor}. <P><b>NOTE: The statement must declare
0626: * semantically corresponding columns for all the data and
0627: * restriction values described in the example above! The names of
0628: * the key columns in the restriction phrase have to be in the
0629: * same order as above (first: map-ident, second: item-ident)</b></P>
0630: *
0631: * @param statement the update statement.
0632: * @see #getUpdateStatement
0633: */
0634: public void setUpdateStatement(String statement) {
0635: int startWhere = statement.toLowerCase().indexOf("where");
0636: if (startWhere == -1) {
0637: throw new IllegalArgumentException(
0638: "No WHERE clause in statement.");
0639: }
0640: updateStatement = statement;
0641: try {
0642: updatePStmt = (UniversalPrepStmt) closePreparedStatement(updatePStmt);
0643: } catch (SQLException e) {
0644: logger.warn("Problem while closing prepared statement: "
0645: + e.getMessage());
0646: }
0647: // Determine key columns names
0648: mapColumnName = null;
0649: itemColumnName = null;
0650: String whereClause = updateStatement.substring(startWhere);
0651: StringTokenizer wtok = new StringTokenizer(whereClause);
0652: int tokCnt = 0;
0653: while (wtok.hasMoreTokens()) {
0654: String tok = wtok.nextToken();
0655: if (tokCnt == 1) {
0656: mapColumnName = tok;
0657: } else if (tokCnt == 5) {
0658: itemColumnName = tok;
0659: }
0660: tokCnt++;
0661: }
0662: if (mapColumnName == null) {
0663: throw new IllegalArgumentException(
0664: "Cannot find map name column in WHERE clause of \""
0665: + statement + "\"");
0666: }
0667: if (itemColumnName == null) {
0668: throw new IllegalArgumentException(
0669: "Cannot find item name column in WHERE clause of \""
0670: + statement + "\"");
0671: }
0672: }
0673:
0674: /**
0675: * Returns the SQL statement used to update an entry for this
0676: * map in the database table.
0677: *
0678: * @return the SQL statement.
0679: * @see #setUpdateStatement
0680: */
0681: public String getUpdateStatement() {
0682: return updateStatement;
0683: }
0684:
0685: /**
0686: * Sets the SQL statement used to delete an entry for this map
0687: * in the database table. The default is
0688: * <pre>DELETE FROM <i>table</i> WHERE MAPID=? AND ITEM=?</pre>
0689: * where <code><i>table</i></code> defaults to
0690: * <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
0691: * {@link #JDBCPersistentMap(String,String) extended constructor}.
0692: * <P><b>NOTE: The statement must declare semantically corresponding
0693: * columns for all the restriction values described in the example
0694: * above!</b></P>
0695: *
0696: * @param statement the delete statement.
0697: * @see #getDeleteStatement
0698: */
0699: public void setDeleteStatement(String statement) {
0700: deleteStatement = statement;
0701: deletePStmt = null;
0702: }
0703:
0704: /**
0705: * Returns the SQL statement used to delete an entry for this
0706: * map in the database table.
0707: *
0708: * @return the SQL statement.
0709: * @see #setDeleteStatement
0710: */
0711: public String getDeleteStatement() {
0712: return deleteStatement;
0713: }
0714:
0715: /**
0716: * Sets the SQL statement used to delete all entries for this map
0717: * in the database table. The default is
0718: * <pre>DELETE FROM <i>table</i> WHERE MAPID=?</pre>
0719: * where <code><i>table</i></code> defaults to
0720: * <code>DEFAULTPERSISTENCEMAP</code> and can be set by calling the
0721: * {@link #JDBCPersistentMap(String,String) extended constructor}.
0722: * <P><b>NOTE: The statement must declare semantically corresponding
0723: * columns for all the restriction values described in the example
0724: * above!</b></P>
0725: *
0726: * @param statement the delete statement.
0727: * @see #getDeleteAllStatement
0728: */
0729: public void setDeleteAllStatement(String statement) {
0730: deleteAllStatement = statement;
0731: deleteAllPStmt = null;
0732: }
0733:
0734: /**
0735: * Returns the SQL statement used to delete all entries for this
0736: * map in the database table.
0737: *
0738: * @return the SQL statement.
0739: * @see #setDeleteAllStatement
0740: */
0741: public String getDeleteAllStatement() {
0742: return deleteAllStatement;
0743: }
0744:
0745: /**
0746: * Sets the maximum length for string value in the
0747: * <code>SVALUE</code> column of the database table. The default
0748: * is <code>256</code>. If the given length value is greater than
0749: * the column length in the database scheme (a DB connection that
0750: * supports column information assumed), the length value is restricted
0751: * to the DB column length.
0752: * @param limit the maximum length for values in the <code>SVALUE</code>
0753: * column.
0754: * @see #getSValueMax
0755: */
0756: public void setSValueMax(int limit) {
0757: if (limit <= 0) {
0758: throw new IllegalArgumentException(
0759: "limit must be greater than zero");
0760: }
0761: if (dbConnection == null) {
0762: sValueMax = limit;
0763: return;
0764: }
0765: try {
0766: int sLength = JDBCUtil.getTableColumnSize(dbConnection,
0767: dbTableName, "SVALUE", limit);
0768: if (limit > sLength) {
0769: logger.warn("Illegal limit (" + limit
0770: + ") for maximum string value. "
0771: + "Using column length from database schema ("
0772: + sLength + ").");
0773: sValueMax = sLength;
0774: } else {
0775: sValueMax = limit;
0776: }
0777: } catch (SQLException exc) {
0778: logger.error(exc.getMessage(), exc);
0779: }
0780: }
0781:
0782: /**
0783: * Returns the maximum length for string value in the <code>SVALUE</code>
0784: * column of the database table.
0785: *
0786: * @return the current maximum length
0787: * @see #setSValueMax
0788: */
0789: public int getSValueMax() {
0790: return sValueMax;
0791: }
0792:
0793: /**
0794: * Sets the database connection used in {@link #load <code>load</code>}
0795: * and {@link #store <code>store</code>}.
0796: * If connection is closed or changes, all prepared statements are
0797: * released.
0798: * If a valid connection with a different database url (compared with
0799: * the previous valid connection) is set, the svalue threshold is
0800: * validated if it hasn't been checked before (@see #setSValueMax).
0801: * @param con the Connection.
0802: * @throws SQLException from the underlying JDBC calls
0803: * @see #getConnection
0804: */
0805: public void setConnection(Connection con) throws SQLException {
0806: boolean sameConnection = ((con == null) && (dbConnection == null))
0807: || ((con != null) && (dbConnection != null) && (dbConnection
0808: .equals(con)));
0809: if ((con == null) || !sameConnection) {
0810: // Close all prepared statements
0811: loadPStmt = closePreparedStatement(loadPStmt);
0812: insertPStmt = (UniversalPrepStmt) closePreparedStatement(insertPStmt);
0813: updatePStmt = (UniversalPrepStmt) closePreparedStatement(updatePStmt);
0814: deletePStmt = closePreparedStatement(deletePStmt);
0815: deleteAllPStmt = closePreparedStatement(deleteAllPStmt);
0816: }
0817: if ((dataSource == null || dataSourceChanged) && con != null) {
0818: dbProps = JDBCUtil.dbProperties(dataSource, con);
0819: String newDbUrl = null;
0820: boolean sameDb = false;
0821: if (dataSource == null) {
0822: newDbUrl = con.getMetaData().getURL();
0823: sameDb = (configDbUrl != null && newDbUrl
0824: .equals(configDbUrl));
0825: configDbUrl = newDbUrl;
0826: }
0827: if (dataSourceChanged || !sameDb) {
0828: // get table column size for "string" column
0829: int sLength = JDBCUtil.getTableColumnSize(con,
0830: dbTableName, "SVALUE", sValueMax);
0831: if (sValueMax > sLength) {
0832: logger
0833: .warn("Illegal limit for maximum string value. "
0834: + "Using column length from database schema.");
0835: sValueMax = sLength;
0836: }
0837: // Simulate that all current values have just
0838: // been inserted, so that they will be stored with
0839: // the naxt call to "store"
0840: insertList.clear();
0841: updateList.clear();
0842: deleteList.clear();
0843: insertList.addAll(this .keySet());
0844: hasBeenLoaded = false;
0845: }
0846: dataSourceChanged = false;
0847: }
0848: dbConnection = con;
0849: }
0850:
0851: /**
0852: * Returns the database connection set by
0853: * {@link #setConnection <code>setConnection</code>}.
0854: *
0855: * @return the current connection
0856: * @see #setConnection
0857: */
0858: public Connection getConnection() {
0859: return dbConnection;
0860: }
0861:
0862: /**
0863: * Retrieves the information about the maximum string length of key
0864: * entries.
0865: * @return the maximum string length of key entries
0866: * @throws IOException problems while storing the map
0867: */
0868: public int maxKeyLength() throws IOException {
0869: if (dbConnection == null) {
0870: throw new IllegalStateException("No connection set");
0871: }
0872: final int defaultMaxKeyLength = 50;
0873: try {
0874: // Request keylength value from database schema
0875: int kLength = JDBCUtil.getTableColumnSize(dbConnection,
0876: dbTableName, mapColumnName, defaultMaxKeyLength);
0877: return kLength;
0878: } catch (SQLException exc) {
0879: throw new PersistentMapSQLException(exc);
0880: }
0881: }
0882:
0883: /**
0884: * Loads the map from persistent store.
0885: * The map is cleared before loading.
0886: *
0887: * @throws IOException problems while loading the map
0888: */
0889: public void load() throws IOException {
0890: if (dbConnection == null) {
0891: throw new IllegalStateException("No connection set");
0892: }
0893: ResultSet result = null;
0894: try {
0895: if (loadPStmt == null) {
0896: loadPStmt = new UniversalPrepStmt(dataSource,
0897: dbConnection, loadStatement);
0898: }
0899: setMapId(loadPStmt, 1);
0900: result = loadPStmt.executeQuery();
0901: // Remove current entries and then add loaded values
0902: super .clear();
0903: insertList.clear();
0904: updateList.clear();
0905: deleteList.clear();
0906: while (result.next()) {
0907: String key = result.getString(1);
0908: Object value = result.getString(2);
0909: if (value == null) {
0910: try {
0911: value = JDBCUtil.getBinary(result, 3);
0912: } catch (ClassNotFoundException exc) {
0913: throw new IOException(exc.getMessage());
0914: }
0915: }
0916: super .put(key, value);
0917: }
0918: result.close();
0919: } catch (SQLException exc) {
0920: throw new PersistentMapSQLException(exc);
0921: }
0922: hasBeenLoaded = true;
0923: isModified = false;
0924: }
0925:
0926: /**
0927: * Stores the map in persistent store.
0928: * If the set has not been loaded before, all entries with the current
0929: * map id are deleted from the persistent store first.
0930: * Only modified (new, changed, deleted) map entries are changed in the
0931: * peristent store.
0932: *
0933: * @throws IOException problems while storing the map
0934: */
0935: public void store() throws IOException {
0936: if (dbConnection == null) {
0937: throw new IllegalStateException("No connection set");
0938: }
0939: if (!hasBeenLoaded || hasBeenCleared) {
0940: if (!isNewMap) {
0941: /* First delete all existing values for that id in the
0942: * persistent store */
0943: try {
0944: if (deleteAllPStmt == null) {
0945: deleteAllPStmt = new UniversalPrepStmt(
0946: dataSource, dbConnection,
0947: deleteAllStatement);
0948: }
0949: setMapId(deleteAllPStmt, 1);
0950: int hits = deleteAllPStmt.executeUpdate();
0951: } catch (SQLException exc) {
0952: throw new PersistentMapSQLException(exc);
0953: }
0954: }
0955: hasBeenLoaded = true;
0956: hasBeenCleared = false;
0957: }
0958: isNewMap = false;
0959: // Iterate through all the modification tracking lists
0960: doDelete();
0961: doInsert();
0962: doUpdate();
0963: isModified = false;
0964: }
0965:
0966: /**
0967: * Removes all mappings from this map.
0968: * This implementation ensures consistency between the map and the
0969: * persistent store.
0970: *
0971: */
0972: public void clear() {
0973: super .clear();
0974: insertList.clear();
0975: updateList.clear();
0976: deleteList.clear();
0977: hasBeenCleared = true;
0978: isModified = false;
0979: }
0980:
0981: /**
0982: * Associates the specified value with the specified key in this map.
0983: * The key has to have a non-null value.
0984: * If the map previously contained a mapping for this key, the old value
0985: * is replaced.
0986: * This implementation ensures consistency between the map and the
0987: * persistent store.
0988: *
0989: * @param key key with which the specified value is to be associated.
0990: * @param value value to be associated with the specified key.
0991: * @return previous value associated with specified key, or null if
0992: * there was no mapping for key.
0993: */
0994: public Object put(Object key, Object value) {
0995: if (key == null || !(key instanceof String)) {
0996: throw new IllegalArgumentException(
0997: "Key must be non-null and of type String");
0998: }
0999: Object oldValue = null;
1000: if (containsKey(key)) {
1001: // Existing entry
1002: oldValue = super .put(key, value);
1003: // Changing an inserted entry leaves it in state "inserted"
1004: if (!insertList.contains(key)
1005: && ((oldValue == null && value != null)
1006: || (oldValue != null && value == null) || (oldValue != null && !oldValue
1007: .equals(value)))) {
1008: updateList.add(key);
1009: }
1010: } else {
1011: // New (or prior deleted) entry
1012: // Adding a prior deleted entry brings it to state "updated"
1013: super .put(key, value);
1014: // The modification lists maybe null during readObject of
1015: // a JDBCPersistantMap (for the restauration of the map entries)
1016: if (insertList != null) {
1017: if (deleteList.contains(key)) {
1018: deleteList.remove(key);
1019: updateList.add(key);
1020: } else {
1021: insertList.add(key);
1022: }
1023: }
1024: }
1025: isModified = true;
1026: return oldValue;
1027: }
1028:
1029: /**
1030: * Removes the mapping for this key from this map if present.
1031: * This implementation ensures consistency between the map and the
1032: * persistent store.
1033: *
1034: * @param key key with which the specified value is to be associated.
1035: * @return previous value associated with specified key, or null if
1036: * there was no mapping for key.
1037: */
1038: public Object remove(Object key) {
1039: if (key == null || !containsKey(key)) {
1040: return null;
1041: }
1042: // If it has been inserted just before, remove all traces
1043: if (insertList.contains(key)) {
1044: insertList.remove(key);
1045: } else {
1046: // otherwise mark as removed
1047: // It doesn't matter any more if entry has been updated before
1048: updateList.remove(key);
1049: deleteList.add(key);
1050: }
1051: isModified = true;
1052: return super .remove(key);
1053: }
1054:
1055: /**
1056: * Copies all of the mappings from the specified map to this map.
1057: * These mappings will replace any mappings that this map had for any of
1058: * the keys currently in the specified map.
1059: * This implementation ensures consistency between the map and the
1060: * persistent store.
1061: *
1062: * @param t Mappings to be stored in this map.
1063: */
1064: public void putAll(Map t) {
1065: for (Iterator it = t.keySet().iterator(); it.hasNext();) {
1066: Object key = it.next();
1067: put(key, t.get(key));
1068: }
1069: }
1070:
1071: /**
1072: * Indicates whether modifications need to be saved currently.
1073: * @return true, if there are non-saved modifications, otherwise false.
1074: */
1075: public boolean isModified() {
1076: return isModified;
1077: }
1078:
1079: /**
1080: * Performs an update of the entry with the given key in the persistent
1081: * store.
1082: *
1083: * @param withBatch flag, indicating if statement batches should be used
1084: * @throws IOException problems while updating the entry
1085: * @throws IllegalStateException if no connection has been set
1086: */
1087: private void doUpdate() throws IOException {
1088: try {
1089: if (updateList.size() == 0) {
1090: return;
1091: }
1092: if (updatePStmt == null) {
1093: updatePStmt = new UniversalPrepStmt(dataSource,
1094: dbConnection, updateStatement);
1095: }
1096: Iterator keys = updateList.iterator();
1097: while (keys.hasNext()) {
1098: final String key = (String) keys.next();
1099: final MapValue value = getValue(key);
1100: updatePStmt.setString(1, value.svalue);
1101: updatePStmt.setBinary(2, value.bvalue);
1102: setMapId(updatePStmt, 3);
1103: updatePStmt.setString(4, key);
1104: if (dbProps.supportsBatchUpdates()) {
1105: updatePStmt.addBatch();
1106: } else {
1107: int hits = updatePStmt.executeUpdate();
1108: if (hits != 1) {
1109: throw new IOException(
1110: "Data store not consistent "
1111: + "(hits on update = " + hits
1112: + ")!");
1113: }
1114: }
1115: }
1116: if (dbProps.supportsBatchUpdates()) {
1117: int[] result = updatePStmt.executeBatch();
1118: if (result.length != updateList.size()) {
1119: throw new IOException(
1120: "Incorrect number of commands "
1121: + "while executing update batch.");
1122: } else {
1123: for (int i = 0; i < result.length; i++) {
1124: if ((result[i] != 1) && (result[i] != -2)) {
1125: throw new IOException(
1126: "Data store not consistent "
1127: + "(hits on update = "
1128: + result[i] + ")!");
1129: }
1130: }
1131: }
1132: }
1133: } catch (SQLException exc) {
1134: throw new PersistentMapSQLException(exc);
1135: }
1136: updateList.clear();
1137: }
1138:
1139: /**
1140: * Deletes entry of the current map with the given key in the
1141: * persistent store.
1142: *
1143: * @throws IOException problems while deleting the entry
1144: * @throws IllegalStateException if no connection has been set
1145: */
1146: private void doDelete() throws IOException {
1147: try {
1148: if (deleteList.size() == 0) {
1149: return;
1150: }
1151: if (deletePStmt == null) {
1152: deletePStmt = new UniversalPrepStmt(dataSource,
1153: dbConnection, deleteStatement);
1154: }
1155: Iterator keys = deleteList.iterator();
1156: while (keys.hasNext()) {
1157: final String key = (String) keys.next();
1158: setMapId(deletePStmt, 1);
1159: deletePStmt.setString(2, key);
1160: if (dbProps.supportsBatchUpdates()) {
1161: deletePStmt.addBatch();
1162: } else {
1163: int hits = deletePStmt.executeUpdate();
1164: if (hits != 1) {
1165: throw new IOException(
1166: "Data store not consistent "
1167: + "(hits on delete = " + hits
1168: + ")!");
1169: }
1170: }
1171: }
1172: if (dbProps.supportsBatchUpdates()) {
1173: int[] result = deletePStmt.executeBatch();
1174: if (result.length != deleteList.size()) {
1175: throw new IOException(
1176: "Incorrect number of commands "
1177: + "while executing delete batch.");
1178: } else {
1179: for (int i = 0; i < result.length; i++) {
1180: if ((result[i] != 1) && (result[i] != -2)) {
1181: throw new IOException(
1182: "Data store not consistent "
1183: + "(hits on delete = "
1184: + result[i] + ")!");
1185: }
1186: }
1187: }
1188: }
1189: } catch (SQLException exc) {
1190: throw new PersistentMapSQLException(exc);
1191: }
1192: deleteList.clear();
1193: }
1194:
1195: /**
1196: * Inserts entry of the current map with the given key in the
1197: * persistent store.
1198: *
1199: * @throws IOException problems while inserting the entry
1200: * @throws IllegalStateException if no connection has been set
1201: */
1202: private void doInsert() throws IOException {
1203: try {
1204: if (insertList.size() == 0) {
1205: return;
1206: }
1207: if (insertPStmt == null) {
1208: insertPStmt = new UniversalPrepStmt(dataSource,
1209: dbConnection, insertStatement, new String[] {
1210: mapColumnName, itemColumnName });
1211: }
1212: Iterator keys = insertList.iterator();
1213: while (keys.hasNext()) {
1214: final String key = (String) keys.next();
1215: final MapValue value = getValue(key);
1216: setMapId(insertPStmt, 1);
1217: insertPStmt.setString(2, key);
1218: insertPStmt.setString(3, value.svalue);
1219: insertPStmt.setBinary(4, value.bvalue);
1220: if (dbProps.supportsBatchUpdates()) {
1221: insertPStmt.addBatch();
1222: } else {
1223: int hits = insertPStmt.executeUpdate();
1224: if (hits != 1) {
1225: throw new IOException(
1226: "Data store not consistent "
1227: + "(hits on insert = " + hits
1228: + ")!");
1229: }
1230: }
1231: }
1232: if (dbProps.supportsBatchUpdates()) {
1233: int[] result = insertPStmt.executeBatch();
1234: if (result.length != insertList.size()) {
1235: throw new IOException(
1236: "Incorrect number of commands "
1237: + "while executing insert batch.");
1238: } else {
1239: for (int i = 0; i < result.length; i++) {
1240: if ((result[i] != 1) && (result[i] != -2)) {
1241: throw new IOException(
1242: "Data store not consistent "
1243: + "(hits on insert = "
1244: + result[i] + ")!");
1245: }
1246: }
1247: }
1248: }
1249: } catch (SQLException exc) {
1250: throw new PersistentMapSQLException(exc);
1251: }
1252: insertList.clear();
1253: }
1254:
1255: /**
1256: * Verify if the string will be stored as string, not as object.
1257: * @param value the string
1258: * @return <code>true</code> if the string will be stored as "svalue"
1259: */
1260: protected boolean isSValue(String value) {
1261: return (value.length() <= sValueMax)
1262: && (!dbProps.isOracle() || ((String) value).length() > 0);
1263: }
1264:
1265: /**
1266: * Returns a map value for a given key.
1267: *
1268: * @param key key to retrieve value for.
1269: * @return value for given key.
1270: */
1271: private MapValue getValue(String key) {
1272: MapValue mapValue = new MapValue();
1273: Object value = get(key);
1274: if ((value instanceof String) && isSValue((String) value)) {
1275: mapValue.svalue = (String) value;
1276: } else {
1277: mapValue.bvalue = value;
1278: }
1279: return mapValue;
1280: }
1281:
1282: /**
1283: * Sets the map id in a prepared statement.
1284: * @param stmt the statement
1285: * @param offset the offset
1286: */
1287: private void setMapId(PreparedStatement stmt, int offset)
1288: throws SQLException {
1289: if (id instanceof String) {
1290: stmt.setString(offset, (String) id);
1291: } else {
1292: stmt.setLong(offset, ((Long) id).longValue());
1293: }
1294: }
1295:
1296: /**
1297: * Closes an existing prepared statement.
1298: *
1299: * @param pStmt the prepared statement to be closed
1300: * @throws SQLException if no connection has been set
1301: * @return always null.
1302: */
1303: private PreparedStatement closePreparedStatement(
1304: PreparedStatement pStmt) throws SQLException {
1305: if (pStmt != null) {
1306: pStmt.close();
1307: }
1308: return null;
1309: }
1310: }
|