001    package hirondelle.web4j.database;
002    
003    import java.util.*;
004    import java.util.logging.*;
005    import java.sql.Connection;
006    import hirondelle.web4j.util.Util;
007    import hirondelle.web4j.util.Args;
008    
009    /**
010     Perform the simplest kinds of transactions.
011     
012     <P>Executes a number of SQL statements, in the same order as passed to the constructor. 
013     Each SQL statement is executed once and only once, without any iteration.
014     Each SQL statement must perform an <tt>INSERT</tt>, <tt>DELETE</tt>, or <tt>UPDATE</tt> operation.
015     No <tt>SELECT</tt>s are allowed.
016     
017     <P><span class="highlight">It is likely that a significant fraction (perhaps as high as 50%) of all 
018     transactions can be implemented with this class. </span>
019     
020     <P>Example use case:
021     <PRE>
022     SqlId[] sqlIds = {ADD_THIS, DELETE_THAT};
023     Object[] params = {blah.getX(), blah.getY(), blah.getZ(), blah.getQ()}; 
024     Tx doStuff = new TxSimple(sqlIds, params);
025     int numRecordsAffected = doStuff.executeTx();
026     </PRE>
027     
028     <P>In this example, <tt>blah</tt> will usually represent a Model Object. The data in <tt>params</tt>
029     is divided up among the various <tt>sqlIds</tt>. (This division is performed internally by this class.) 
030     
031     <P>This class uses strong ordering conventions. <span class="highlight">The order of 
032     items in each array passed to the constructor is very important</span> (see {@link #TxSimple(SqlId[], Object[])}).
033    */
034    public final class TxSimple implements Tx {
035      
036      /**
037       Constructor.
038      
039       <P><tt>aAllParams</tt> includes the parameters for all operations, concatenated in a single array. Has at least 
040       one member. 
041       
042       <P>In general, successive pieces of <tt>aAllParams</tt> are used to populate each corresponding 
043       statement, in their order of execution. The order of items is here <em>doubly</em> important: the 
044       first N parameters are used against the first SQL statement, the next M parameters are 
045       used against the second SQL statement, and so on. (This breakdown is deduced internally.)
046        
047       <P>And as usual, within each subset of <tt>aAllParams</tt> 
048       corresponding to an SQL statement, the order of parameters matches the order 
049       of <tt>'?'</tt> placeholders appearing in the underlying SQL statement.  
050       
051       @param aSqlIds identifiers for the operations to be performed, <em>in the order of their intended execution</em>. Has 
052       at least one member. All of the operations are against the same database.
053       @param aAllParams the parameters for all operations, concatenated in a single array. Has at least 
054       one member.  (See above for important requirements on their order.) 
055      */
056      public TxSimple(SqlId[] aSqlIds, Object[] aAllParams){
057        Args.checkForPositive(aSqlIds.length);
058        Args.checkForPositive(aAllParams.length);
059        
060        fSqlIds = aSqlIds;
061        fParams = aAllParams;
062        fParamsForStatement = new LinkedHashMap<SqlId, Object[]>();
063        divideUpParams();
064      }
065      
066      public int executeTx() throws DAOException {
067        Tx transaction = new SimpleTransaction(getDbName());
068        return transaction.executeTx();
069      }
070    
071      // PRIVATE //
072      private SqlId[] fSqlIds;
073      private Object[] fParams;
074      private Map<SqlId, Object[]> fParamsForStatement;
075      private static final Object[] EMPTY = new Object[0];
076      private static final Logger fLogger = Util.getLogger(TxSimple.class);
077      
078      private final class SimpleTransaction extends TxTemplate {
079        SimpleTransaction(String aDbName){
080          super(aDbName);
081        }
082        public int executeMultipleSqls(Connection aConnection) throws DAOException {
083          int result = 0;
084          for (SqlId sqlId : fSqlIds){
085            Object[] params = fParamsForStatement.get(sqlId);
086            SqlEditor editor = null;
087            if( params.length == 0 ){
088              editor = SqlEditor.forTx(sqlId, aConnection);
089            }
090            else {
091              editor = SqlEditor.forTx(sqlId, aConnection, params);
092            }
093            result = result + editor.editDatabase();
094          }
095          return result;
096        }
097      }
098      
099      private String getDbName() {
100        return fSqlIds[0].getDatabaseName();
101      }
102      
103      private void divideUpParams(){
104        int startWith = 0;
105        int totalNumExpectedParams = 0;
106        for (SqlId sqlId : fSqlIds){
107          int numExpectedParams = SqlStatement.getNumParameters(sqlId);
108          totalNumExpectedParams = totalNumExpectedParams + numExpectedParams;
109          chunkParams(startWith, numExpectedParams, sqlId);
110          startWith = startWith + numExpectedParams;
111        }
112        if ( totalNumExpectedParams != fParams.length ){
113          throw new IllegalArgumentException(
114            "Size mismatch. Number of parameters passed as data: " + fParams.length + 
115            ".  Number of '?' items appearing in underlying SQL statements: " + totalNumExpectedParams
116          );
117        }
118        fLogger.finest("Chunked params " + Util.logOnePerLine(fParamsForStatement));
119      }
120      
121      private void chunkParams(int aStartWithIdx, int aNumExpectedParams, SqlId aSqlId){
122        int endWithIdx = aStartWithIdx + aNumExpectedParams - 1;
123        int maxIdx = fParams.length - 1;
124        fLogger.fine("Chunking params for " + aSqlId +  ". Start with index " + aStartWithIdx + ", end with index " + endWithIdx);
125        if( aStartWithIdx > maxIdx ){
126          throw new IllegalArgumentException("Error chunking parameter data. Starting-index (" + aStartWithIdx + ") greater than expected maximum (" + maxIdx + "). Mismatch between number of params passed as data, and number of params expected by underlying SQL statements.");
127        }
128        if ( endWithIdx > maxIdx ){
129          throw new IllegalArgumentException("Error chunking parameter data. Ending-index (" + endWithIdx + ") greater than expected maximum (" + maxIdx + "). Mismatch between number of params passed as data, and number of params expected by underlying SQL statements.");
130        }
131        
132        if( aNumExpectedParams == 0 ){
133          fParamsForStatement.put(aSqlId, EMPTY);
134        }
135        else {
136          List list = Arrays.asList(fParams);
137          List sublist = list.subList(aStartWithIdx, endWithIdx + 1);
138          fParamsForStatement.put(aSqlId, sublist.toArray(EMPTY));
139        }
140      }
141    }