import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;

import com.ibm.mq.MQException;
import com.ibm.mq.MQGetMessageOptions;
import com.ibm.mq.MQMessage;
import com.ibm.mq.MQPutMessageOptions;
import com.ibm.mq.MQQueue;
import com.ibm.mq.MQQueueManager;
import com.ibm.mq.constants.CMQC;

/**
 * Program Name
 *  RQClient01
 *
 * Description
 *  This java class will:
 *  - connect to a remote queue manager
 *  - open request queue
 *  - open temporary dynamic queue for reply messages
 *  - loop 10 times  
 *  - - put a request message to a queue
 *  - - insert record to DB
 *  - - get COA, COD & reply message
 *  - - update record in DB
 *  - close queues
 *  - disconnect from queue manager
 *  - close DB 
 *
 * Sample Command Line Parameters
 *  -m MQA1 -h 127.0.0.1 -p 1414 -c TEST.CHL -q REQUEST.Q -u userId -x password
 *
 * @author Roger Lacroix, Capitalware Inc.
 */
public class RQClient01
{
   private static final String            HEX = "0123456789ABCDEF";
   private static final SimpleDateFormat  lOGGER_TIMESTAMP = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");

   private Hashtable<String, String>   params = null;
   private Hashtable<String, Object>   mqht;
   private String                      qMgrName;
   private String                      outputQName;
   private static PrintStream          ps = null;
   private static boolean              debugMe = false;

   /**
    * The constructor
    * @param args
    * @throws IllegalArgumentException
    * @throws ClassNotFoundException
    */
   public RQClient01(String[] args)  throws IllegalArgumentException, ClassNotFoundException
   {
      super();
//      writeLogToFile();  // have logging info go to a file rather than console.
      
      if (debugMe) RQClient01.logger("");
      
      RQClient01.logger("Starting");
      
      initParms(args);

      // load the sqlite-JDBC driver using the current class loader
      Class.forName("org.sqlite.JDBC");
      String dbName = "RQClient01_"+qMgrName+".db";
      String dbURL = "jdbc:sqlite:" + dbName;
      if (!((new File(dbName)).exists()))
         createNewTable(dbURL);
      begin(dbURL);
   }

   /**
    * Extract the command-line parameters and initialize the MQ HashTable.
    * @param args
    * @throws IllegalArgumentException
    */
   private void initParms(String[] args) throws IllegalArgumentException
   {
      if (debugMe) RQClient01.logger("");

      int port = 1414;
      params = new Hashtable<String, String>(10);
      if (args.length > 0 && (args.length % 2) == 0)
      {
         for (int i = 0; i < args.length; i += 2)
         {
            params.put(args[i], args[i + 1]);
         }
      }
      else
      {
         throw new IllegalArgumentException();
      }

      if (allParamsPresent())
      {
         qMgrName = (String) params.get("-m");
         outputQName = (String) params.get("-q");
         
         try
         {
            port = Integer.parseInt((String) params.get("-p"));
         }
         catch (NumberFormatException e)
         {
            port = 1414;
         }

         mqht = new Hashtable<String, Object>();
         
         mqht.put(CMQC.CHANNEL_PROPERTY, params.get("-c"));
         mqht.put(CMQC.HOST_NAME_PROPERTY, params.get("-h"));
         mqht.put(CMQC.PORT_PROPERTY, new Integer(port));
         mqht.put(CMQC.USER_ID_PROPERTY, params.get("-u"));
         mqht.put(CMQC.PASSWORD_PROPERTY, params.get("-x"));

         // I don't want to see MQ exceptions at the console.
         MQException.log = null;
      }
      else
      {
         throw new IllegalArgumentException();
      }
   }

   /**
    * Make sure the required parameters are present.
    * @return true/false
    */
   private boolean allParamsPresent()
   {
      if (debugMe) RQClient01.logger("");

      boolean b = params.containsKey("-h") && params.containsKey("-p") &&
                  params.containsKey("-c") && params.containsKey("-m") &&
                  params.containsKey("-q") && 
                  params.containsKey("-u") && params.containsKey("-x");
      if (b)
      {
         try
         {
            Integer.parseInt((String) params.get("-p"));
         }
         catch (NumberFormatException e)
         {
            b = false;
         }
      }

      return b;
   }

