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