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        &amp;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    }