   /**
    * Connect, open queues, loop, close queues and disconnect.
    * @param dbURL
    */
   private void begin(String dbURL)
   {
      if (debugMe) RQClient01.logger("");

      Connection dbConn = null;
      MQQueueManager qMgr = null;
      MQQueue outQ = null;
      MQQueue inQ = null;
      int outputOpenOptions = CMQC.MQOO_OUTPUT + CMQC.MQOO_FAIL_IF_QUIESCING;

      int inputOpenOptions = CMQC.MQOO_INQUIRE + CMQC.MQOO_INPUT_SHARED + CMQC.MQOO_FAIL_IF_QUIESCING;
      String modelQName    = "SYSTEM.DEFAULT.MODEL.QUEUE";
      String dynamicQName  = "AMQ.RQClient01.*";
      String replyQName = "";

      try
      {
         // create a database connection
         dbConn = DriverManager.getConnection(dbURL);
         
         qMgr = new MQQueueManager(qMgrName, mqht);
         RQClient01.logger("successfully connected to "+ qMgrName);

         outQ = qMgr.accessQueue(outputQName, outputOpenOptions);
         RQClient01.logger("successfully opened "+ outputQName);

         /* Create Temporary Dynamic Queue to receive COA, COD reply messages*/
         inQ = qMgr.accessQueue(modelQName, inputOpenOptions, null, dynamicQName, null);         
         replyQName = inQ.getName();
         RQClient01.logger("successfully opened "+ replyQName);
         
         for (int mCount=1; mCount <= 10; mCount++)
         {
            byte[] msgId = sendRequestMessage(mCount, outQ, replyQName, dbConn);
            getReplyMessage(inQ, msgId, dbConn);
         }
      }
      catch (MQException e)
      {
         RQClient01.logger("cc=" +e.completionCode + " : rc=" + e.reasonCode);
      }
      catch (SQLException e)
      {
         RQClient01.logger(e.getLocalizedMessage());
      }
      finally
      {
         if (outQ != null)
         {
            try
            {
               outQ.close();
               RQClient01.logger("closed: "+ outputQName);
            }
            catch (MQException e)
            {
               RQClient01.logger("close cc=" +e.completionCode + " : rc=" + e.reasonCode);
            }
         }

         if (inQ != null)
         {
            try
            {
               inQ.close();
               RQClient01.logger("closed: "+ replyQName);
            }
            catch (MQException e)
            {
               RQClient01.logger("close cc=" +e.completionCode + " : rc=" + e.reasonCode);
            }
         }
         
         if ( qMgr != null)
         {
            try
            {
               qMgr.disconnect();
               RQClient01.logger("disconnected from "+ qMgrName);
            }
            catch (MQException e)
            {
               RQClient01.logger("disconnect cc=" +e.completionCode + " : rc=" + e.reasonCode);
            }
         }
         
         try
         {
            if (dbConn != null)
               dbConn.close();
         }
         catch (SQLException e)
         {
            // connection close failed.
            RQClient01.logger(e.getLocalizedMessage());
         }
      }
   }

   /**
    * Send a 'Request' message to the backend server component
    * Set the replyToQ, replyToQMgr and report fields to get the COA, COD and reply messages. 
    * 
    * @param mCount
    * @param outQ
    * @param replyQName
    * @param dbConn
    * @return
    */
   private byte[] sendRequestMessage(int mCount, MQQueue outQ, String replyQName, Connection dbConn)
   {
      if (debugMe) RQClient01.logger("");

      byte[] msgId = new byte[0];
      String msgText = "Message Request # " + mCount + " from RQClient01";
      MQPutMessageOptions pmo = new MQPutMessageOptions();

      MQMessage requestMsg = new MQMessage();
      requestMsg.messageId = CMQC.MQMI_NONE;
      requestMsg.correlationId = CMQC.MQCI_NONE;
      requestMsg.format = CMQC.MQFMT_STRING;
      requestMsg.messageType = CMQC.MQMT_REQUEST;

      requestMsg.replyToQueueManagerName = qMgrName;
      requestMsg.replyToQueueName = replyQName;
      requestMsg.report = CMQC.MQRO_COA | CMQC.MQRO_COD;
      
      try
      {
         requestMsg.writeString(msgText);
         Date beforePut_TS = new Date();
         outQ.put(requestMsg, pmo);
         RQClient01.logger("Sent request message: \"" + msgText + "\"");
         msgId = requestMsg.messageId;
         
         // Insert record in table
         insertToTable(dbConn, bytesToHex(requestMsg.messageId), beforePut_TS, requestMsg.putDateTime.getTime());
      }
      catch (MQException e)
      {
         RQClient01.logger("cc=" +e.completionCode + " : rc=" + e.reasonCode);
      }
      catch (IOException ioex)
      {
         RQClient01.logger(ioex.getLocalizedMessage());
      }
      
      return msgId;
   }

