Package org.jboss.messaging.core.plugin

Source Code of org.jboss.messaging.core.plugin.JDBCPersistenceManager

/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.messaging.core.plugin;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.sql.DataSource;
import javax.transaction.TransactionManager;
import javax.transaction.xa.Xid;

import org.jboss.logging.Logger;
import org.jboss.messaging.core.message.Message;
import org.jboss.messaging.core.message.MessageFactory;
import org.jboss.messaging.core.message.MessageReference;
import org.jboss.messaging.core.message.MessageSupport;
import org.jboss.messaging.core.plugin.contract.PersistenceManager;
import org.jboss.messaging.core.tx.MessagingXid;
import org.jboss.messaging.core.tx.PreparedTxInfo;
import org.jboss.messaging.core.tx.Transaction;
import org.jboss.messaging.core.tx.TxCallback;
import org.jboss.messaging.util.JDBCUtil;
import org.jboss.messaging.util.StreamUtils;
import org.jboss.messaging.util.Util;

/**
* JDBC implementation of PersistenceManager.
* @author <a href="mailto:ovidiu@jboss.org">Ovidiu Feodorov</a>
* @author <a href="mailto:tim.fox@jboss.com">Tim Fox</a>
* @author <a href="mailto:adrian@jboss.org">Adrian Brock</a>
* @author <a href="mailto:juha@jboss.org">Juha Lindfors</a>
*
* @version <tt>1.1</tt>
*
* JDBCPersistenceManager.java,v 1.1 2006/02/22 17:33:41 timfox Exp
*/
public class JDBCPersistenceManager extends JDBCSupport implements PersistenceManager
{
   // Constants -----------------------------------------------------
  
   private static final Logger log = Logger.getLogger(JDBCPersistenceManager.class);

   // Static --------------------------------------------------------
  
   // Attributes ----------------------------------------------------
  
   private boolean trace = log.isTraceEnabled();
     
   private boolean usingBatchUpdates = false;
  
   private boolean usingBinaryStream = true;
  
   private boolean usingTrailingByte = false;
  
   private int maxParams;
  
   private short orderCount;
  
  
   // Constructors --------------------------------------------------
   
   public JDBCPersistenceManager(DataSource ds, TransactionManager tm, Properties sqlProperties,
                                 boolean createTablesOnStartup, boolean usingBatchUpdates,
                                 boolean usingBinaryStream, boolean usingTrailingByte, int maxParams)
   {
      super(ds, tm, sqlProperties, createTablesOnStartup);
     
      this.usingBatchUpdates = usingBatchUpdates;
     
      this.usingBinaryStream = usingBinaryStream;
     
      this.usingTrailingByte = usingTrailingByte;
     
      this.maxParams = maxParams;     
   }
  
  
   // MessagingComponent overrides ---------------------------------
  
   public void start() throws Exception
   {
      super.start();

      Connection conn = null;

      TransactionWrapper wrap = new TransactionWrapper();

      try
      {
         conn = ds.getConnection();     
         //JBossMessaging requires transaction isolation of READ_COMMITTED
         //Any looser isolation level and we cannot maintain consistency for paging (HSQL)
         if (conn.getTransactionIsolation() != Connection.TRANSACTION_READ_COMMITTED)
         {
            int level = conn.getTransactionIsolation();

            String warn =
               "\n\n" +
               "JBoss Messaging Warning: DataSource connection transaction isolation should be READ_COMMITTED, but it is currently " + Util.transactionIsolationToString(level) + ".\n" +
               "                         Using an isolation level less strict than READ_COMMITTED may lead to data consistency problems.\n" +
               "                         Using an isolation level more strict than READ_COMMITTED may lead to deadlock.\n";
            log.warn(warn);
         }
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
         wrap.end();
      }
            
      log.debug(this + " started");
   }
  
   public void stop() throws Exception
   {
      super.stop();
   }
  
   // PersistenceManager implementation -------------------------
  
   // Related to XA Recovery
   // ======================
  
   public List getMessageChannelPairRefsForTx(long transactionId) throws Exception
   {
      String sql = this.getSQLStatement("SELECT_MESSAGE_ID_FOR_REF");
      return getMessageChannelPair(sql, transactionId);
   }
  
   public List getMessageChannelPairAcksForTx(long transactionId) throws Exception
   {
      String sql = this.getSQLStatement("SELECT_MESSAGE_ID_FOR_ACK");
      return getMessageChannelPair(sql, transactionId);
   }
  
