001 package hirondelle.web4j.database; 002 003 import hirondelle.web4j.BuildImpl; 004 import hirondelle.web4j.readconfig.Config; 005 import hirondelle.web4j.util.Consts; 006 import hirondelle.web4j.util.Util; 007 008 import java.sql.Connection; 009 import java.sql.SQLException; 010 import java.util.logging.Logger; 011 012 /** 013 Template for executing a local, non-distributed transaction versus a single database, 014 using a single connection. 015 016 <P>This abstract base class implements the template method design pattern. 017 018 <P>The {@link TxSimple} class should be the first choice for implementing a transaction. 019 If it is not suitable (for example, if iteration is involved), then this class can always be used. 020 The benefits of using this class to implement transactions is that the caller avoids 021 repeated code involving connections, commit/rollback, handling exceptions and errors, and so on. 022 023 <P>See {@link TxIsolationLevel} for remarks on selection of correct isolation level. The {@link DbTx} class 024 is often useful for implementors. 025 026 <P>Do not use this class in the context of a <tt>UserTransaction</tt>. 027 028 <h3>Example Use Case</h3> 029 A DAO method which uses a <tt>TxTemplate</tt> called <tt>AddAllUnknowns</tt> to perform multiple <tt>INSERT</tt> operations : 030 <PRE> 031 {@code 032 public int addAll(Set<String> aUnknowns) throws DAOException { 033 Tx addTx = new AddAllUnknowns(aUnknowns); 034 return addTx.executeTx(); 035 } 036 } 037 </PRE> 038 039 The <tt>TxTemplate</tt> class itself, defined inside the same DAO, as an inner class : 040 <PRE> 041 {@code 042 private static final class AddAllUnknowns extends TxTemplate { 043 AddAllUnknowns(Set<String> aUnknowns){ 044 super(ConnectionSrc.TRANSLATION); 045 fUnknowns = aUnknowns; 046 } 047 &Override public int executeMultipleSqls(Connection aConnection) throws SQLException, DAOException { 048 int result = 0; 049 for(String unknown: fUnknowns){ 050 addUnknown(unknown, aConnection); 051 result = result + 1; 052 } 053 return result; 054 } 055 private Set<String> fUnknowns; 056 private void addUnknown(String aUnknown, Connection aConnection) throws DAOException { 057 DbTx.edit(aConnection, UnknownBaseTextEdit.ADD, aUnknown); 058 } 059 } 060 } 061 </PRE> 062 */ 063 public abstract class TxTemplate implements Tx { 064 065 /** 066 Constructor for a transaction versus the default database, at the 067 default isolation level. 068 069 <P>The default transaction isolation level is configured in <tt>web.xml</tt>. 070 */ 071 public TxTemplate(){ 072 fDatabaseName = DEFAULT_DB; 073 fTxIsolationLevel = fConfig.getSqlEditorDefaultTxIsolationLevel(DEFAULT_DB); 074 } 075 076 /** 077 Constructor for transaction versus the default database, at a custom 078 isolation level. 079 080 <P>The default transaction isolation level is configured in <tt>web.xml</tt>. 081 */ 082 public TxTemplate(TxIsolationLevel aTxIsolationLevel){ 083 fDatabaseName = DEFAULT_DB; 084 fTxIsolationLevel = aTxIsolationLevel; 085 } 086 087 /** 088 Constructor for a transaction versus a non-default database, at its 089 isolation level, as configured in <tt>web.xml</tt>. 090 091 @param aDatabaseName one of the return values of {@link ConnectionSource#getDatabaseNames()} 092 */ 093 public TxTemplate(String aDatabaseName){ 094 fDatabaseName = aDatabaseName; 095 fTxIsolationLevel = fConfig.getSqlEditorDefaultTxIsolationLevel(aDatabaseName); 096 } 097 098 /** 099 Constructor for a transaction versus a non-default database, at a custom 100 isolation level. 101 102 <P>The default transaction isolation level is configured in <tt>web.xml</tt>. 103 104 @param aDatabaseName one of the return values of {@link ConnectionSource#getDatabaseNames()} 105 */ 106 public TxTemplate(String aDatabaseName, TxIsolationLevel aTxIsolationLevel){ 107 fDatabaseName = aDatabaseName; 108 fTxIsolationLevel = aTxIsolationLevel; 109 } 110 111 /** 112 <b>Template</b> method calls the abstract method {@link #executeMultipleSqls}. 113 <P>Returns the same value as <tt>executeMultipleSqls</tt>. 114 115 <P>A <tt>rollback</tt> is performed if <tt>executeMultipleSqls</tt> throws a {@link SQLException} or 116 {@link DAOException}, or if {@link #executeMultipleSqls(Connection)} returns {@link #BUSINESS_RULE_FAILURE}. 117 */ 118 public final int executeTx() throws DAOException { 119 int result = 0; 120 fLogger.fine( 121 "Editing within a local transaction, with isolation level : " + fTxIsolationLevel 122 ); 123 ConnectionSource connSource = BuildImpl.forConnectionSource(); 124 if(Util.textHasContent(fDatabaseName)){ 125 fConnection = connSource.getConnection(fDatabaseName); 126 } 127 else { 128 fConnection = connSource.getConnection(); 129 } 130 131 try { 132 TxIsolationLevel.set(fTxIsolationLevel, fConnection); 133 startTx(); 134 result = executeMultipleSqls(fConnection); 135 endTx(result); 136 } 137 catch(SQLException rootCause){ 138 //if SqlEditor is used, this branch will not be exercised, since it throws only 139 //DAOExceptions 140 fLogger.fine("Transaction throws SQLException."); 141 rollbackTx(); 142 String message = 143 "Cannot execute edit. Error code : " + rootCause.getErrorCode() + 144 Consts.SPACE + rootCause 145 ; 146 Integer errorCode = new Integer(rootCause.getErrorCode()); 147 if (fConfig.getErrorCodesForDuplicateKey(fDatabaseName).contains(errorCode)){ 148 throw new DuplicateException(message, rootCause); 149 } 150 else if (fConfig.getErrorCodesForForeignKey(fDatabaseName).contains(errorCode)){ 151 throw new ForeignKeyException(message, rootCause); 152 } 153 throw new DAOException(message, rootCause); 154 } 155 catch (DAOException ex){ 156 //if SqlEditor is used, it will always throw a DAOException, not SQLException 157 fLogger.fine("Transaction throws DAOException."); 158 rollbackTx(); 159 throw ex; 160 } 161 finally { 162 DbUtil.logWarnings(fConnection); 163 DbUtil.close(fConnection); 164 } 165 fLogger.fine("Total number of edited records: " + result); 166 return result; 167 } 168 169 /** 170 Execute multiple SQL operations in a single local transaction. 171 172 <P>This method returns the number of records edited. If a business rule determines that a 173 rollback should be performed, then it is recommended that the special value 174 {@link #BUSINESS_RULE_FAILURE} be returned by the implementation. This will signal to 175 {@link #executeTx()} that a rollback must be performed. (Another option for 176 signalling that a rollback is desired is to throw a checked exception.) 177 178 <P><em>Design Note</em>: allowing <tt>SQLException</tt> in the <tt>throws</tt> 179 clause simplifies the implementor significantly, since no <tt>try-catch</tt> blocks are 180 needed. Thus, the caller has simple, "straight-line" code. 181 182 @param aConnection must be used by all SQL statements participating in this transaction 183 @return number of records edited by this operation. Implementations may return 184 {@link #BUSINESS_RULE_FAILURE} if there is a business rule failure. 185 */ 186 public abstract int executeMultipleSqls(Connection aConnection) throws SQLException, DAOException; 187 188 /** 189 Value {@value}. Special value returned by {@link #executeMultipleSqls(Connection)} to indicate that 190 a business rule has been violated. Such a return value indicates to this class that a rollback must be 191 performed. 192 */ 193 public static final int BUSINESS_RULE_FAILURE = -1; 194 195 // PRIVATE 196 197 /** 198 The connection through which all SQL statements attached to this 199 transaction are executed. This connection may be for the default 200 database, or any other defined database. See {@link #fDatabaseName}. 201 */ 202 private Connection fConnection; 203 204 /** 205 Identifier for the database. The connection taken from the default 206 database only if this item has no content. An empty string implies the default 207 database. 208 */ 209 private String fDatabaseName; 210 private static final String DEFAULT_DB = ""; 211 212 /** The transaction isolation level, set only during the constructor. */ 213 private final TxIsolationLevel fTxIsolationLevel; 214 215 private static final boolean fOFF = false; 216 private static final boolean fON = true; 217 private Config fConfig = new Config(); 218 private static final Logger fLogger = Util.getLogger(TxTemplate.class); 219 220 private void startTx() throws SQLException { 221 fConnection.setAutoCommit(fOFF); 222 } 223 224 private void endTx(int aNumEdits) throws SQLException, DAOException { 225 if ( BUSINESS_RULE_FAILURE == aNumEdits ) { 226 fLogger.severe("Business rule failure occured. Cannot commit transaction."); 227 rollbackTx(); 228 } 229 else { 230 fLogger.fine("Commiting transaction."); 231 fConnection.commit(); 232 fConnection.setAutoCommit(fON); 233 } 234 } 235 236 private void rollbackTx() throws DAOException { 237 fLogger.severe("ROLLING BACK TRANSACTION."); 238 try { 239 fConnection.rollback(); 240 } 241 catch(SQLException ex){ 242 throw new DAOException("Cannot rollback transaction", ex); 243 } 244 } 245 }