   /**
    * Get up to 3 messages: COA, COD & reply messages from the input queue (reply queue).
    * Wait up to 10 seconds for each message.
    *
    * @param inQ
    * @param requestMsgId
    * @param dbConn
    */
   private void getReplyMessage(MQQueue inQ, byte[] requestMsgId, Connection dbConn)
   {
      if (debugMe) RQClient01.logger("");

      String msgIdAsStr = bytesToHex(requestMsgId);

      MQGetMessageOptions gmo = new MQGetMessageOptions();
      gmo.options = CMQC.MQGMO_FAIL_IF_QUIESCING + CMQC.MQGMO_WAIT;
      gmo.waitInterval = 10 * 1000; // wait up to 10 seconds
      gmo.matchOptions = CMQC.MQMO_MATCH_CORREL_ID;
      MQMessage replyMsg;

      for (int i=0; i < 3; i++)
      {
         replyMsg = new MQMessage();
         replyMsg.messageId = CMQC.MQMI_NONE;

         // Specifically get the message with the matching value.
         replyMsg.correlationId = requestMsgId;
         
         try
         {
            inQ.get(replyMsg, gmo);

            if ( (replyMsg.messageType == CMQC.MQMT_REPORT) && 
                 (replyMsg.feedback == CMQC.MQFB_COA) )
            {
               updateTable(dbConn, msgIdAsStr, "COA_TS", replyMsg.putDateTime.getTime());
               RQClient01.logger("Received COA");
            }
            else if ( (replyMsg.messageType == CMQC.MQMT_REPORT) && 
                      (replyMsg.feedback == CMQC.MQFB_COD) )
            {
               updateTable(dbConn, msgIdAsStr, "COD_TS", replyMsg.putDateTime.getTime());
               RQClient01.logger("Received COD");
            }
            else
            {
               updateTable(dbConn, msgIdAsStr, "Received_TS", new Date());
               updateTable(dbConn, msgIdAsStr, "Reply_TS", replyMsg.putDateTime.getTime());
               
               byte[] b = new byte[replyMsg.getMessageLength()];
               replyMsg.readFully(b);
               RQClient01.logger("Received reply message: \"" + new String(b) + "\"");
            }
         }
         catch (MQException e)
         {
            RQClient01.logger("cc=" +e.completionCode + " : rc=" + e.reasonCode);
         }
         catch (IOException ioex)
         {
            RQClient01.logger(ioex.getLocalizedMessage());
         }
      }
   }

   /**
    * Create new table in DB.
    * @param dbURL
    */
   private void createNewTable(String dbURL)
   {
      if (debugMe) RQClient01.logger("");

      Connection dbConn = null;
      
      try
      {
         // create a database connection
         dbConn = DriverManager.getConnection(dbURL);
         Statement statement = dbConn.createStatement();

         statement.executeUpdate("CREATE TABLE 'MQSLA' ( " +
                                                       "   'MessageId'     TEXT NOT NULL PRIMARY KEY UNIQUE," +
                                                       "   'BeforePut_TS'  TEXT," +
                                                       "   'Put_TS'        TEXT," +
                                                       "   'COA_TS'        TEXT," +
                                                       "   'COD_TS'        TEXT," +
                                                       "   'Reply_TS'      TEXT," +
                                                       "   'Received_TS'   TEXT"  +
                                                       ");");

         RQClient01.logger("MQSLA table created.");
      }
      catch (SQLException e)
      {
         RQClient01.logger(e.getLocalizedMessage());
      }
      finally
      {
         try
         {
            if (dbConn != null)
               dbConn.close();
         }
         catch (SQLException e)
         {
            RQClient01.logger(e.getLocalizedMessage());
         }
      }
   }


   /**
    * Insert a new row into the table
    * @param dbConn
    * @param msgId
    * @param beforePut_TS
    * @param put_TS
    */
   private void insertToTable(Connection dbConn, String msgId, Date beforePut_TS, Date put_TS)
   {
      if (debugMe) RQClient01.logger("");

      SimpleDateFormat dtFormat = new SimpleDateFormat("HH:mm:ss.SSS");
      PreparedStatement pStmt = null;
      PreparedStatement pStmt2 = null;

      try
      {
         pStmt = dbConn.prepareStatement("INSERT INTO MQSLA (MessageId,BeforePut_TS,Put_TS) VALUES (?, ?, ?)");
         pStmt.setString(1, msgId);
         pStmt.setString(2, dtFormat.format(beforePut_TS));
         pStmt.setString(3, dtFormat.format(put_TS));
         
         int rcode = pStmt.executeUpdate();
         RQClient01.logger("Insert rcode="+rcode);

         pStmt2 = dbConn.prepareStatement("SELECT MessageId,BeforePut_TS,Put_TS FROM MQSLA WHERE MessageId = ?");
         pStmt2.setString(1, msgId);

         ResultSet rs = pStmt2.executeQuery();
         while (rs.next())
         {
            // read the result set
            RQClient01.logger("MessageId    = " + rs.getString("MessageId"));
            RQClient01.logger("BeforePut_TS = " + rs.getString("BeforePut_TS"));
            RQClient01.logger("Put_TS       = " + rs.getString("Put_TS"));
         }
      }
      catch (SQLException e)
      {
         RQClient01.logger(e.getLocalizedMessage());
      }
      finally
      {
         closePStmt(pStmt);
         closePStmt(pStmt2);
      }
   }

