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