package com.mcafee; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Enumeration; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.Destination; import javax.jms.InvalidSelectorException; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageListener; import javax.jms.Queue; import javax.jms.QueueBrowser; import javax.jms.QueueSession; import javax.jms.Session; import javax.jms.Topic; import javax.jms.TopicSession; import javax.jms.TopicSubscriber; import javax.naming.InitialContext; import javax.naming.NamingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class writes the contents of a JMS destination (Queue, Topic and DurableSubscribers) \ * to the local file system using the JMSWriter object. * @author Gursev Singh Kalra @ McAfee, Inc. * */ public class JmsDumpDestination implements MessageListener { private static final Logger LOG = LoggerFactory.getLogger(JmsDumpDestination.class); private static String filenameIdentifier = "qDump"; private static final int TIMEOUT = 2000; // 2 second timeout for synchronously receiving messages private int msgCountToDump = 0; private JmsLoginInfo loginInfo; private boolean initialized; private JmsDestination dstType; private JmsWriter jmsWriter; private InitialContext ctx; private String msgSelector; private String dstName; private String targetDirectory; private ConnectionFactory connFact; private String connFactName; private Connection connection; private Destination dst; private int msgsWritten = 0; private int msgsTraversed = 0; private boolean dumpComplete; // For durable subscriber dumps private String durableSubscriberName; private String clientId; private MessageCountUpdater messageCountUpdater; /** * Main constructor the initialize the JMS Destination dumps * @param ctx - Initial Context * @param dstName - Destination name * @param connFactName - Name of the connection factory * @param msgSelector - Message selector * @param loginInfo - Login Info */ public JmsDumpDestination(InitialContext ctx, String dstName, String connFactName, String msgSelector, JmsLoginInfo loginInfo) { if(ctx == null || dstName == null || connFactName == null) throw new IllegalArgumentException("Unexpected null object passed to JmsDumpQueue constructor"); this.ctx = ctx; this.dstName = dstName; this.msgSelector = msgSelector; this.connFactName = connFactName; this.loginInfo = loginInfo; this.initialized = false; this.dumpComplete = false; } public void setTargetDirectory(String targetDirectory) { this.targetDirectory = targetDirectory; } public boolean isDumpComplete() { return this.dumpComplete; } public void setMessageCountUpdater(MessageCountUpdater messageCountUpdater) { this.messageCountUpdater = messageCountUpdater; } /** * Initialize JMS destination dump with anonymous authentication and no message selector * @param ctx - Initial Context * @param dstName - Destination name * @param connFactName - Connection factory name * @throws NamingException * @throws JMSException */ public JmsDumpDestination(InitialContext ctx, String dstName, String connFactName) { this(ctx, dstName, connFactName, null, null); } /** * Initialize JMS destination dump with anonymous authentication and a message selector * @param ctx * @param dstName * @param connFactName * @param msgSelector */ public JmsDumpDestination(InitialContext ctx, String dstName, String connFactName, String msgSelector) { this(ctx, dstName, connFactName, msgSelector, null); } /** * Set only when you want your client to use a client Id. The value cannot be null. * @param clientId */ public void setClientId(String clientId) { if(clientId == null) { LOG.info("Null client ID provided"); throw new IllegalArgumentException("Null client ID provided"); } this.clientId = clientId; } /** * Set only when you want to dump a durable subscriber. Both clientID and Durable subscriber name must * be set for the durable subscriber dump functionality to work. If not, an exception will be thrown * during the initialization phased with init. * @param durableSubscriberName */ public void setDurableSubscriberName(String durableSubscriberName) { if(durableSubscriberName == null) { LOG.info("Null durable subscriber name provided provided"); throw new IllegalArgumentException("Null durable subscriber name provided"); } this.durableSubscriberName = durableSubscriberName; } /** * The init method initiates three types of processing * 1. Queue Dump - Via Queue Browsers * 2. Topic Dumps * 3. Durable Subscriber Dumps * * For a Topic, the algorithm to choose a destination when dst is a Topic instance * is as follows based on "clientId" and "durableSubscriberName" values * *<br/> * -------------------------------------------------------------<br/> * clientId | durableSubscriberName | LookFor |<br/> * -------------------------------------------------------------<br/> * null/empty | null/empty | Topic |<br/> * null/empty | value | Throw Error |<br/> * value | null/empty | Topic |<br/> * value | value | Dump Durable Subscriber |<br/> * --------------------------------------------------------------<br/> */ public void init() throws JmsDiggerException { LOG.debug("Entering init method"); try { dst = (Destination) ctx.lookup(dstName); if(dst == null) throw new IllegalArgumentException(dstName + " not found in JNDI"); if(dst instanceof Queue) { dstType = JmsDestination.QUEUE; filenameIdentifier = "queue-" + dstName; } else { if(dst instanceof Topic) { if(JmsHelper.isStringNullOrEmpty(durableSubscriberName)) { dstType = JmsDestination.TOPIC; filenameIdentifier = "topic-" + dstName; } else { if(JmsHelper.isStringNullOrEmpty(clientId)) { LOG.info("clientId cannot be null when durableSubscriberName has a value"); throw new IllegalArgumentException("clientId cannot be null when durableSubscriberName has a value"); } else { dstType = JmsDestination.DURABLESUBSCRIBER; filenameIdentifier = "durableSubscriber-" + dstName; } } } else { throw new IllegalArgumentException(dstName + " is neither a Queue nor a Topic"); } } connFact = (ConnectionFactory) ctx.lookup(this.connFactName); jmsWriter = new JmsWriter(targetDirectory, filenameIdentifier); if(loginInfo == null) connection = (Connection) connFact.createConnection(); else connection = (Connection) connFact.createConnection(loginInfo.getUsername(), loginInfo.getPassword()); // Set clientId - This is very important for durable subscriber identification if(clientId != null) connection.setClientID(clientId); } catch (JmsDiggerException e) { throw e; } catch (NamingException e) { LOG.info("Either a Queue or Destination name not found", e); throw JmsHelper.buildJmsDiggerException("Either a Queue or Destination name not found", e); } catch (JMSException e) { LOG.info("Could not create a connection", e); throw JmsHelper.buildJmsDiggerException("Could not create a connection", e); } msgsWritten = 0; initialized = true; LOG.debug("Leaving init method"); } /** * Initializes the number of messages that objects of this class will * attempt to dump to the local filesystem. If value of count paramter is * less than 0, the value is left unchanged. * @param count */ public void setMsgCountToDump(int count) { LOG.debug("Entering setMsgCountToDump method"); if(count >= 0) msgCountToDump = count; LOG.debug("Leaving setMsgCountToDump method"); } public int getMsgsWritten() { return msgsWritten; } /** * Dumps contents of a Queue. Creates a QueueBrowser, gets the enumeration and writes messages. * @throws FileNotFoundException * @throws IOException * @throws JmsDiggerException * @throws InterruptedException */ private void initQueueDump() throws FileNotFoundException, IOException, JmsDiggerException, InterruptedException { LOG.debug("Entering initQueueDump method"); QueueSession qSession = null; QueueBrowser qBrowser = null; Enumeration qEnum = null; jmsWriter.init(); try { qSession = (QueueSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); if(msgSelector == null) qBrowser = qSession.createBrowser((Queue)dst); else qBrowser = qSession.createBrowser((Queue)dst, msgSelector); connection.start(); boolean breakout = false; while(msgsTraversed <= msgCountToDump || msgCountToDump == 0) { qEnum = qBrowser.getEnumeration(); if(!qEnum.hasMoreElements()) { Thread.sleep(2 * 1000); // Wait and try to get more elements continue; } Message msg = null; while(qEnum.hasMoreElements()) { msg = (Message) qEnum.nextElement(); try { jmsWriter.writeMsg(msg); msgsWritten++; /** * Update the count in GUI */ if(messageCountUpdater != null) { messageCountUpdater.setCount(msgsWritten); Thread t = new Thread(messageCountUpdater); t.start(); } } catch(JmsDiggerException ex) { //Swallow exceptions for failed write messages } msgsTraversed++; if(msgsTraversed >= msgCountToDump && msgCountToDump != 0) { breakout = true; // never break out when msgCountToDump is 0 dumpComplete = true; break; // break from inner while loop } } if(breakout) break; // break from outer while loop } } catch (InvalidSelectorException ex) { LOG.info("Message selector exception encountered", ex); throw JmsHelper.buildJmsDiggerException("Message selector exception encountered", ex); } catch (JMSException ex) { LOG.info("A JMSException occured while creating a QueueBrowser", ex); throw JmsHelper.buildJmsDiggerException("A JMSException occured while creating a QueueBrowser", ex); } try { qBrowser.close(); qSession.close(); } catch (JMSException e) { LOG.info("qBroswer or qSession close failed ", e); //Swallow the exception on close. } if(msgsTraversed > msgsWritten) { LOG.info("Messages Traversed " + msgsTraversed, ", Messages Written: " + msgsWritten); throw JmsHelper.buildJmsDiggerException("Not all messages were written. Check log for more details"); } LOG.debug("Leaving initQueueDump method"); } /** * Write one message at a time. The object of this class is set as a message listener for Topics * Every message is received by this method is in turn written unless the number of messages * written exceeds the target. */ public void onMessage(Message msg){ LOG.debug("Entering onMessage method"); if(msgCountToDump == 0 || msgsTraversed < msgCountToDump) { msgsTraversed++; try { jmsWriter.writeMsg(msg); msgsWritten++; /** * Update the count in GUI */ if(messageCountUpdater != null) { messageCountUpdater.setCount(msgsWritten); Thread t = new Thread(messageCountUpdater); t.start(); } } catch (JmsDiggerException e) { LOG.info("Message write failed", e); } } else { this.dumpComplete = true; } LOG.debug("Leaving onMessage method"); } /** * Dump contents of a topic. * @throws JmsDiggerException * @throws FileNotFoundException * @throws IOException */ private void initTopicDump() throws JmsDiggerException, FileNotFoundException, IOException { LOG.debug("Entering initTopicDump method"); TopicSession tSession; TopicSubscriber tSubscriber; jmsWriter.init(); try { tSession = (TopicSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); tSubscriber = tSession.createSubscriber((Topic)dst); //Set the current object as message listener. All messages will be sent to onMessage method tSubscriber.setMessageListener(this); connection.start(); // start the connection - very important } catch (JMSException e) { LOG.info("Failure while initiating a Topic Dump", e); throw JmsHelper.buildJmsDiggerException("Failure while initiating a Topic Dump", e); } //IMPORTANT: The subscriber and session must not be closed as closing them will cause the // onMessage listener to be dysfunctional and no messages will be received. LOG.debug("Leaving initTopicDump method"); } /** * Dump contents of a durable subscriber. The reads are synchronous and block for duration specified * in the TIMEOUT (milliseconds) static variable. * @throws FileNotFoundException * @throws IOException * @throws JmsDiggerException */ private void initDurableSubscriberDump() throws FileNotFoundException, IOException, JmsDiggerException { // Lot of duplicate code between initTopicDump and initDurableSubscriberDump. //TODO: Potentially combine these functions -- REVIEW and DECIDE. LOG.debug("Entering initDurableSubscriberDump method"); Message msg; TopicSession tSession; TopicSubscriber tSubscriber; jmsWriter.init(); try { tSession = (TopicSession) connection.createSession(false, Session.AUTO_ACKNOWLEDGE); tSubscriber = tSession.createDurableSubscriber((Topic)dst, durableSubscriberName); connection.start(); while(true) { msg = tSubscriber.receive(TIMEOUT); //Synchronous read, returns null when no message is received during TIMEOUT time //Ensure that null message is not passed to JmsWriter as it will throw an IllegalArgumentException if(msg == null) { continue; } if(msgCountToDump == 0 || msgsTraversed < msgCountToDump) { try { jmsWriter.writeMsg(msg); msgsWritten++; /** * Update the count in GUI */ if(messageCountUpdater != null) { messageCountUpdater.setCount(msgsWritten); Thread t = new Thread(messageCountUpdater); t.start(); } } catch (JmsDiggerException ex) { LOG.info("Message write failed ", ex); //Swallow single message write failure exception } msgsTraversed++; } else { this.dumpComplete = true; break; } } } catch (JMSException e) { LOG.info("Failure while performing a Durable Subscriber Dump", e); throw JmsHelper.buildJmsDiggerException("Failure while performing a Durable Subscriber Dump", e); } try { tSubscriber.close(); tSession.close(); } catch (JMSException ex) { //Swallow the close exception } LOG.debug("Leaving initDurableSubscriberDump method"); } /** * The public method that initiates the message dump from different types of destinations based on the * destination type determined by the init call. * @throws IllegalAccessException * @throws JmsDiggerException * @throws FileNotFoundException * @throws IOException * @throws InterruptedException */ public void dump() throws IllegalAccessException, JmsDiggerException, FileNotFoundException, IOException, InterruptedException { LOG.debug("Entering dump method"); if(!initialized) throw new IllegalAccessException("dump method called without initializing"); if(dstType == JmsDestination.QUEUE) initQueueDump(); else { if(dstType == JmsDestination.TOPIC) initTopicDump(); else { if(dstType == JmsDestination.DURABLESUBSCRIBER) initDurableSubscriberDump(); else { LOG.info(dstName + "is neither a Queue nor Topic or a Durable Subscriber"); throw new IllegalArgumentException(dstName + "is neither a Queue nor Topic or a Durable Subscriber"); } } } LOG.debug("Leaving dump method"); } /** * Important to call the close method to close all the file handles * for the jmsWriter and also close the JMS connection. * @throws JMSException * @throws IOException */ public void close() throws JMSException, IOException { LOG.debug("Entering close method"); jmsWriter.close(); connection.close(); LOG.debug("Leaving close method"); } }