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 }