   public List retrievePreparedTransactions() throws Exception
   {
      /* Note the API change for 1.0.2 XA Recovery -- List now contains instances of PreparedTxInfo<TxId, Xid>
       * instead of direct Xids [JPL] */
     
      Connection conn = null;
      Statement st = null;
      ResultSet rs = null;
      PreparedTxInfo txInfo = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      try
      {
         List transactions = new ArrayList();
        
         conn = ds.getConnection();
        
         st = conn.createStatement();
        
         String sql = this.getSQLStatement("SELECT_PREPARED_TRANSACTIONS");
        
         rs = st.executeQuery(sql);
        
         while (rs.next())
         {
            //get the existing tx id --MK START
            long txId = rs.getLong(1);
           
            byte[] branchQual = getVarBinaryColumn(rs, 2);
           
            int formatId = rs.getInt(3);
           
            byte[] globalTxId = getVarBinaryColumn(rs, 4);
           
            Xid xid = new MessagingXid(branchQual, formatId, globalTxId);
           
            // create a tx info object with the result set detailsdetails
            txInfo = new PreparedTxInfo(txId, xid);
           
            transactions.add(txInfo);
         }
        
         return transactions;
        
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (rs != null)
         {
            try
            {
               rs.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (st != null)
         {
            try
            {
               st.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }
   }
  
              
   // Related to counters
   // ===================
  
   public long reserveIDBlock(String counterName, int size) throws Exception
   {
      if (trace) { log.trace("Getting ID block for counter " + counterName + ", size " + size); }
     
      if (size <= 0)
      {
         throw new IllegalArgumentException("block size must be > 0");
      }
     
      Connection conn = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      try
      {
         conn = ds.getConnection();

         //For the clustered case - this MUST use SELECT .. FOR UPDATE or a similar
         //construct the locks the row
         String selectCounterSQL = getSQLStatement("SELECT_COUNTER");
        
         ps = conn.prepareStatement(selectCounterSQL);
        
         ps.setString(1, counterName);
        
         rs = ps.executeQuery();
        
         if (trace) { log.trace(JDBCUtil.statementToString(selectCounterSQL, counterName)); }        
        
         if (!rs.next())
         {
            rs.close();
            rs = null;
           
            ps.close();
           
            //There is a very small possibility that two threads will attempt to insert the same counter
            //at the same time, if so, then the second one will fail eventually after a few retries by throwing
            //a primary key violation.
           
            String insertCounterSQL = getSQLStatement("INSERT_COUNTER");
           
            ps = conn.prepareStatement(insertCounterSQL);
           
            ps.setString(1, counterName);
            ps.setLong(2, size);
           
            int rows = updateWithRetry(ps);
            if (trace) { log.trace(JDBCUtil.statementToString(insertCounterSQL, counterName, new Integer(size)) + " inserted " + rows + " rows"); }
           
            ps.close();           
            ps = null;
            return 0;
         }
        
         long nextId = rs.getLong(1);
        
         rs.close();
         rs = null;
        
         ps.close();

         String updateCounterSQL = getSQLStatement("UPDATE_COUNTER");

         ps = conn.prepareStatement(updateCounterSQL);
        
         ps.setLong(1, nextId + size);
         ps.setString(2, counterName);
        
         int rows = updateWithRetry(ps);
         if (trace) { log.trace(JDBCUtil.statementToString(updateCounterSQL, new Long(nextId + size), counterName) + " updated " + rows + " rows"); }
        
         return nextId;
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }    
   }
        
   /*
    * Retrieve a List of messages corresponding to the specified List of message ids.
    * The implementation here for HSQLDB does this by using a PreparedStatment with an IN clause
    * with a maximum of 100 elements.
    * If there are more than maxParams message to retrieve this is repeated a number of times.
    * For "Enterprise" databases (Oracle, DB2, Sybase etc) a more sophisticated technique should be used
    * e.g. Oracle ARRAY types in Oracle which can be submitted as a param to an Oracle prepared statement
    * Although this would all be DB specific.
    */
   public List getMessages(List messageIds) throws Exception
   {
      if (trace) { log.trace("Getting batch of messages for " + messageIds); }
     
      Connection conn = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      try
      {
         conn = ds.getConnection();
        
         Iterator iter = messageIds.iterator();
        
         int size = messageIds.size();
        
         int count = 0;
        
         List msgs = new ArrayList();
        
         while (iter.hasNext())
         {
            if (ps == null)
            {
               //PreparedStatements are cached in the JCA layer so we will never actually have more than
               //100 distinct ones           
               int numParams;
               if (count < (size / maxParams) * maxParams)
               {
                  numParams = maxParams;
               }
               else
               {
                  numParams = size % maxParams;
               }
               StringBuffer buff = new StringBuffer(getSQLStatement("LOAD_MESSAGES"));
               buff.append(" WHERE ").append(getSQLStatement("MESSAGE_ID_COLUMN")).append(" IN (");
               for (int i = 0; i < numParams; i++)
               {
                  buff.append("?");
                  if (i < numParams - 1)
                  {
                     buff.append(",");
                  }
               }
               buff.append(")");
               ps = conn.prepareStatement(buff.toString());
              
               if (trace)
               {
                  log.trace(buff.toString());
               }
            }
           
            long msgId = ((Long)iter.next()).longValue();
           
            ps.setLong((count % maxParams) + 1, msgId);
           
            count++;
           
            if (!iter.hasNext() || count % maxParams == 0)
            {
               rs = ps.executeQuery();
              
               while (rs.next())
               {      
                  long messageId = rs.getLong(1);
                 
                  boolean reliable = rs.getString(2).equals("Y");
                 
                  long expiration = rs.getLong(3);
                 
                  long timestamp = rs.getLong(4);
                 
                  byte priority = rs.getByte(5);       
                 
                  byte[] bytes = getBytes(rs, 6);
                 
                  HashMap headers = bytesToMap(bytes);
                 
                  byte[] payload = getBytes(rs, 7);
                 
                  byte type = rs.getByte(8);
                 
                  Message m = MessageFactory.createMessage(messageId, reliable, expiration, timestamp, priority,
                                                           headers, payload, type);
                  msgs.add(m);
               }
              
               rs.close();
               rs = null;
              
               ps.close();
               ps = null;
            }
         }
        
         if (trace) { log.trace("Loaded " + msgs.size() + " messages in total"); }

         return msgs;
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (rs != null)
         {
            try
            {
               rs.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }
   } 
  
      
   // Related to paging functionality
   // ===============================                
  
   public void pageReferences(long channelID, List references, boolean paged) throws Exception
  
      Connection conn = null;
      PreparedStatement psInsertReference = null
      PreparedStatement psInsertMessage = null;
      PreparedStatement psUpdateMessage = null;
      PreparedStatement psMessageExists = null;
      ResultSet rsMessageExists = null;
      TransactionWrapper wrap = new TransactionWrapper();
           
      //First we order the references in message order
      orderReferences(references);
                        
      try
      {
         //Now we get a lock on all the messages. Since we have ordered the refs we should avoid deadlock
         getLocks(references);
        
         conn = ds.getConnection();
        
         Iterator iter = references.iterator();
        
         boolean messageInsertsInBatch = false;
         boolean messageUpdatesInBatch = false;
        
         if (usingBatchUpdates)
         {
            psInsertReference = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE_REF"));
            psInsertMessage = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE"));
            psUpdateMessage = conn.prepareStatement(getSQLStatement("INC_CHANNEL_COUNT"));
         }
        
         while (iter.hasNext())
         {
            //We may need to persist the message itself
            MessageReference ref = (MessageReference) iter.next();
                                           
            //For non reliable refs we insert the ref (and maybe the message) itself
                          
            if (!usingBatchUpdates)
            {
               psInsertReference = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE_REF"));
            }
           
            //Now store the reference
            addReference(channelID, ref, psInsertReference, paged);
                       
            if (usingBatchUpdates)
            {
               psInsertReference.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psInsertReference);
              
               if (trace)
               {
                  log.trace("Inserted " + rows + " rows");
               }
              
               psInsertReference.close();
               psInsertReference = null;
            }
           
            if (!usingBatchUpdates)
            {
               psInsertMessage = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE"));
               psUpdateMessage = conn.prepareStatement(getSQLStatement("INC_CHANNEL_COUNT"));
            }
                                                                                    
            //Maybe we need to persist the message itself
            Message m = ref.getMessage();
           
            //In a paging situation, we cannot use the persisted flag on the message to determine whether
            //to insert the message or not.
            //This is because a channel (possibly on another node) may be paging too and referencing
            //the same message, and might have removed the message independently, the other
            //channel will not know about this.
            //Therefore we have to check if the message is already in the database and insert it if it isn't
           
            //TODO This is a bit of a hassle -
            //A cleaner and better solution here is to completely separate out the paging functionality from the
            //standard persistence functionality since it complicates things considerably.
            //We should define a paging store which is separate from the persistence store, and
            //typically not using the database for the paging store - probably use a file based store
            //e.g HOWL or some other logger
           
            //Note when running this with two or more competing channels in the same process, then
            //we do not need a FOR UPDATE on the select since we lock the messages in memory
            //However for competing nodes, we do, therefore we require a database that supports
            //this, this is another reason why we cannot use HSQL in a clustered environment
            //since it does not have a for update equivalent
           
            boolean added;
           
            psMessageExists = conn.prepareStatement(getSQLStatement("MESSAGE_EXISTS"));
           
            psMessageExists.setLong(1, m.getMessageID());
           
            rsMessageExists = psMessageExists.executeQuery();
            
            if (rsMessageExists.next())
            {
               //Message exists
              
               // Update the message with the new channel count
               incrementChannelCount(m, psUpdateMessage);
                 
               added = false;             
            }
            else
            {
               //Hasn't been persisted before so need to persist the message
               storeMessage(m, psInsertMessage);
              
               added = true;
            }   
           
            if (usingBatchUpdates)
            {
               if (added)
               {
                  psInsertMessage.addBatch();
                  messageInsertsInBatch = true;
               }
               else
               {
                  psUpdateMessage.addBatch();
                  messageUpdatesInBatch = true;
               }
            }
            else
            {
               if (added)
               {
                  int rows = updateWithRetry(psInsertMessage);
                                     
                  if (trace)
                  {
                     log.trace("Inserted " + rows + " rows");
                  }
               }
               else
               {              
                  int rows = updateWithRetry(psUpdateMessage);
                 
                  if (trace)
                  {
                     log.trace("Updated " + rows + " rows");
                  }
               }
               psInsertMessage.close();
               psInsertMessage = null;
               psUpdateMessage.close();
               psUpdateMessage = null;
            }     
         }        
        
         if (usingBatchUpdates)
         {
            int[] rowsReference = updateWithRetryBatch(psInsertReference);
           
            if (trace) { logBatchUpdate(getSQLStatement("INSERT_MESSAGE_REF"), rowsReference, "inserted"); }
           
            if (messageInsertsInBatch)
            {
               int[] rowsMessage = updateWithRetryBatch(psInsertMessage);
              
               if (trace) { logBatchUpdate(getSQLStatement("INSERT_MESSAGE"), rowsMessage, "inserted"); }
            }
            if (messageUpdatesInBatch)
            {
               int[] rowsMessage = updateWithRetryBatch(psUpdateMessage);
              
               if (trace) { logBatchUpdate(getSQLStatement("INC_CHANNEL_COUNT"), rowsMessage, "updated"); }
            }
           
            psInsertReference.close();
            psInsertReference = null;
            psInsertMessage.close();
            psInsertMessage = null;
            psUpdateMessage.close();
            psUpdateMessage = null;
         }       
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (psInsertReference != null)
         {
            try
            {
               psInsertReference.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psInsertMessage != null)
         {
            try
            {
               psInsertMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psUpdateMessage != null)
         {
            try
            {
               psUpdateMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable t)
            {
            }
         }
         try
         {
            wrap.end();                      
         }
         finally
         {           
            //And then release locks
            this.releaseLocks(references);
         }        
      }     
   }
        
   public void removeDepagedReferences(long channelID, List references) throws Exception
   {
      if (trace) { log.trace(this + " Removing " + references.size() + " refs from channel " + channelID); }
         
      Connection conn = null;
      PreparedStatement psDeleteReference = null
      PreparedStatement psDeleteMessage = null;
      PreparedStatement psUpdateMessage = null;
      TransactionWrapper wrap = new TransactionWrapper();
       
      //We order the references
      orderReferences(references);
            
      try
      {
         //We get locks on all the messages - since they are ordered we avoid deadlock
         getLocks(references);
        
         conn = ds.getConnection();
        
         Iterator iter = references.iterator();
        
         if (usingBatchUpdates)
         {
            psDeleteReference = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE_REF"));
            psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
            psUpdateMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
         }
        
         while (iter.hasNext())
         {
            MessageReference ref = (MessageReference) iter.next();
                                                            
            if (!usingBatchUpdates)
            {
               psDeleteReference = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE_REF"));
            }
           
            removeReference(channelID, ref, psDeleteReference);
           
            if (usingBatchUpdates)
            {
               psDeleteReference.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psDeleteReference);
              
               if (trace) { log.trace("Deleted " + rows + " rows"); }
              
               psDeleteReference.close();
               psDeleteReference = null;
            }
           
            if (!usingBatchUpdates)
            {
               psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
               psUpdateMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
            }
              
            Message m = ref.getMessage();
                                   
            //Maybe we need to delete the message itself
             
            //Update the message with the new channel count
            decrementChannelCount(m, psUpdateMessage);
           

            //Run the remove message update
            removeMessage(m, psDeleteMessage);
                       
            if (usingBatchUpdates)
            {
               psUpdateMessage.addBatch();
              
               psDeleteMessage.addBatch();
            }
            else
           
               int rows = updateWithRetry(psUpdateMessage);
                                                
               if (trace) { log.trace("Updated " + rows + " rows"); }
              
               rows = updateWithRetry(psDeleteMessage);
       
               if (trace) { log.trace("Deleted " + rows + " rows"); }
           
               psDeleteMessage.close();
               psDeleteMessage = null;
               psUpdateMessage.close();
               psUpdateMessage = null;
           
           
         }        
        
         if (usingBatchUpdates)
         {
            int[] rowsReference = updateWithRetryBatch(psDeleteReference);
           
            if (trace) { logBatchUpdate(getSQLStatement("DELETE_MESSAGE_REF"), rowsReference, "deleted"); }
           
            rowsReference = updateWithRetryBatch(psUpdateMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DEC_CHANNEL_COUNT"), rowsReference, "updated"); }
           
            rowsReference = updateWithRetryBatch(psDeleteMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DELETE_MESSAGE"), rowsReference, "deleted"); }
                                   
            psDeleteReference.close();
            psDeleteReference = null;
            psDeleteMessage.close();
            psDeleteMessage = null;
            psUpdateMessage.close();
            psUpdateMessage = null;
         }             
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (psDeleteReference != null)
         {
            try
            {
               psDeleteReference.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psDeleteMessage != null)
         {
            try
            {
               psDeleteMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psUpdateMessage != null)
         {
            try
            {
               psUpdateMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable t)
            {
            }
         }
         try
         {
            wrap.end();
         }
         finally
         {    
            //And then release locks
            this.releaseLocks(references);
         }        
      }     
   }
  
   public void updateReferencesNotPagedInRange(long channelID, long orderStart, long orderEnd, long num) throws Exception
   {
      if (trace) { log.trace("Updating paaged references for channel " + channelID + " between " + orderStart + " and " + orderEnd); }
     
      Connection conn = null;
      PreparedStatement ps = null;
      TransactionWrapper wrap = new TransactionWrapper();
   
      try
      {
         conn = ds.getConnection();
        
         ps = conn.prepareStatement(getSQLStatement("UPDATE_REFS_NOT_PAGED"));
                
         ps.setLong(1, orderStart);
        
         ps.setLong(2, orderEnd);
        
         ps.setLong(3, channelID);
        
         int rows = updateWithRetry(ps);
          
         if (trace) { log.trace(JDBCUtil.statementToString(getSQLStatement("UPDATE_REFS_NOT_PAGED"), new Long(channelID),
                                new Long(orderStart), new Long(orderEnd)) + " updated " + rows + " rows"); }

         //Sanity check
         if (rows != num)
         {
            throw new IllegalStateException("Did not update correct number of rows");
         }           
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }
   }
  
   public InitialLoadInfo mergeAndLoad(long fromChannelID, long toChannelID, int numberToLoad, long firstPagingOrder, long nextPagingOrder) throws Exception
   {
      if (trace) { log.trace("Merging channel from " + fromChannelID + " to " + toChannelID + " numberToLoad:" + numberToLoad + " firstPagingOrder:" + firstPagingOrder + " nextPagingOrder:" + nextPagingOrder); }
     
      Connection conn = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();
      PreparedStatement ps2 = null;
   
      try
      {
         conn = ds.getConnection();
        
         /*
          * If channel is paging and has full size f
          *
          * then we don't need to load any refs but we need to:
          *
          * make sure the page ord is correct across the old paged and new refs
          *
          * we know the max page ord (from the channel) for the old refs so we just need to:
          *
          * 1) Iterate through the failed channel and update page_ord = max + 1, max + 2 etc
          *
          * 2) update channel id
          *
          *
          * If channel is not paging and the total refs before and after <=f
          *
          * 1) Load all refs from failed channel
          *
          * 2) Update channel id
          *
          * return those refs
          *
          *
          * If channel is not paging but total new refs > f
          *
          * 1) Iterate through failed channel refs and take the first x to make the channel full
          *
          * 2) Update the others with page_ord starting at zero
          *
          * 3) Update channel id
          *
          * In general:
          *
          * We have number to load n, max page size p
          *
          * 1) Iterate through failed channel refs in page_ord order
          *
          * 2) Put the first n in a List.
          *
          * 3) Initialise page_ord_count to be p or 0 depending on whether it was specified
          *
          * 4) Update the page_ord of the remaining refs accordiningly
          *
          * 5) Update the channel id
          *
          */
        
         //First load the refs from the failed channel

         List refs = new ArrayList();
        
         ps = conn.prepareStatement(getSQLStatement("LOAD_REFS"));
        
         ps.setLong(1, fromChannelID);
                
         rs = ps.executeQuery();
        
         int count = 0;
        
         boolean arePaged = false;
        
         long pageOrd = nextPagingOrder;
        
         while (rs.next())
         {
            long msgId = rs.getLong(1);           
            int deliveryCount = rs.getInt(2);
            long sched = rs.getLong(3);
           
            if (count < numberToLoad)
            {          
               ReferenceInfo ri = new ReferenceInfo(msgId, deliveryCount, sched);
              
               refs.add(ri);
            }
           
            // Set page ord
           
            if (ps2 == null)
            {
               ps2 = conn.prepareStatement(getSQLStatement("UPDATE_PAGE_ORDER"));
            }
               
            if (count < numberToLoad)
            {
               ps2.setNull(1, Types.BIGINT);
              
               if (trace) { log.trace("Set page ord to null"); }
            }
            else
            {                                
               ps2.setLong(1, pageOrd);
              
               if (trace) { log.trace("Set page ord to " + pageOrd); }
              
               arePaged = true;
              
               pageOrd++;                     
            }
           
            ps2.setLong(2, msgId);
           
            ps2.setLong(3, fromChannelID);
           
            int rows = updateWithRetry(ps2);
           
            if (trace) { log.trace("Update page ord updated " + rows + " rows"); }

            count++;           
         }
        
         ps.close();
        
         // Now swap the channel id
        
         ps = conn.prepareStatement(getSQLStatement("UPDATE_CHANNEL_ID"));
        
         ps.setLong(1, toChannelID);
        
         ps.setLong(2, fromChannelID);
        
         int rows = updateWithRetry(ps);
        
         if (trace) { log.trace("Update channel id updated " + rows + " rows"); }
                          
         if (arePaged)
         {           
            return new InitialLoadInfo(new Long(firstPagingOrder), new Long(pageOrd - 1), refs);
         }
         else
         {
            return new InitialLoadInfo(null, null, refs);
         }        
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (ps2 != null)
         {
            try
            {
               ps2.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }
   }
  
   public void updatePageOrder(long channelID, List references) throws Exception
   {
      Connection conn = null;
      PreparedStatement psUpdateReference = null
      TransactionWrapper wrap = new TransactionWrapper();
     
      if (trace) { log.trace("Updating page order for channel:" + channelID); }
       
      try
      {
         conn = ds.getConnection();
        
         Iterator iter = references.iterator();
        
         if (usingBatchUpdates)
         {
            psUpdateReference = conn.prepareStatement(getSQLStatement("UPDATE_PAGE_ORDER"));
         }
        
         while (iter.hasNext())
         {
            MessageReference ref = (MessageReference) iter.next();
                
            if (!usingBatchUpdates)
            {
               psUpdateReference = conn.prepareStatement(getSQLStatement("UPDATE_PAGE_ORDER"));
            }
           
            psUpdateReference.setLong(1, ref.getPagingOrder());

            psUpdateReference.setLong(2, ref.getMessage().getMessageID());
           
            psUpdateReference.setLong(3, channelID);
           
            if (usingBatchUpdates)
            {
               psUpdateReference.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psUpdateReference);
              
               if (trace) { log.trace("Updated " + rows + " rows"); }
              
               psUpdateReference.close();
               psUpdateReference = null;
            }
         }
                    
         if (usingBatchUpdates)
         {
            int[] rowsReference = updateWithRetryBatch(psUpdateReference);
           
            if (trace) { logBatchUpdate(getSQLStatement("UPDATE_PAGE_ORDER"), rowsReference, "updated"); }
                       
            psUpdateReference.close();
            psUpdateReference = null;
         }
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (psUpdateReference != null)
         {
            try
            {
               psUpdateReference.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable t)
            {
            }
         }
         wrap.end();
      }   
   }
     
   public List getPagedReferenceInfos(long channelID, long orderStart, int number) throws Exception
   {
      if (trace) { log.trace("loading message reference info for channel " + channelID + " from " + orderStart + " number " + number);      }
                
      List refs = new ArrayList();
     
      Connection conn = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      try
      {
         conn = ds.getConnection();
        
         ps = conn.prepareStatement(getSQLStatement("LOAD_PAGED_REFS"));
        
         ps.setLong(1, channelID);
        
         ps.setLong(2, orderStart);
        
         ps.setLong(3, orderStart + number - 1);
        
         rs = ps.executeQuery();
        
         long ord = orderStart;
        
         while (rs.next())
         {
            long msgId = rs.getLong(1);    
            int deliveryCount = rs.getInt(2);
            int pageOrd = rs.getInt(3);
            long sched = rs.getLong(4);
           
            //Sanity check
            if (pageOrd != ord)
            {
               throw new IllegalStateException("Unexpected pageOrd: " + pageOrd + " expected: " + ord);
            }
           
            ReferenceInfo ri = new ReferenceInfo(msgId, deliveryCount, sched);
           
            refs.add(ri);
            ord++;
         }
        
         //Sanity check
         if (ord != orderStart + number)
         {
            throw new IllegalStateException("Didn't load expected number of references, loaded: " + (ord - orderStart) +
                                            " expected: " + number);
         }
        
         return refs;
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (rs != null)
         {
            try
            {
               rs.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }     
   }  
  
   /*
    * Load the initial, non paged refs
    */
   public InitialLoadInfo loadFromStart(long channelID, int number) throws Exception
   {
      if (trace) { log.trace("loading initial reference infos for channel " + channelID)}
                   
      Connection conn = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();
                               
      try
      {
         conn = ds.getConnection();        
        
         //First we get the values for min() and max() page order
         ps = conn.prepareStatement(getSQLStatement("SELECT_MIN_MAX_PAGE_ORD"));
        
         ps.setLong(1, channelID);
        
         rs = ps.executeQuery();
                 
         rs.next();
        
         Long minOrdering = new Long(rs.getLong(1));
        
         if (rs.wasNull())
         {
            minOrdering = null;
         }
        
         Long maxOrdering = new Long(rs.getLong(2));
        
         if (rs.wasNull())
         {
            maxOrdering = null;
         }
         conn.close();
        
         conn = ds.getConnection();        
        
         ps = conn.prepareStatement(getSQLStatement("LOAD_UNPAGED_REFS"));
        
         ps.setLong(1, channelID);
                
         rs = ps.executeQuery();
        
         List refs = new ArrayList();
        
         int count = 0;
         while (rs.next())
         {
            long msgId = rs.getLong(1);           
            int deliveryCount = rs.getInt(2);
            long sched = rs.getLong(3);
           
            ReferenceInfo ri = new ReferenceInfo(msgId, deliveryCount, sched);
           
            if (count < number)
            {
               refs.add(ri);
            }           
           
            count++;
         }
                 
         //No refs paged
           
         if (count > number)
         {
            throw new IllegalStateException("Cannot load channel " + channelID + " since the fullSize parameter is too small to load " +
                     " all the required references, fullSize needs to be at least " + count + " it is currently " + number);
         }
                        
         return new InitialLoadInfo(minOrdering, maxOrdering, refs);
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (rs != null)
         {
            try
            {
               rs.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }     
   }  
  
  
   // End of paging functionality
   // ===========================
  
   public void addReference(long channelID, MessageReference ref, Transaction tx) throws Exception
   {     
      if (tx != null)
      {
         //In a tx so we just add the ref in the tx in memory for now

         TransactionCallback callback = getCallback(tx);

         callback.addReferenceToAdd(channelID, ref);
      }
      else
      {        
         //No tx so add the ref directly in the db
        
         TransactionWrapper wrap = new TransactionWrapper();
        
         PreparedStatement psReference = null;
         PreparedStatement psMessage = null;
        
         Connection conn = ds.getConnection();
        
         Message m = ref.getMessage();    
          
         try
         {           
            // Get lock on message
            LockMap.instance.obtainLock(m);
                                   
            psReference = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE_REF"));
           
            // Add the reference
            addReference(channelID, ref, psReference, false);
           
            int rows = updateWithRetry(psReference);     
           
            if (trace) { log.trace("Inserted " + rows + " rows"); }
             
            if (!m.isPersisted())
            {
               // First time so persist the message
               psMessage = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE"));
              
               storeMessage(m, psMessage);
              
               m.setPersisted(true);
            }
            else
            {
               //Update the message's channel count
               psMessage = conn.prepareStatement(getSQLStatement("INC_CHANNEL_COUNT"));
              
               incrementChannelCount(m, psMessage);
            }
                          
            rows = updateWithRetry(psMessage);
           
            if (trace) { log.trace("Inserted/updated " + rows + " rows"); }    
           
            log.trace("message Inserted/updated " + rows + " rows");
         }
         catch (Exception e)
         {
            wrap.exceptionOccurred();
            throw e;
         }
         finally
         {
            if (psReference != null)
            {
               try
               {
                  psReference.close();
               }
               catch (Throwable t)
               {
               }
            }
            if (psMessage != null)
            {
               try
               {
                  psMessage.close();
               }
               catch (Throwable t)
               {
               }
            }
            if (conn != null)
            {
               try
               {
                  conn.close();
               }
               catch (Throwable t)
               {
               }
            }
            try
            {
               wrap.end();
            }
            finally
            {  
               //Release Lock
               LockMap.instance.releaseLock(m);
            }
         }     
      }
   }
  
   public void updateDeliveryCount(long channelID, MessageReference ref) throws Exception
   {
      TransactionWrapper wrap = new TransactionWrapper();
     
      PreparedStatement psReference = null;
     
      Connection conn = ds.getConnection();
      
      try
      {                                   
         psReference = conn.prepareStatement(getSQLStatement("UPDATE_DELIVERY_COUNT"));
        
         psReference.setInt(1, ref.getDeliveryCount());
        
         psReference.setLong(2, channelID);
        
         psReference.setLong(3, ref.getMessage().getMessageID());
        
         int rows = updateWithRetry(psReference);

         if (trace) { log.trace("Updated " + rows + " rows"); }
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (psReference != null)
         {
            try
            {
               psReference.close();
            }
            catch (Throwable t)
            {
            }
         }        
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable t)
            {
            }
         }
         wrap.end();                       
     
   }
  
   public void removeReference(long channelID, MessageReference ref, Transaction tx) throws Exception
   {     
      if (tx != null)
      {
         //In a tx so we just add the ref in the tx in memory for now

         TransactionCallback callback = getCallback(tx);

         callback.addReferenceToRemove(channelID, ref);
      }
      else
      {        
         //No tx so we remove the reference directly from the db
        
         TransactionWrapper wrap = new TransactionWrapper();
        
         PreparedStatement psReference = null;
         PreparedStatement psUpdate = null;
         PreparedStatement psMessage = null;
        
         Connection conn = ds.getConnection();
        
         Message m = ref.getMessage();        
        
         try
         {
            //get lock on message
            LockMap.instance.obtainLock(m);
                             
            psReference = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE_REF"));
           
            //Remove the message reference
            removeReference(channelID, ref, psReference);
           
            int rows = updateWithRetry(psReference);
           
            if (rows != 1)
            {
               log.warn("Failed to remove row for: " + ref);
               return;
            }
           
            if (trace) { log.trace("Deleted " + rows + " rows"); }
           
            //Update the messages channel count
           
            psUpdate = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
           
            decrementChannelCount(m, psUpdate);
           
            rows = updateWithRetry(psUpdate);
           
            if (trace) { log.trace("Updated " + rows + " rows"); }
           
            //Delete the message (if necessary)
           
            psMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
           
            removeMessage(m, psMessage);
                      
            rows = updateWithRetry(psMessage);
           
            if (trace) { log.trace("Delete " + rows + " rows"); }                          
         }
         catch (Exception e)
         {
            wrap.exceptionOccurred();
            throw e;
         }
         finally
         {
            if (psReference != null)
            {
               try
               {
                  psReference.close();
               }
               catch (Throwable t)
               {
               }
            }
            if (psUpdate != null)
            {
               try
               {
                  psUpdate.close();
               }
               catch (Throwable t)
               {
               }
            }
            if (psMessage != null)
            {
               try
               {
                  psMessage.close();
               }
               catch (Throwable t)
               {
               }
            }
            if (conn != null)
            {
               try
               {
                  conn.close();
               }
               catch (Throwable t)
               {
               }
            }
            try
            {
               wrap.end();              
            }
            finally
            {     
               //release the lock
               LockMap.instance.releaseLock(m);
            }
         }     
      }
   }
  
          
   public boolean referenceExists(long channelID, long messageID) throws Exception
   {
      Connection conn = null;
      PreparedStatement st = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();

      try
      {
         conn = ds.getConnection();

         st = conn.prepareStatement(getSQLStatement("SELECT_EXISTS_REF"));
         st.setLong(1, channelID);
         st.setLong(2, messageID);

         rs = st.executeQuery();

         if (rs.next())
         {
            return true;
         }
         else
         {
            return false;
         }
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (rs != null)
         {
            try
            {
               rs.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (st != null)
         {
            try
            {
               st.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }
   }

   public boolean referenceExists(long messageID) throws Exception
   {
      Connection conn = null;
      PreparedStatement st = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();

      try
      {
         conn = ds.getConnection();

         st = conn.prepareStatement(getSQLStatement("SELECT_EXISTS_REF_MESSAGE_ID"));
         st.setLong(1, messageID);

         rs = st.executeQuery();

         if (rs.next())
         {
            return true;
         }
         else
         {
            return false;
         }
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (rs != null)
         {
            try
            {
               rs.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (st != null)
         {
            try
            {
               st.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }
   }

   // Public --------------------------------------------------------
  
   public String toString()
   {
      return "JDBCPersistenceManager[" + Integer.toHexString(hashCode()) + "]";
   }  
  
   // Package protected ---------------------------------------------
  
   // Protected -----------------------------------------------------
     
   protected TransactionCallback getCallback(Transaction tx)
   {
      TransactionCallback callback = (TransactionCallback) tx.getCallback(this);

      if (callback == null)
      {
         callback = new TransactionCallback(tx);

         tx.addCallback(callback, this);
      }

      return callback;
   }
  
   /**
    * We order the list of references in ascending message order thus preventing deadlock when 2 or
    * more channels are updating the same messages in different transactions.
    */
   protected void orderReferences(List references)
   {     
      Collections.sort(references, MessageOrderComparator.instance);
   }
  
   protected void handleBeforeCommit1PC(List refsToAdd, List refsToRemove, Transaction tx)
      throws Exception
   {
      //TODO - A slight optimisation - it's possible we have refs referring to the same message
      //       so we will end up acquiring the lock more than once which is unnecessary. If find
      //       unique set of messages can avoid this.

      List allRefs = new ArrayList(refsToAdd.size() + refsToRemove.size());

      for(Iterator i = refsToAdd.iterator(); i.hasNext(); )
      {
         ChannelRefPair pair = (ChannelRefPair)i.next();
         allRefs.add(pair.ref);
      }

      for(Iterator i = refsToRemove.iterator(); i.hasNext(); )
      {
         ChannelRefPair pair = (ChannelRefPair)i.next();
         allRefs.add(pair.ref);
      }
           
      orderReferences(allRefs);
     
      // For one phase we simply add rows corresponding to the refs and remove rows corresponding to
      // the deliveries in one jdbc tx. We also need to store or remove messages as necessary,
      // depending on whether they've already been stored or still referenced by other channels.
        
      Connection conn = null;
      PreparedStatement psReference = null;
      PreparedStatement psInsertMessage = null;
      PreparedStatement psIncMessage = null;
      PreparedStatement psDecMessage = null;
      PreparedStatement psDeleteMessage = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      try
      {
         conn = ds.getConnection();
        
         // Obtain locks on all messages
         getLocks(allRefs);
        
         // First the adds

         boolean messageInsertsInBatch = false;
         boolean messageUpdatesInBatch = false;
         boolean batch = usingBatchUpdates && refsToAdd.size() > 0;

         if (batch)
         {
            psReference = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE_REF"));
            psInsertMessage = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE"));
            psIncMessage = conn.prepareStatement(getSQLStatement("INC_CHANNEL_COUNT"));
         }

         for(Iterator i = refsToAdd.iterator(); i.hasNext(); )
         {
            ChannelRefPair pair = (ChannelRefPair)i.next();
            MessageReference ref = pair.ref;
                                               
            if (!batch)
            {
               psReference = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE_REF"));
            }
           
            // Now store the reference
            addReference(pair.channelID, ref, psReference, false);
             
            if (batch)
            {
               psReference.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psReference);
              
               if (trace) { log.trace("Inserted " + rows + " rows"); }                             
              
               psReference.close();
               psReference = null;
            }
           
            Message m = ref.getMessage();       
           
            if (!batch)
            {
               psInsertMessage = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE"));
               psIncMessage = conn.prepareStatement(getSQLStatement("INC_CHANNEL_COUNT"));
            }
                        
            boolean added;
            if (!m.isPersisted())
            {              
               // First time so add message
               storeMessage(m, psInsertMessage);
               added = true;
               m.setPersisted(true);
            }
            else
            {              
               // Update message channel count
               incrementChannelCount(m, psIncMessage);
               added = false;
            }
           
            if (batch)
            {
               if (added)
               {
                  psInsertMessage.addBatch();
                  if (trace) { log.trace("Message does not already exist so inserting it"); }
                  messageInsertsInBatch = true;
               }
               else
               {
                  if (trace) { log.trace("Message already exists so updating count"); }
                  psIncMessage.addBatch();
                  messageUpdatesInBatch = true;
               }
            }
            else
            {
               if (added)
               {
                  if (trace) { log.trace("Message does not already exist so inserting it"); }
                  int rows = updateWithRetry(psInsertMessage);
                  if (trace) { log.trace("Inserted " + rows + " rows"); }
               }
               else
               {
                  if (trace) { log.trace("Message already exists so updating count"); }
                  int rows = updateWithRetry(psIncMessage);
                  if (trace) { log.trace("Updated " + rows + " rows"); }
               }
               psInsertMessage.close();
               psInsertMessage = null;
               psIncMessage.close();
               psIncMessage = null;
            }
         }        
        
         if (batch)
         {
            // Process the add batch

            int[] rowsReference = updateWithRetryBatch(psReference);
           
            if (trace) { logBatchUpdate(getSQLStatement("INSERT_MESSAGE_REF"), rowsReference, "inserted"); }
           
            if (messageInsertsInBatch)
            {
               int[] rowsMessage = updateWithRetryBatch(psInsertMessage);
               if (trace) { logBatchUpdate(getSQLStatement("INSERT_MESSAGE"), rowsMessage, "inserted"); }
            }

            if (messageUpdatesInBatch)
            {
               int[] rowsMessage = updateWithRetryBatch(psIncMessage);
               if (trace) { logBatchUpdate(getSQLStatement("INC_CHANNEL_COUNT"), rowsMessage, "updated"); }
            }
           
            psReference.close();
            psReference = null;
            psInsertMessage.close();
            psInsertMessage = null;
            psIncMessage.close();
            psIncMessage = null;
         }
        
         // Now the removes

         psReference = null;
         psDeleteMessage = null;
         batch = usingBatchUpdates && refsToRemove.size() > 0;

         if (batch)
         {
            psReference = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE_REF"));
            psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
            psDecMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
         }

        
         for(Iterator i = refsToRemove.iterator(); i.hasNext(); )
         {
            ChannelRefPair pair = (ChannelRefPair)i.next();
           
            if (!batch)
            {
               psReference = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE_REF"));
            }
           
            removeReference(pair.channelID, pair.ref, psReference);
           
            if (batch)
            {
               psReference.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psReference);
               if (trace) { log.trace("Deleted " + rows + " rows"); }
               psReference.close();
               psReference = null;
            }
           
            if (!batch)
            {
               psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
               psDecMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
            }
           
            Message m = pair.ref.getMessage();
                               
            // Update the channel count
           
            decrementChannelCount(m, psDecMessage);
           
            // Delete the message (if necessary)
           
            removeMessage(m, psDeleteMessage);
                      
            if (batch)
            {
               psDecMessage.addBatch();
               psDeleteMessage.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psDecMessage);
               if (trace) { log.trace("Updated " + rows + " rows"); }
              
               rows = updateWithRetry(psDeleteMessage);
               if (trace) { log.trace("Deleted " + rows + " rows"); }

               psDeleteMessage.close();
               psDeleteMessage = null;
               psDecMessage.close();
               psDecMessage = null;
            }           
         }
        
         if (batch)
         {
            // Process the remove batch

            int[] rows = updateWithRetryBatch(psReference);
           
            if (trace) { logBatchUpdate(getSQLStatement("DELETE_MESSAGE_REF"), rows, "deleted"); }
           
            rows = updateWithRetryBatch(psDecMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DEC_CHANNEL_COUNT"), rows, "updated"); }

            rows = updateWithRetryBatch(psDeleteMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DELETE_MESSAGE"), rows, "deleted"); }

            psReference.close();
            psReference = null;
            psDeleteMessage.close();
            psDeleteMessage = null;
            psDecMessage.close();
            psDecMessage = null;
         }        
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();                 
         throw e;
      }
      finally
      {
         if (psReference != null)
         {
            try
            {
               psReference.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psInsertMessage != null)
         {
            try
            {
               psInsertMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psIncMessage != null)
         {
            try
            {
               psIncMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psDecMessage != null)
         {
            try
            {
               psDecMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psDeleteMessage != null)
         {
            try
            {
               psDeleteMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               if (trace) { log.trace("Closing connection"); }
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         try
         {
            wrap.end();                       
         }
         finally
        
            //Release the locks
            this.releaseLocks(allRefs);
         }
      }
   }
  
   protected void handleBeforeCommit2PC(List refsToRemove, Transaction tx)
      throws Exception
   {         
      Connection conn = null;
      PreparedStatement psUpdateMessage = null;
      PreparedStatement psDeleteMessage = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      List refs = new ArrayList(refsToRemove.size());
      Iterator iter = refsToRemove.iterator();
      while (iter.hasNext())
      {
         ChannelRefPair pair = (ChannelRefPair)iter.next();
         refs.add(pair.ref);
      }
           
      orderReferences(refs);     
     
      try
      {
         //get locks on all the refs
         this.getLocks(refs);
        
         conn = ds.getConnection();
                 
         //2PC commit
        
         //First we commit any refs in state "+" to "C" and delete any
         //refs in state "-", then we
         //remove any messages due to refs we just removed
         //if they're not referenced elsewhere
        
         commitPreparedTransaction(tx, conn);
        
         boolean batch = usingBatchUpdates && refsToRemove.size() > 0;

         if (batch)
         {
            psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
            psUpdateMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
         }
                 
         iter = refsToRemove.iterator();
         while (iter.hasNext())
         {
            ChannelRefPair pair = (ChannelRefPair) iter.next();
           
            MessageReference ref = pair.ref;
           
            if (!batch)
            {
               psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
               psUpdateMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
            }
           
            Message m = ref.getMessage();
                                  
            //We may need to remove the message itself
           
            //Update the channel count
           
            decrementChannelCount(m, psUpdateMessage);
           
            //Remove the message (if necessary)
           
            removeMessage(m, psDeleteMessage);         
                          
            if (batch)
            {
               psUpdateMessage.addBatch();
               
               psDeleteMessage.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psUpdateMessage);
              
               if (trace) { log.trace("Updated " + rows + " rows"); }
              
               rows = updateWithRetry(psDeleteMessage);
              
               if (trace) { log.trace("Deleted " + rows + " rows"); }
              
               psDeleteMessage.close();
               psDeleteMessage = null;
               psUpdateMessage.close();
               psUpdateMessage = null;
            }
         }        
        
         if (batch)
         {
            int[] rows = updateWithRetryBatch(psUpdateMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DEC_CHANNEL_COUNT"), rows, "updated"); }
           
            psUpdateMessage.close();
            psUpdateMessage = null;
           
            rows = updateWithRetryBatch(psDeleteMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DELETE_MESSAGE"), rows, "deleted"); }
           
            psDeleteMessage.close();
            psDeleteMessage = null;                          
         }        
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (psDeleteMessage != null)
         {
            try
            {
               psDeleteMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psUpdateMessage != null)
         {
            try
            {
               psUpdateMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         try
         {
            wrap.end();
         }
         finally
         {
            //release the locks
            this.releaseLocks(refs);
         }
      }
   }
  
   protected void handleBeforePrepare(List refsToAdd, List refsToRemove, Transaction tx) throws Exception
   {
      //We only need to lock on the adds
      List refs = new ArrayList(refsToAdd.size());
     
      Iterator iter = refsToAdd.iterator();
      while (iter.hasNext())
      {
         ChannelRefPair pair = (ChannelRefPair)iter.next();
        
         refs.add(pair.ref);
      }
     
      orderReferences(refs);
     
      //We insert a tx record and
      //a row for each ref with +
      //and update the row for each delivery with "-"
     
      PreparedStatement psReference = null;
      PreparedStatement psInsertMessage = null;
      PreparedStatement psUpdateMessage = null;
      Connection conn = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      try
      {
         //get the locks
         getLocks(refs);
        
         conn = ds.getConnection();
        
         //Insert the tx record
         if (!refsToAdd.isEmpty() || !refsToRemove.isEmpty())
         {
            addTXRecord(conn, tx);
         }
        
         iter = refsToAdd.iterator();
        
         boolean batch = usingBatchUpdates && refsToAdd.size() > 1;
         boolean messageInsertsInBatch = false;
         boolean messageUpdatesInBatch = false;
         if (batch)
         {
            psReference = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE_REF"));
            psInsertMessage = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE"));
            psUpdateMessage = conn.prepareStatement(getSQLStatement("INC_CHANNEL_COUNT"));
         }
        
         while (iter.hasNext())
         {
            ChannelRefPair pair = (ChannelRefPair) iter.next();
           
            if (!batch)
            {
               psReference = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE_REF"));
            }
           
            prepareToAddReference(pair.channelID, pair.ref, tx, psReference);
           
            if (batch)
            {
               psReference.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psReference);
              
               if (trace) { log.trace("Inserted " + rows + " rows"); }
              
               psReference.close();
               psReference = null;
            }
           
            if (!batch)
            {
               psInsertMessage = conn.prepareStatement(getSQLStatement("INSERT_MESSAGE"));
               psUpdateMessage = conn.prepareStatement(getSQLStatement("INC_CHANNEL_COUNT"));
            }
           
            Message m = pair.ref.getMessage();
                  
            boolean added;        
           
            if (!m.isPersisted())
            {
               //First time so persist the message
               storeMessage(m, psInsertMessage);
              
               m.setPersisted(true);
              
               added = true;
            }
            else
            {
               //Update message channel count
               incrementChannelCount(m, psUpdateMessage);
              
               added = false;
            }
           
            if (batch)
            {
               if (added)
               {
                  psInsertMessage.addBatch();
                  messageInsertsInBatch = true;
               }
               else
               {
                  psUpdateMessage.addBatch();
                  messageUpdatesInBatch = true;
               }
            }
            else
            {
               if (added)
               {
                  int rows = updateWithRetry(psInsertMessage);
                 
                  if (trace) { log.trace("Inserted " + rows + " rows"); }
               }
               else
               {
                  int rows = updateWithRetry(psUpdateMessage);
                 
                  if (trace) { log.trace("Updated " + rows + " rows"); }
               }
               psInsertMessage.close();
               psInsertMessage = null;
               psUpdateMessage.close();
               psUpdateMessage = null;
            }
         }        
        
         if (batch)
         {
            int[] rowsReference = updateWithRetryBatch(psReference);
           
            if (trace) { logBatchUpdate(getSQLStatement("INSERT_MESSAGE_REF"), rowsReference, "inserted"); }
           
            if (messageInsertsInBatch)
            {
               int[] rowsMessage = updateWithRetryBatch(psInsertMessage);
              
               if (trace) { logBatchUpdate(getSQLStatement("INSERT_MESSAGE"), rowsMessage, "inserted"); }
            }
            if (messageUpdatesInBatch)
            {
               int[] rowsMessage = updateWithRetryBatch(psUpdateMessage);
              
               if (trace) { logBatchUpdate(getSQLStatement("INC_CHANNEL_COUNT"), rowsMessage, "updated"); }
            }
           
            psReference.close();
            psReference = null;
            psInsertMessage.close();
            psInsertMessage = null;
            psUpdateMessage.close();
            psUpdateMessage = null;
         }
        
         //Now the removes
        
         iter = refsToRemove.iterator();
        
         batch = usingBatchUpdates && refsToRemove.size() > 1;
         if (batch)
         {
            psReference = conn.prepareStatement(getSQLStatement("UPDATE_MESSAGE_REF"));
         }

         while (iter.hasNext())
         {
            ChannelRefPair pair = (ChannelRefPair) iter.next();
           
            if (!batch)
            {
               psReference = conn.prepareStatement(getSQLStatement("UPDATE_MESSAGE_REF"));
            }
           
            prepareToRemoveReference(pair.channelID, pair.ref, tx, psReference);
           
            if (batch)
            {
               psReference.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psReference);
              
               if (trace) { log.trace("updated " + rows + " rows"); }
              
               psReference.close();
               psReference = null;
            }
         }
        
         if (batch)
         {
            int[] rows = updateWithRetryBatch(psReference);
           
            if (trace) { logBatchUpdate(getSQLStatement("UPDATE_MESSAGE_REF"), rows, "updated"); }
           
            psReference.close();
            psReference = null;
         }
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (psReference != null)
         {
            try
            {
               psReference.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psInsertMessage != null)
         {
            try
            {
               psInsertMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psUpdateMessage != null)
         {
            try
            {
               psUpdateMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         try
         {
            wrap.end();           
         }
         finally
         {
            //release the locks
           
            this.releaseLocks(refs);
         }
      }
   }
  
   protected void handleBeforeRollback(List refsToAdd, Transaction tx) throws Exception
   {
      //remove refs marked with +
      //and update rows marked with - to C
           
      PreparedStatement psDeleteMessage = null;
      PreparedStatement psUpdateMessage = null;
      Connection conn = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      List refs = new ArrayList(refsToAdd.size());
     
      Iterator iter = refsToAdd.iterator();
     
      while (iter.hasNext())
      {
         ChannelRefPair pair = (ChannelRefPair)iter.next();
         refs.add(pair.ref);
      }
     
      orderReferences(refs);
     
      try
      {
         this.getLocks(refs);
        
         conn = ds.getConnection();
        
         rollbackPreparedTransaction(tx, conn);
        
         iter = refsToAdd.iterator();
        
         boolean batch = usingBatchUpdates && refsToAdd.size() > 1;

         if (batch)
         {
            psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
            psUpdateMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
         }
                                
         while (iter.hasNext())
         {
            ChannelRefPair pair = (ChannelRefPair) iter.next();
           
            if (!batch)
            {
               psDeleteMessage = conn.prepareStatement(getSQLStatement("DELETE_MESSAGE"));
               psUpdateMessage = conn.prepareStatement(getSQLStatement("DEC_CHANNEL_COUNT"));
            }
           
            Message m = pair.ref.getMessage();
                                        
            //We may need to remove the message for messages added during the prepare stage
                       
            //update the channel count
           
            decrementChannelCount(m, psUpdateMessage);
           
            //remove the message (if necessary)
           
            removeMessage(m, psDeleteMessage);
                                       
            if (batch)
            {
               psUpdateMessage.addBatch();
              
               psDeleteMessage.addBatch();
            }
            else
            {
               int rows = updateWithRetry(psUpdateMessage);
              
               if (trace) { log.trace("updated " + rows + " rows"); }
              
               rows = updateWithRetry(psDeleteMessage);
              
               if (trace) { log.trace("deleted " + rows + " rows"); }
              
               psDeleteMessage.close();
               psDeleteMessage = null;
               psUpdateMessage.close();
               psUpdateMessage = null;
            }           
         }
        
         if (batch)
         {
            int[] rows = updateWithRetryBatch(psUpdateMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DEC_CHANNEL_COUNT"), rows, "updated"); }
           
            rows = updateWithRetryBatch(psDeleteMessage);
           
            if (trace) { logBatchUpdate(getSQLStatement("DELETE_MESSAGE"), rows, "deleted"); }
           
            psDeleteMessage.close();
            psDeleteMessage = null;
           
            psUpdateMessage.close();
            psUpdateMessage = null;           
         }
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (psDeleteMessage != null)
         {
            try
            {
               psDeleteMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (psUpdateMessage != null)
         {
            try
            {
               psUpdateMessage.close();
            }
            catch (Throwable t)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         try
         {
            wrap.end();
         }
         finally
         {
            //release locks
            this.releaseLocks(refs);
         }
      }     
   }
  
  
   protected void addTXRecord(Connection conn, Transaction tx) throws Exception
   {
      if (trace)
      {
         log.trace("Inserting tx record for " + tx);
      }
     
      PreparedStatement ps = null;
      String statement = "UNDEFINED";
      int rows = -1;
      int formatID = -1;
      try
      {
         statement = getSQLStatement("INSERT_TRANSACTION");
        
         ps = conn.prepareStatement(statement);
        
         ps.setLong(1, tx.getId());
        
         Xid xid = tx.getXid();
        
         formatID = xid.getFormatId();
        
         setVarBinaryColumn(2, ps, xid.getBranchQualifier());
        
         ps.setInt(3, formatID);
        
         setVarBinaryColumn(4, ps, xid.getGlobalTransactionId());
        
         rows = updateWithRetry(ps);
        
      }
      finally
      {
         if (trace)
         {
            String s = JDBCUtil.statementToString(statement, new Long(tx.getId()), "<byte-array>",
                  new Integer(formatID), "<byte-array>");
            log.trace(s + (rows == -1 ? " failed!" : " inserted " + rows + " row(s)"));
         }
         try
         {
            if (ps != null)
            {
               ps.close();
            }
         }
         catch (Throwable e)
         {
            //Ignore
         }
      }
   }
  
   protected void removeTXRecord(Connection conn, Transaction tx) throws Exception
   {
      PreparedStatement ps = null;
      try
      {
         ps = conn.prepareStatement(getSQLStatement("DELETE_TRANSACTION"));
        
         ps.setLong(1, tx.getId());
        
         int rows = updateWithRetry(ps);
        
         if (trace)
         {
            log.trace(JDBCUtil.statementToString(getSQLStatement("DELETE_TRANSACTION"), new Long(tx.getId())) + " removed " + rows + " row(s)");
         }
      }
      finally
      {
         try
         {
            if (ps != null)
            {
               ps.close();
            }
         }
         catch (Throwable e)
         {
            //Ignore
         }
      }
   } 
  
   protected void addReference(long channelID, MessageReference ref,
                               PreparedStatement ps, boolean paged) throws Exception
   {
      if (trace) { log.trace("adding " + ref + " to channel " + channelID); }
     
      ps.setLong(1, channelID);
      ps.setLong(2, ref.getMessage().getMessageID());
      ps.setNull(3, Types.BIGINT);
      ps.setString(4, "C");
      ps.setLong(5, getOrdering());
      if (paged)
      {
         ps.setLong(6, ref.getPagingOrder());
      }
      else
      {
         ps.setNull(6, Types.BIGINT);
      }
      ps.setInt(7, ref.getDeliveryCount());
      ps.setLong(8, ref.getScheduledDeliveryTime());
   }
  
   protected void removeReference(long channelID, MessageReference ref, PreparedStatement ps)
      throws Exception
   {
      if (trace) { log.trace("removing " + ref + " from channel " + channelID); }
     
      ps.setLong(1, ref.getMessage().getMessageID());
      ps.setLong(2, channelID);     
   }
  
   protected void prepareToAddReference(long channelID, MessageReference ref, Transaction tx, PreparedStatement ps)
     throws Exception
   {
      if (trace) { log.trace("adding " + ref + " to channel " + channelID + (tx == null ? " non-transactionally" : " on transaction: " + tx)); }
     
      ps.setLong(1, channelID);
      ps.setLong(2, ref.getMessage().getMessageID());
      ps.setLong(3, tx.getId());
      ps.setString(4, "+");
      ps.setLong(5, getOrdering());
      ps.setNull(6, Types.BIGINT);     
      ps.setInt(7, ref.getDeliveryCount());
      ps.setLong(8, ref.getScheduledDeliveryTime());
   }
  
   protected void prepareToRemoveReference(long channelID, MessageReference ref, Transaction tx, PreparedStatement ps)
      throws Exception
   {
      if (trace)
      {
         log.trace("removing " + ref + " from channel " + channelID
               + (tx == null ? " non-transactionally" : " on transaction: " + tx));
      }
     
      ps.setLong(1, tx.getId());
      ps.setLong(2, ref.getMessage().getMessageID());
      ps.setLong(3, channelID);          
   }
  
   protected void commitPreparedTransaction(Transaction tx, Connection conn) throws Exception
   {
      PreparedStatement ps = null;
     
      if (trace) { log.trace(this + " commitPreparedTransaction, tx= " + tx); }
       
      try
      {
         ps = conn.prepareStatement(getSQLStatement("COMMIT_MESSAGE_REF1"));
        
         ps.setLong(1, tx.getId());       
        
         int rows = updateWithRetry(ps);
        
         if (trace)
         {
            log.trace(JDBCUtil.statementToString(getSQLStatement("COMMIT_MESSAGE_REF1"), new Long(tx.getId())) + " removed " + rows + " row(s)");
         }
        
         ps.close();
         ps = conn.prepareStatement(getSQLStatement("COMMIT_MESSAGE_REF2"));
         ps.setLong(1, tx.getId());        
        
         rows = updateWithRetry(ps);
        
         if (trace)
         {
            log.trace(JDBCUtil.statementToString(getSQLStatement("COMMIT_MESSAGE_REF2"), new Long(tx.getId())) + " updated " + rows
                  + " row(s)");
         }
        
         removeTXRecord(conn, tx);
      }
      finally
      {
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
      }
   }
  
   protected void rollbackPreparedTransaction(Transaction tx, Connection conn) throws Exception
   {
      PreparedStatement ps = null;
     
      try
      {
         ps = conn.prepareStatement(getSQLStatement("ROLLBACK_MESSAGE_REF1"));
        
         ps.setLong(1, tx.getId());        
        
         int rows = updateWithRetry(ps);
        
         if (trace)
         {
            log.trace(JDBCUtil.statementToString(getSQLStatement("ROLLBACK_MESSAGE_REF1"), new Long(tx.getId())) + " removed " + rows + " row(s)");
         }
        
         ps.close();
        
         ps = conn.prepareStatement(getSQLStatement("ROLLBACK_MESSAGE_REF2"));
         ps.setLong(1, tx.getId());
        
         rows = updateWithRetry(ps);
        
         if (trace)
         {
            log.trace(JDBCUtil.statementToString(getSQLStatement("ROLLBACK_MESSAGE_REF2"), new Long(tx.getId())) + " updated " + rows
                  + " row(s)");
         }
        
         removeTXRecord(conn, tx);
      }
      finally
      {
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
      }
   }
  
   protected byte[] mapToBytes(Map map) throws Exception
   {
      if (map == null || map.isEmpty())
      {
         return null;
      }
     
      final int BUFFER_SIZE = 1024;
      
      ByteArrayOutputStream bos = new ByteArrayOutputStream(BUFFER_SIZE);
     
      DataOutputStream oos = new DataOutputStream(bos);
     
      StreamUtils.writeMap(oos, map, true);
     
      oos.close();
     
      return bos.toByteArray();
   }
  
   protected HashMap bytesToMap(byte[] bytes) throws Exception
   {
      if (bytes == null)
      {
         return new HashMap();
      }
      
      ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
     
      DataInputStream dais = new DataInputStream(bis);
     
      HashMap map = StreamUtils.readMap(dais, true);
     
      dais.close();
           
      return map;
   }
  

   //TODO - combine these
   protected void incrementChannelCount(Message m, PreparedStatement ps) throws Exception
   {
      ps.setLong(1, m.getMessageID());
   }
        
   protected void decrementChannelCount(Message m, PreparedStatement ps) throws Exception
   {
      ps.setLong(1, m.getMessageID());
   }
  
   /**
    * Stores the message in the MESSAGE table.
    */
   protected void storeMessage(Message m, PreparedStatement ps) throws Exception
   {     
      // physically insert the row in the database
      // first set the fields from org.jboss.messaging.core.Routable
      ps.setLong(1, m.getMessageID());
      ps.setString(2, m.isReliable() ? "Y" : "N");
      ps.setLong(3, m.getExpiration());
      ps.setLong(4, m.getTimestamp());
      ps.setByte(5, m.getPriority());
     
      //headers
      byte[] bytes = mapToBytes(((MessageSupport) m).getHeaders());
      if (bytes != null)
      {
         setBytes(ps, 6, bytes);
      }
      else
      {
         ps.setNull(6, Types.LONGVARBINARY);
      }
     
      byte[] payload = m.getPayloadAsByteArray();
      if (payload != null)
      {
         setBytes(ps, 7, payload);
      }
      else
      {
         ps.setNull(7, Types.LONGVARBINARY);
      }
     
      //The number of channels that hold a reference to the message - initially always 1
      ps.setInt(8, 1);    
     
      ps.setByte(9, m.getType());
   }
  
   /**
    * Removes the message from the MESSAGE table.
    */
   protected void removeMessage(Message message, PreparedStatement ps) throws Exception
   {
      // physically delete the row in the database
      ps.setLong(1, message.getMessageID());     
   }
  
   protected void setVarBinaryColumn(int column, PreparedStatement ps, byte[] bytes) throws Exception
   {
      if (usingTrailingByte)
      {
         // Sybase has the stupid characteristic of truncating all trailing in zeros
         // in varbinary columns
         // So we add an extra byte on the end when we store the varbinary data
         // otherwise we might lose data
         // http://jira.jboss.org/jira/browse/JBMESSAGING-825
        
         byte[] res = new byte[bytes.length + 1];
        
         System.arraycopy(bytes, 0, res, 0, bytes.length);
        
         res[bytes.length] = 127;

         bytes = res;
      }
     
      ps.setBytes(column, bytes);           
     
      if (trace) { log.trace("Setting varbinary column of length: " + bytes.length); }
   }
  
   protected byte[] getVarBinaryColumn(ResultSet rs, int columnIndex) throws Exception
   {
      byte[] bytes = rs.getBytes(columnIndex);
     
      if (usingTrailingByte)
      {
         // Get rid of the trailing byte
        
         // http://jira.jboss.org/jira/browse/JBMESSAGING-825
        
         byte[] newBytes = new byte[bytes.length - 1];
        
         System.arraycopy(bytes, 0, newBytes, 0, bytes.length - 1);
        
         bytes = newBytes;
      }
     
      return bytes;
   }
    
   // Used for storing message headers and bodies
   protected void setBytes(PreparedStatement ps, int columnIndex, byte[] bytes) throws Exception
   {
      if (usingBinaryStream)
      {
         //Set the bytes using a binary stream - likely to be better for large byte[]
        
         InputStream is = null;
        
         try
         {
            is = new ByteArrayInputStream(bytes);
           
            ps.setBinaryStream(columnIndex, is, bytes.length);
         }
         finally
         {
            if (is != null)
            {
               is.close();
            }
         }
      }
      else
      {
         //Set the bytes using setBytes() - likely to be better for smaller byte[]
        
         setVarBinaryColumn(columnIndex, ps, bytes);
      }
   }
  
   protected byte[] getBytes(ResultSet rs, int columnIndex) throws Exception
   {
      if (usingBinaryStream)
      {
         //Get the bytes using a binary stream - likely to be better for large byte[]
        
         InputStream is = null;
         ByteArrayOutputStream os = null;
        
         final int BUFFER_SIZE = 4096;
        
         try
         {
            InputStream i = rs.getBinaryStream(columnIndex);
           
            if (i == null)
            {
               return null;
            }
           
            is = new BufferedInputStream(rs.getBinaryStream(columnIndex), BUFFER_SIZE);
           
            os = new ByteArrayOutputStream(BUFFER_SIZE);
           
            int b;
            while ((b = is.read()) != -1)
            {
               os.write(b);
            }
           
            return os.toByteArray();
         }
         finally
         {
            if (is != null)
            {
               is.close();
            }
            if (os != null)
            {
               os.close();
            }
         }
      }
      else
      {
         //Get the bytes using getBytes() - better for smaller byte[]
         return getVarBinaryColumn(rs, columnIndex);
      }
   }
  
   protected void getLocks(List refs)
   {
      Iterator iter = refs.iterator();
      while (iter.hasNext())
      {
         MessageReference ref = (MessageReference)iter.next();
         Message m = ref.getMessage();
         LockMap.instance.obtainLock(m);       
      }
   }
  
   protected void releaseLocks(List refs)
   {
      Iterator iter = refs.iterator();
      while (iter.hasNext())
      {
         MessageReference ref = (MessageReference)iter.next();
         Message m = ref.getMessage();
         LockMap.instance.releaseLock(m);        
      }
   }
  
   protected void logBatchUpdate(String name, int[] rows, String action)
   {
      int count = 0;
      for (int i = 0; i < rows.length; i++)
      {
         count += rows[i];
      }
      log.trace("Batch update " + name + ", " + action + " total of " + count + " rows");
   }
  
   protected int updateWithRetry(PreparedStatement ps) throws Exception
   {
      return updateWithRetry(ps, false)[0];
   }
  
   protected int[] updateWithRetryBatch(PreparedStatement ps) throws Exception
   {
      return updateWithRetry(ps, true);
   }
  
   //PersistentServiceSupport overrides ----------------------------
  
   protected Map getDefaultDDLStatements()
   {
      Map map = new LinkedHashMap();
      //Message reference
      map.put("CREATE_MESSAGE_REFERENCE",
              "CREATE TABLE JBM_MSG_REF (CHANNEL_ID BIGINT, " +
              "MESSAGE_ID BIGINT, TRANSACTION_ID BIGINT, STATE CHAR(1), ORD BIGINT, PAGE_ORD BIGINT, " +
              "DELIVERY_COUNT INTEGER, SCHED_DELIVERY BIGINT, PRIMARY KEY(CHANNEL_ID, MESSAGE_ID))");
      map.put("CREATE_IDX_MESSAGE_REF_TX", "CREATE INDEX JBM_MSG_REF_TX ON JBM_MSG_REF (TRANSACTION_ID)");
      map.put("CREATE_IDX_MESSAGE_REF_ORD", "CREATE INDEX JBM_MSG_REF_ORD ON JBM_MSG_REF (ORD)");
      map.put("CREATE_IDX_MESSAGE_REF_PAGE_ORD", "CREATE INDEX JBM_MSG_REF__PAGE_ORD ON JBM_MSG_REF (PAGE_ORD)");
      map.put("CREATE_IDX_MESSAGE_REF_MESSAGE_ID", "CREATE INDEX JBM_MSG_REF_MESSAGE_ID ON JBM_MSG_REF (MESSAGE_ID)");     
      map.put("CREATE_IDX_MESSAGE_REF_SCHED_DELIVERY", "CREATE INDEX JBM_MSG_REF_SCHED_DELIVERY ON JBM_MSG_REF (SCHED_DELIVERY)");
      //Message
      map.put("CREATE_MESSAGE",
              "CREATE TABLE JBM_MSG (MESSAGE_ID BIGINT, RELIABLE CHAR(1), " +
              "EXPIRATION BIGINT, TIMESTAMP BIGINT, PRIORITY TINYINT, HEADERS LONGVARBINARY, " +
              "PAYLOAD LONGVARBINARY, CHANNEL_COUNT INTEGER, TYPE TINYINT, " +
              "PRIMARY KEY (MESSAGE_ID))");
      //Transaction
      map.put("CREATE_TRANSACTION",
              "CREATE TABLE JBM_TX (" +
              "TRANSACTION_ID BIGINT, BRANCH_QUAL VARBINARY(254), " +
              "FORMAT_ID INTEGER, GLOBAL_TXID VARBINARY(254), PRIMARY KEY (TRANSACTION_ID))");
      //Counter
      map.put("CREATE_COUNTER",
              "CREATE TABLE JBM_COUNTER (NAME VARCHAR(255), NEXT_ID BIGINT, PRIMARY KEY(NAME))");
      return map;
   }
     
   protected Map getDefaultDMLStatements()
   {               
      Map map = new LinkedHashMap();
      //Message reference
      map.put("INSERT_MESSAGE_REF",
              "INSERT INTO JBM_MSG_REF (CHANNEL_ID, MESSAGE_ID, TRANSACTION_ID, STATE, ORD, PAGE_ORD, DELIVERY_COUNT, SCHED_DELIVERY) " +
              "VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
      map.put("DELETE_MESSAGE_REF", "DELETE FROM JBM_MSG_REF WHERE MESSAGE_ID=? AND CHANNEL_ID=? AND STATE='C'");
      map.put("UPDATE_MESSAGE_REF",
              "UPDATE JBM_MSG_REF SET TRANSACTION_ID=?, STATE='-' " +
              "WHERE MESSAGE_ID=? AND CHANNEL_ID=? AND STATE='C'");
      map.put("UPDATE_PAGE_ORDER", "UPDATE JBM_MSG_REF SET PAGE_ORD = ? WHERE MESSAGE_ID=? AND CHANNEL_ID=?");
      map.put("COMMIT_MESSAGE_REF1", "UPDATE JBM_MSG_REF SET STATE='C', TRANSACTION_ID = NULL WHERE TRANSACTION_ID=? AND STATE='+'");
      map.put("COMMIT_MESSAGE_REF2", "DELETE FROM JBM_MSG_REF WHERE TRANSACTION_ID=? AND STATE='-'");
      map.put("ROLLBACK_MESSAGE_REF1", "DELETE FROM JBM_MSG_REF WHERE TRANSACTION_ID=? AND STATE='+'");
      map.put("ROLLBACK_MESSAGE_REF2", "UPDATE JBM_MSG_REF SET STATE='C', TRANSACTION_ID = NULL WHERE TRANSACTION_ID=? AND STATE='-'");
      map.put("LOAD_PAGED_REFS",
              "SELECT MESSAGE_ID, DELIVERY_COUNT, PAGE_ORD, SCHED_DELIVERY FROM JBM_MSG_REF " +
              "WHERE CHANNEL_ID = ? AND PAGE_ORD BETWEEN ? AND ? ORDER BY PAGE_ORD");
      map.put("LOAD_UNPAGED_REFS",
              "SELECT MESSAGE_ID, DELIVERY_COUNT, SCHED_DELIVERY FROM JBM_MSG_REF WHERE STATE = 'C' " +
              "AND CHANNEL_ID = ? AND PAGE_ORD IS NULL ORDER BY ORD");
      map.put("LOAD_REFS",
              "SELECT MESSAGE_ID, DELIVERY_COUNT, SCHED_DELIVERY FROM JBM_MSG_REF WHERE STATE = 'C' " +
              "AND CHANNEL_ID = ? ORDER BY ORD");     
     
      map.put("UPDATE_REFS_NOT_PAGED", "UPDATE JBM_MSG_REF SET PAGE_ORD = NULL WHERE PAGE_ORD BETWEEN ? AND ? AND CHANNEL_ID=?");      
      map.put("SELECT_MIN_MAX_PAGE_ORD", "SELECT MIN(PAGE_ORD), MAX(PAGE_ORD) FROM JBM_MSG_REF WHERE CHANNEL_ID = ?");
      map.put("SELECT_EXISTS_REF", "SELECT MESSAGE_ID FROM JBM_MSG_REF WHERE CHANNEL_ID = ? AND MESSAGE_ID = ?");
      map.put("SELECT_EXISTS_REF_MESSAGE_ID", "SELECT MESSAGE_ID FROM JBM_MSG_REF WHERE MESSAGE_ID = ?");
      map.put("UPDATE_DELIVERY_COUNT", "UPDATE JBM_MSG_REF SET DELIVERY_COUNT = ? WHERE CHANNEL_ID = ? AND MESSAGE_ID = ?");
      map.put("UPDATE_CHANNEL_ID", "UPDATE JBM_MSG_REF SET CHANNEL_ID = ? WHERE CHANNEL_ID = ?");
      //Message
      map.put("LOAD_MESSAGES",
              "SELECT MESSAGE_ID, RELIABLE, EXPIRATION, TIMESTAMP, " +
              "PRIORITY, HEADERS, PAYLOAD, TYPE " +
              "FROM JBM_MSG");
      map.put("INSERT_MESSAGE",
              "INSERT INTO JBM_MSG (MESSAGE_ID, RELIABLE, EXPIRATION, " +
              "TIMESTAMP, PRIORITY, HEADERS, PAYLOAD, CHANNEL_COUNT, TYPE) " +          
              "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" );
      map.put("INC_CHANNEL_COUNT", "UPDATE JBM_MSG SET CHANNEL_COUNT = CHANNEL_COUNT + 1 WHERE MESSAGE_ID=?");
      map.put("DEC_CHANNEL_COUNT", "UPDATE JBM_MSG SET CHANNEL_COUNT = CHANNEL_COUNT - 1 WHERE MESSAGE_ID=?");
      map.put("DELETE_MESSAGE", "DELETE FROM JBM_MSG WHERE MESSAGE_ID=? AND CHANNEL_COUNT = 0");
      map.put("MESSAGE_ID_COLUMN", "MESSAGE_ID");
      map.put("MESSAGE_EXISTS", "SELECT MESSAGE_ID FROM JBM_MSG WHERE MESSAGE_ID = ?");
      //Transaction
      map.put("INSERT_TRANSACTION",
              "INSERT INTO JBM_TX (TRANSACTION_ID, BRANCH_QUAL, FORMAT_ID, GLOBAL_TXID) " +
              "VALUES(?, ?, ?, ?)");
      map.put("DELETE_TRANSACTION", "DELETE FROM JBM_TX WHERE TRANSACTION_ID = ?");
      map.put("SELECT_PREPARED_TRANSACTIONS", "SELECT TRANSACTION_ID, BRANCH_QUAL, FORMAT_ID, GLOBAL_TXID FROM JBM_TX");
      map.put("SELECT_MESSAGE_ID_FOR_REF", "SELECT MESSAGE_ID, CHANNEL_ID FROM JBM_MSG_REF WHERE TRANSACTION_ID = ? AND STATE = '+' ORDER BY ORD");
      map.put("SELECT_MESSAGE_ID_FOR_ACK", "SELECT MESSAGE_ID, CHANNEL_ID FROM JBM_MSG_REF WHERE TRANSACTION_ID = ? AND STATE = '-' ORDER BY ORD");
     
      //Counter
      map.put("UPDATE_COUNTER", "UPDATE JBM_COUNTER SET NEXT_ID = ? WHERE NAME=?");
      map.put("SELECT_COUNTER", "SELECT NEXT_ID FROM JBM_COUNTER WHERE NAME=?");
      map.put("INSERT_COUNTER", "INSERT INTO JBM_COUNTER (NAME, NEXT_ID) VALUES (?, ?)");
      //Other
      map.put("SELECT_ALL_CHANNELS", "SELECT DISTINCT(CHANNEL_ID) FROM JBM_MSG_REF");     
      return map;
   }
  
   // Private -------------------------------------------------------
  

  
   private int[] updateWithRetry(PreparedStatement ps, boolean batch) throws Exception
   {
      final int MAX_TRIES = 25;     
     
      int rows = 0;
     
      int[] rowsArr = null;
     
      int tries = 0;
     
      while (true)
      {
         try
         {
            if (batch)
            {
               rowsArr = ps.executeBatch();
            }
            else
            {
               rows = ps.executeUpdate();
            }
           
            if (tries > 0)
            {
               log.warn("Update worked after retry");
            }
            break;
         }
         catch (SQLException e)
         {
            log.warn("SQLException caught - assuming deadlock detected, try:" + (tries + 1), e);
            tries++;
            if (tries == MAX_TRIES)
            {
               log.error("Retried " + tries + " times, now giving up");
               throw new IllegalStateException("Failed to update references");
            }
            log.warn("Trying again after a pause");
            //Now we wait for a random amount of time to minimise risk of deadlock
            Thread.sleep((long)(Math.random() * 500));
        
      }
     
      if (batch)
      {
         return rowsArr;
      }
      else
      {
         return new int[] { rows };
      }
   }
  
   private List getMessageChannelPair(String sqlQuery, long transactionId) throws Exception
   {
      if (trace) log.trace("loading message and channel ids for tx [" + transactionId + "]");
     
      Connection conn = null;
      PreparedStatement ps = null;
      ResultSet rs = null;
      TransactionWrapper wrap = new TransactionWrapper();
     
      try
      {
         conn = ds.getConnection();
        
         ps = conn.prepareStatement(sqlQuery);
        
         ps.setLong(1, transactionId);
        
         rs = ps.executeQuery();
        
         //Don't use a Map. A message could be in multiple channels in a tx, so if you use a map
         //when you put the same message again it's going to overwrite the previous put!!
        
         List holders = new ArrayList();
        
         //Unique set of messages
         Set msgIds = new HashSet();
        
         //TODO it would probably have been simpler just to have done all this in a SQL JOIN rather
         //than do the join in memory.....
        
         class Holder
         {
            long messageId;
            long channelId;
            Holder(long messageId, long channelId)
            {
               this.messageId = messageId;
               this.channelId = channelId;
            }
         }
                 
         while(rs.next())
         {           
            long messageId = rs.getLong(1);
            long channelId = rs.getLong(2);
           
            Holder holder = new Holder(messageId, channelId);
           
            holders.add(holder);
                       
            msgIds.add(new Long(messageId));
           
            if (trace) log.trace("Loaded MsgID: " + messageId + " and ChannelID: " + channelId);
         }
        
         Map messageMap = new HashMap();
        
         List messages = getMessages(new ArrayList(msgIds));
        
         for (Iterator iter = messages.iterator(); iter.hasNext(); )
         {
            Message msg = (Message)iter.next();
           
            messageMap.put(new Long(msg.getMessageID()), msg);           
         }
        
         List returnList = new ArrayList();
        
         for (Iterator iter = holders.iterator(); iter.hasNext(); )
         {
            Holder holder = (Holder)iter.next();
           
            Message msg = (Message)messageMap.get(new Long(holder.messageId));
           
            if (msg == null)
            {
               throw new IllegalStateException("Cannot find message " + holder.messageId);
            }
           
            MessageChannelPair pair = new MessageChannelPair(msg, holder.channelId);
           
            returnList.add(pair);
         }
        
         return returnList;
      }
      catch (Exception e)
      {
         wrap.exceptionOccurred();
         throw e;
      }
      finally
      {
         if (rs != null)
         {
            try
            {
               rs.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (ps != null)
         {
            try
            {
               ps.close();
            }
            catch (Throwable e)
            {
            }
         }
         if (conn != null)
         {
            try
            {
               conn.close();
            }
            catch (Throwable e)
            {
            }
         }
         wrap.end();
      }
   }
  
   private synchronized long getOrdering()
   {
      //We generate the ordering for the message reference by taking the lowest 48 bits of the current time and
      //concatenating with a 15 bit rotating counter to form a string of 63 bits which we then place
      //in the right most bits of a long, giving a positive signed 63 bit integer.
     
      //Having a time element in the ordering means we don't have to maintain a counter in the database
      //It also helps with failover since if two queues merge after failover then, the ordering will mean
      //their orderings interleave nicely and they still get consumed in pretty much time order
     
      //We only have to guarantee ordering per session, so having slight differences of time on different nodes is
      //not a problem
     
      //The time element is good for about 8919 years - if you're still running JBoss Messaging then, I suggest you need an
      //upgrade!
     
      long order = System.currentTimeMillis();
     
      order = order << 15;
     
      order = order | orderCount;
     
      if (orderCount == Short.MAX_VALUE)
      {
         orderCount = 0;
      }
      else
      {
         orderCount++;
      }
     
      return order;
   }

   // Inner classes -------------------------------------------------
       
   private static class ChannelRefPair
   {
      private long channelID;
      private MessageReference ref;
     
      private ChannelRefPair(long channelID, MessageReference ref)
      {
         this.channelID = channelID;
         this.ref = ref;
      }
   }
  
   private class TransactionCallback implements TxCallback
   {
      private Transaction tx;
     
      private List refsToAdd;
     
      private List refsToRemove;
     
      private TransactionCallback(Transaction tx)
      {
         this.tx = tx;
        
         refsToAdd = new ArrayList();
        
         refsToRemove = new ArrayList();
      }
     
      private void addReferenceToAdd(long channelId, MessageReference ref)
      {
         refsToAdd.add(new ChannelRefPair(channelId, ref));
      }
     
      private void addReferenceToRemove(long channelId, MessageReference ref)
      {
         refsToRemove.add(new ChannelRefPair(channelId, ref));
      }
     
      public void afterCommit(boolean onePhase)
      {
         //NOOP
      }
     
      public void afterPrepare()
      {
         //NOOP
      }
     
      public void afterRollback(boolean onePhase)
      {
         //NOOP
      }
     
      public void beforeCommit(boolean onePhase) throws Exception
      {
         if (onePhase)
         {
            handleBeforeCommit1PC(refsToAdd, refsToRemove, tx);
         }
         else
         {
            handleBeforeCommit2PC(refsToRemove, tx);
         }
      }
     
      public void beforePrepare() throws Exception
      {
         handleBeforePrepare(refsToAdd, refsToRemove, tx);
      }
     
      public void beforeRollback(boolean onePhase) throws Exception
      {
         if (onePhase)
         {
            //NOOP - nothing in db
         }
         else
         {
            handleBeforeRollback(refsToAdd, tx);
         }
      }
   }
  
   static class MessageOrderComparator implements Comparator
   {
      static MessageOrderComparator instance = new MessageOrderComparator();
     
      public int compare(Object o1, Object o2)
      {       
         MessageReference ref1 = (MessageReference)o1;
         MessageReference ref2 = (MessageReference)o2;

         long id1 = ref1.getMessage().getMessageID();        
         long id2 = ref2.getMessage().getMessageID();
        
         return (id1 < id2 ? -1 : (id1 == id2 ? 0 : 1));
      }     
   }
  
}
TOP

Related Classes of org.jboss.messaging.core.plugin.JDBCPersistenceManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.