   /**
    * Update an existing row in the table 
    * @param dbConn
    * @param msgId
    * @param colName
    * @param colValue
    */
   private void updateTable(Connection dbConn, String msgId, String colName, Date colValue)
   {
      if (debugMe) RQClient01.logger("");

      SimpleDateFormat dtFormat = new SimpleDateFormat("HH:mm:ss.SSS");
      PreparedStatement pStmt = null;
      PreparedStatement pStmt2 = null;

      try
      {
         pStmt = dbConn.prepareStatement("UPDATE MQSLA SET " + colName + "= ? WHERE MessageId = ?");
         pStmt.setString(1, dtFormat.format(colValue));
         pStmt.setString(2, msgId);
         
         int rcode = pStmt.executeUpdate();
         RQClient01.logger("Update rcode="+rcode);

         pStmt2 = dbConn.prepareStatement("SELECT * FROM MQSLA WHERE MessageId = ?");
         pStmt2.setString(1, msgId);

         ResultSet rs = pStmt2.executeQuery();
         while (rs.next())
         {
            // read the result set
            RQClient01.logger("MessageId    = " + rs.getString("MessageId"));
            RQClient01.logger("BeforePut_TS = " + rs.getString("BeforePut_TS"));
            RQClient01.logger("Put_TS       = " + rs.getString("Put_TS"));
            RQClient01.logger("COA_TS       = " + rs.getString("COA_TS"));
            RQClient01.logger("COD_TS       = " + rs.getString("COD_TS"));
            RQClient01.logger("Reply_TS     = " + rs.getString("Reply_TS"));
            RQClient01.logger("Received_TS  = " + rs.getString("Received_TS"));
         }
      }
      catch (SQLException e)
      {
         RQClient01.logger(e.getLocalizedMessage());
      }
      finally
      {
         closePStmt(pStmt);
         closePStmt(pStmt2);
      }
   }
   /**
    * CLose the Prepared Statement
    * @param pStmt
    */
   private void closePStmt(PreparedStatement pStmt)
   {
      try
      {
         if (pStmt != null)
            pStmt.close();
      }
      catch (SQLException e)
      {
         RQClient01.logger(e.getLocalizedMessage());
      }
   }
   
   /**
    * Convenience method to convert a byte array to a hex string.
    *
    * @param data the byte[] to convert
    * @return String the converted byte[]
    */
   private String bytesToHex(byte[] data)
   {
      if (debugMe) RQClient01.logger("");

      StringBuffer buf = new StringBuffer();
      for (int i = 0; i < data.length; i++)
      {
         buf.append(byteToHex(data[i]));
      }
      return buf.toString();
   }

   /**
    * Convenience method to convert a byte to a hex string.
    *
    * @param data the byte to convert
    * @return String the converted byte
    */
   private String byteToHex(byte data)
   {
      int hi = (data & 0xF0) >> 4;
      int lo = (data & 0x0F);
      return "" + HEX.charAt(hi) + HEX.charAt(lo);
   }

   /**
    * Code to setup to have logging information go to a file rather than console.
    */
   private void writeLogToFile()
   {
      if (debugMe) RQClient01.logger("");

      try
      {
         String logFile = "RQClient01.log";

         if ((new File(logFile)).length() > 1048576)                     // Larger than 1MB
            ps = new PrintStream(new FileOutputStream(logFile, false));  // create new
         else
            ps = new PrintStream(new FileOutputStream(logFile, true));   // append
      }
      catch(Exception e)
      {
         RQClient01.logger(e.getLocalizedMessage());
      }

      if (ps != null)
      {
         System.setOut(ps);
         System.setErr(ps);
      }
   }

   /**
    * A simple logger method
    * @param data
    */
   public static void logger(String data)
   {
      String className = Thread.currentThread().getStackTrace()[2].getClassName();

      // Remove the package info.
      if ( (className != null) && (className.lastIndexOf('.') != -1) )
         className = className.substring(className.lastIndexOf('.')+1);

      System.out.println(lOGGER_TIMESTAMP.format(new Date())+" "+className+": "+Thread.currentThread().getStackTrace()[2].getMethodName()+": "+data);
   }

   /**
    * main line
    * @param args
    */
   public static void main(String[] args)
   {
      if (System.getProperty("RQClient01_DEBUG") != null)
         debugMe = true;

      try
      {
         new RQClient01(args);
      }
      catch (ClassNotFoundException e)
      {
         RQClient01.logger(e.getLocalizedMessage());
      }
      catch (IllegalArgumentException e)
      {
         System.err.println("Usage: java RQClient01 -m QueueManagerName -h host -p port -c channel -q RequestQueueName -u userId -x password");
      }
      
      RQClient01.logger("Ending");
   }
}
