/******************************************************************************* * Copyright (C) 2013, 2014, International Business Machines Corporation * All Rights Reserved *******************************************************************************/ package com.ibm.streamsx.messaging.jms; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.jms.JMSException; import javax.jms.Message; import javax.naming.NamingException; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; import com.ibm.streams.operator.OperatorContext; import com.ibm.streams.operator.OperatorContext.ContextCheck; import com.ibm.streams.operator.OutputTuple; import com.ibm.streams.operator.StreamSchema; import com.ibm.streams.operator.StreamingOutput; import com.ibm.streams.operator.Type; import com.ibm.streams.operator.Type.MetaType; import com.ibm.streams.operator.compile.OperatorContextChecker; import com.ibm.streams.operator.logging.LogLevel; import com.ibm.streams.operator.logging.LoggerNames; import com.ibm.streams.operator.logging.TraceLevel; import com.ibm.streams.operator.metrics.Metric; import com.ibm.streams.operator.model.CustomMetric; import com.ibm.streams.operator.model.Parameter; import com.ibm.streams.operator.samples.patterns.ProcessTupleProducer; import com.ibm.streams.operator.state.Checkpoint; import com.ibm.streams.operator.state.ConsistentRegionContext; import com.ibm.streams.operator.state.StateHandler; import com.ibm.streams.operator.types.RString; import com.ibm.streamsx.messaging.common.DataGovernanceUtil; import com.ibm.streamsx.messaging.common.IGovernanceConstants; import com.ibm.streamsx.messaging.common.PropertyProvider; //The JMSSource operator converts a message JMS queue or topic to stream public class JMSSource extends ProcessTupleProducer implements StateHandler{ private static final String CLASS_NAME = "com.ibm.streamsx.messaging.jms.JMSSource"; //$NON-NLS-1$ /** * Create a {@code Logger} specific to this class that will write to the SPL * log facility as a child of the {@link LoggerNames#LOG_FACILITY} * {@code Logger}. The {@code Logger} uses a */ private static Logger logger = Logger.getLogger(LoggerNames.LOG_FACILITY + "." + CLASS_NAME, "com.ibm.streamsx.messaging.jms.JMSMessages"); //$NON-NLS-1$ //$NON-NLS-2$ // variable to hold the output port private StreamingOutput<OutputTuple> dataOutputPort; // Variables required by the optional error output port // hasErrorPort signifies if the operator has error port defined or not // assuming in the beginning that the operator does not have a error output // port by setting hasErrorPort to false // further down in the code, if the no of output ports is 2, we set to true // We send data to error ouput port only in case where hasErrorPort is set // to true which implies that the opeator instance has a error output port // defined. private boolean hasErrorPort = false; // Variable to specify error output port private StreamingOutput<OutputTuple> errorOutputPort; // Create an instance of class JMSConnectionhelper which is responsible for // creating and maintaining connection and receiving messages from JMS // Provider JMSConnectionHelper jmsConnectionHelper; // Variable to hold the message type as received in the connections // document. private MessageClass messageType; // Variable to specify if the JSMProvider is Apache Active MQ or WebSphere // MQ // set to true for Apache Active MQ. private boolean isAMQ; // consistent region context private ConsistentRegionContext consistentRegionContext; // Variables to hold performance metrices for JMSSource // nMessagesRead is the number of messages read successfully. // nMessagesDropped is the number of messages dropped. // nReconnectionAttempts is the number of reconnection attempts made before // a successful connection. Metric nMessagesRead; Metric nMessagesDropped; Metric nReconnectionAttempts; // when in consistent region, this parameter is used to indicate max time the receive method should block public static final long RECEIVE_TIMEOUT = 500l; // initialize the metrices. @CustomMetric(kind = Metric.Kind.COUNTER) public void setnMessagesRead(Metric nMessagesRead) { this.nMessagesRead = nMessagesRead; } @CustomMetric(kind = Metric.Kind.COUNTER) public void setnMessagesDropped(Metric nMessagesDropped) { this.nMessagesDropped = nMessagesDropped; } @CustomMetric(kind = Metric.Kind.COUNTER) public void setnReconnectionAttempts(Metric nReconnectionAttempts) { this.nReconnectionAttempts = nReconnectionAttempts; } // operator parameters // This optional parameter codepage speciifes the code page of the target // system using which ustring conversions have to be done for a BytesMessage // type. // If present, it must have exactly one value that is a String constant. If // the parameter is absent, the operator will use the default value of to // UTF-8 private String codepage = "UTF-8"; //$NON-NLS-1$ // This mandatory parameter access specifies access specification name. private String access; // This mandatory parameter connection specifies name of the connection // specification containing a JMS element private String connection; // This optional parameter connectionDocument specifies the pathname of a // file containing the connection information. // If present, it must have exactly one value that is a String constant. // If the parameter is absent, the operator will use the default location // filepath etc/connections.xml (with respect to the application directory) private String connectionDocument = null; // This optional parameter reconnectionBound specifies the number of // successive connections that // will be attempted for this operator. // It is an optional parameter of type uint32. // It can appear only when the reconnectionPolicy parameter is set to // BoundedRetry and cannot appear otherwise.If not present the default value // is taken to be 5. private int reconnectionBound = 5; // This optional parameter reconnectionPolicy specifies the reconnection // policy that would be applicable during initial/intermittent connection // failures. // The valid values for this parameter are NoRetry, BoundedRetry and // InfiniteRetry. // If not specified, it is set to BoundedRetry with a reconnectionBound of 5 // and a period of 60 seconds. private ReconnectionPolicies reconnectionPolicy = ReconnectionPolicies .valueOf("BoundedRetry"); //$NON-NLS-1$ // This optional parameter period specifies the time period in seconds which // the // operator will wait before trying to reconnect. // It is an optional parameter of type float64. // If not specified, the default value is 60.0. It must appear only when the // reconnectionPolicy parameter is specified private double period = 60.0; // Declaring the JMSMEssagehandler, private JMSMessageHandlerImpl messageHandlerImpl; // Specify after how many messages are received, the operator should establish consistent region private int triggerCount = 0; // instance of JMSSourceCRState to hold variables required for consistent region private JMSSourceCRState crState = null; private String messageSelector = null; private boolean initalConnectionEstablished = false; private String messageIDOutAttrName = null; private Object resetLock = new Object(); // application configuration name private String appConfigName; // user property name stored in application configuration private String userPropName; // password property name stored in application configuration private String passwordPropName; private String keyStore; private String trustStore; private String keyStorePassword; private String trustStorePassword; private boolean sslConnection; public boolean isSslConnection() { return sslConnection; } @Parameter(optional = true) public void setSslConnection(boolean sslConnection) { this.sslConnection = sslConnection; } public String getTrustStorePassword() { return trustStorePassword; } @Parameter(optional = true) public void setTrustStorePassword(String trustStorePassword) { this.trustStorePassword = trustStorePassword; } public String getTrustStore() { return trustStore; } @Parameter(optional = true) public void setTrustStore(String trustStore) { this.trustStore = trustStore; } public String getKeyStorePassword() { return keyStorePassword; } @Parameter(optional = true) public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; } public String getKeyStore() { return keyStore; } @Parameter(optional = true) public void setKeyStore(String keyStore) { this.keyStore = keyStore; } public String getAppConfigName() { return appConfigName; } @Parameter(optional = true) public void setAppConfigName(String appConfigName) { this.appConfigName = appConfigName; } public String getUserPropName() { return userPropName; } @Parameter(optional = true) public void setUserPropName(String userPropName) { this.userPropName = userPropName; } public String getPasswordPropName() { return passwordPropName; } @Parameter(optional = true) public void setPasswordPropName(String passwordPropName) { this.passwordPropName = passwordPropName; } public String getMessageIDOutAttrName() { return messageIDOutAttrName; } @Parameter(optional = true) public void setMessageIDOutAttrName(String messageIDOutAttrName) { this.messageIDOutAttrName = messageIDOutAttrName; } public String getMessageSelector() { return messageSelector; } @Parameter(optional = true) public void setMessageSelector(String messageSelector) { this.messageSelector = messageSelector; } public int getTriggerCount() { return triggerCount; } @Parameter(optional = true) public void setTriggerCount(int triggerCount) { this.triggerCount = triggerCount; } // Mandatory parameter access @Parameter(optional = false) public void setAccess(String access) { this.access = access; } // Mandatory parameter connection @Parameter(optional = false) public void setConnection(String connection) { this.connection = connection; } // Optional parameter codepage @Parameter(optional = true) public void setCodepage(String codepage) { this.codepage = codepage; } // Optional parameter reconnectionPolicy @Parameter(optional = true) public void setReconnectionPolicy(String reconnectionPolicy) { this.reconnectionPolicy = ReconnectionPolicies .valueOf(reconnectionPolicy); } // Optional parameter reconnectionBound @Parameter(optional = true) public void setReconnectionBound(int reconnectionBound) { this.reconnectionBound = reconnectionBound; } // Optional parameter period @Parameter(optional = true) public void setPeriod(double period) { this.period = period; } // Optional parameter connectionDocument @Parameter(optional = true, description="Connection document containing connection information to connect to messaging servers. If not specified, the connection document is assumed to be application_dir/etc/connections.xml.") public void setConnectionDocument(String connectionDocument) { this.connectionDocument = connectionDocument; } // Class to hold various variables for consistent region private class JMSSourceCRState { // Last fully processed message private Message lastMsgSent; // Counters for counting number of received messages private int msgCounter; // Flag to indicate a checkpoint has been made. private boolean isCheckpointPerformed; private List<String> msgIDWIthSameTS; JMSSourceCRState() { lastMsgSent = null; msgCounter = 0; isCheckpointPerformed = false; msgIDWIthSameTS = new ArrayList<String>(); } public List<String> getMsgIDWIthSameTS() { return msgIDWIthSameTS; } public boolean isCheckpointPerformed() { return isCheckpointPerformed; } public void setCheckpointPerformed(boolean isCheckpointPerformed) { this.isCheckpointPerformed = isCheckpointPerformed; } public void increaseMsgCounterByOne() { this.msgCounter++; } public Message getLastMsgSent() { return lastMsgSent; } public void setLastMsgSent(Message lastMsgSent) { try { if(this.lastMsgSent != null && this.lastMsgSent.getJMSTimestamp() != lastMsgSent.getJMSTimestamp()) { this.msgIDWIthSameTS.clear(); } this.msgIDWIthSameTS.add(lastMsgSent.getJMSMessageID()); } catch (JMSException e) { } this.lastMsgSent = lastMsgSent; } public int getMsgCounter() { return msgCounter; } public void acknowledgeMsg() throws JMSException { if(lastMsgSent != null) { lastMsgSent.acknowledge(); } } public void reset() { lastMsgSent = null; msgCounter = 0; isCheckpointPerformed = false; msgIDWIthSameTS = new ArrayList<String>(); } } public String getConnectionDocument() { if (connectionDocument == null) { connectionDocument = getOperatorContext().getPE().getApplicationDirectory() + "/etc/connections.xml"; //$NON-NLS-1$ } // if relative path, convert to absolute path if (!connectionDocument.startsWith("/")) //$NON-NLS-1$ { connectionDocument = getOperatorContext().getPE().getApplicationDirectory() + File.separator + connectionDocument; } return connectionDocument; } // Add the context checks @ContextCheck(compile = true) public static void checkInConsistentRegion(OperatorContextChecker checker) { ConsistentRegionContext consistentRegionContext = checker.getOperatorContext().getOptionalContext(ConsistentRegionContext.class); OperatorContext context = checker.getOperatorContext(); if(consistentRegionContext != null && consistentRegionContext.isTriggerOperator() && !context.getParameterNames().contains("triggerCount")) { //$NON-NLS-1$ checker.setInvalidContext(Messages.getString("TRIGGERCOUNT_PARAM_MUST_BE_SET_WHEN_CONSISTENT_REGION_IS_OPERATOR_DRIVEN"), new String[] {}); //$NON-NLS-1$ } } /* * The method checkErrorOutputPort validates that the stream on error output * port contains the mandatory attribute of type rstring which will contain * the error message. */ @ContextCheck public static void checkErrorOutputPort(OperatorContextChecker checker) { OperatorContext context = checker.getOperatorContext(); // Check if the error port is defined or not if (context.getNumberOfStreamingOutputs() == 2) { StreamingOutput<OutputTuple> streamingOutputErrorPort = context .getStreamingOutputs().get(1); // The optional error output port can have only one attribute of // type rstring if (streamingOutputErrorPort.getStreamSchema().getAttributeCount() != 1) { logger.log(LogLevel.ERROR, "ERROR_PORT_ATTR_COUNT_MAX"); //$NON-NLS-1$ } if (streamingOutputErrorPort.getStreamSchema().getAttribute(0) .getType().getMetaType() != Type.MetaType.RSTRING) { logger.log(LogLevel.ERROR, "ERROR_PORT_ATTR_TYPE"); //$NON-NLS-1$ } } } /* * The method checkParametersRuntime validates that the reconnection policy * parameters are appropriate */ @ContextCheck(compile = false) public static void checkParametersRuntime(OperatorContextChecker checker) { OperatorContext context = checker.getOperatorContext(); if ((context.getParameterNames().contains("reconnectionBound"))) { // reconnectionBound //$NON-NLS-1$ // reconnectionBound value should be non negative. if (Integer.parseInt(context .getParameterValues("reconnectionBound").get(0)) < 0) { //$NON-NLS-1$ logger.log(LogLevel.ERROR, "REC_BOUND_NEG"); //$NON-NLS-1$ checker.setInvalidContext( Messages.getString("PARAM_VALUE_SHOULD_BE_GREATER_OR_EQUAL_TO_ZERO"), //$NON-NLS-1$ new String[] { "reconnectionBound", context.getParameterValues("reconnectionBound").get(0) }); //$NON-NLS-1$ } if (context.getParameterNames().contains("reconnectionPolicy")) { //$NON-NLS-1$ // reconnectionPolicy can be either InfiniteRetry, NoRetry, // BoundedRetry ReconnectionPolicies reconPolicy = ReconnectionPolicies .valueOf(context .getParameterValues("reconnectionPolicy") //$NON-NLS-1$ .get(0).trim()); // reconnectionBound can appear only when the reconnectionPolicy // parameter is set to BoundedRetry and cannot appear otherwise if (reconPolicy != ReconnectionPolicies.BoundedRetry) { logger.log(LogLevel.ERROR, "REC_BOUND_NOT_ALLOWED"); //$NON-NLS-1$ checker.setInvalidContext( Messages.getString("RECONNECTIONBOUND_CAN_APPEAR_ONLY_WHEN_RECONNECTIONPOLICY_IS_BOUNDEDRETRY"), //$NON-NLS-1$ new String[] { context.getParameterValues("reconnectionBound").get(0) }); //$NON-NLS-1$ } } } // If initDelay parameter os specified then its value should be non // negative. if (context.getParameterNames().contains("initDelay")) { //$NON-NLS-1$ if (Integer.valueOf(context.getParameterValues("initDelay").get(0)) < 0) { //$NON-NLS-1$ logger.log(LogLevel.ERROR, "INIT_DELAY_NEG"); //$NON-NLS-1$ checker.setInvalidContext( Messages.getString("PARAM_VALUE_SHOULD_BE_GREATER_OR_EQUAL_TO_ZERO"), //$NON-NLS-1$ new String[] { "initDelay", context.getParameterValues("initDelay").get(0).trim() }); //$NON-NLS-1$ } } if(context.getParameterNames().contains("triggerCount")) { //$NON-NLS-1$ if(Integer.valueOf(context.getParameterValues("triggerCount").get(0)) < 1) { //$NON-NLS-1$ logger.log(LogLevel.ERROR, "TRIGGERCOUNT_VALUE_SHOULD_BE_GREATER_THAN_ZERO", new String[] { context.getParameterValues("triggerCount").get(0).trim() } ); //$NON-NLS-1$ //$NON-NLS-2$ checker.setInvalidContext( Messages.getString("TRIGGERCOUNT_VALUE_SHOULD_BE_GREATER_THAN_ZERO"), //$NON-NLS-1$ new String[] { context.getParameterValues("triggerCount").get(0).trim() }); //$NON-NLS-1$ } } if (checker.getOperatorContext().getParameterNames().contains("messageIDOutAttrName")) { //$NON-NLS-1$ List<String> parameterValues = checker.getOperatorContext().getParameterValues("messageIDOutAttrName"); //$NON-NLS-1$ String outAttributeName = parameterValues.get(0); List<StreamingOutput<OutputTuple>> outputPorts = checker.getOperatorContext().getStreamingOutputs(); if (outputPorts.size() > 0) { StreamingOutput<OutputTuple> outputPort = outputPorts.get(0); StreamSchema streamSchema = outputPort.getStreamSchema(); boolean check = checker.checkRequiredAttributes(streamSchema, outAttributeName); if (check) checker.checkAttributeType(streamSchema.getAttribute(outAttributeName), MetaType.RSTRING); } } if((checker.getOperatorContext().getParameterNames().contains("appConfigName"))) { //$NON-NLS-1$ String appConfigName = checker.getOperatorContext().getParameterValues("appConfigName").get(0); //$NON-NLS-1$ String userPropName = checker.getOperatorContext().getParameterValues("userPropName").get(0); //$NON-NLS-1$ String passwordPropName = checker.getOperatorContext().getParameterValues("passwordPropName").get(0); //$NON-NLS-1$ PropertyProvider provider = new PropertyProvider(checker.getOperatorContext().getPE(), appConfigName); String userName = provider.getProperty(userPropName); String password = provider.getProperty(passwordPropName); if(userName == null || userName.trim().length() == 0) { logger.log(LogLevel.ERROR, "PROPERTY_NOT_FOUND_IN_APP_CONFIG", new String[] {userPropName, appConfigName}); //$NON-NLS-1$ checker.setInvalidContext( Messages.getString("PROPERTY_NOT_FOUND_IN_APP_CONFIG"), //$NON-NLS-1$ new Object[] {userPropName, appConfigName}); } if(password == null || password.trim().length() == 0) { logger.log(LogLevel.ERROR, "PROPERTY_NOT_FOUND_IN_APP_CONFIG", new String[] {passwordPropName, appConfigName}); //$NON-NLS-1$ checker.setInvalidContext( Messages.getString("PROPERTY_NOT_FOUND_IN_APP_CONFIG"), //$NON-NLS-1$ new Object[] {passwordPropName, appConfigName}); } } } // add check for reconnectionPolicy is present if either period or // reconnectionBound is specified @ContextCheck(compile = true) public static void checkParameters(OperatorContextChecker checker) { checker.checkDependentParameters("period", "reconnectionPolicy"); //$NON-NLS-1$ //$NON-NLS-2$ checker.checkDependentParameters("reconnectionBound", //$NON-NLS-1$ "reconnectionPolicy"); //$NON-NLS-1$ // Make sure if appConfigName is specified then both userPropName and passwordPropName are needed checker.checkDependentParameters("appConfigName", "userPropName", "passwordPropName"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ checker.checkDependentParameters("userPropName", "appConfigName", "passwordPropName"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ checker.checkDependentParameters("passwordPropName", "appConfigName", "userPropName"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } @Override public synchronized void initialize(OperatorContext context) throws ParserConfigurationException, InterruptedException, IOException, ParseConnectionDocumentException, SAXException, NamingException, ConnectionException, Exception { super.initialize(context); consistentRegionContext = context.getOptionalContext(ConsistentRegionContext.class); JmsClasspathUtil.setupClassPaths(context); // set SSL system properties if(isSslConnection()) { if(context.getParameterNames().contains("keyStore")) System.setProperty("javax.net.ssl.keyStore", getAbsolutePath(getKeyStore())); if(context.getParameterNames().contains("keyStorePassword")) System.setProperty("javax.net.ssl.keyStorePassword", getKeyStorePassword()); if(context.getParameterNames().contains("trustStore")) System.setProperty("javax.net.ssl.trustStore", getAbsolutePath(getTrustStore())); if(context.getParameterNames().contains("trustStorePassword")) System.setProperty("javax.net.ssl.trustStorePassword", getTrustStorePassword()); } if(consistentRegionContext != null) { crState = new JMSSourceCRState(); } // create connection document parser object (which is responsible for // parsing the connection document) ConnectionDocumentParser connectionDocumentParser = new ConnectionDocumentParser(); // check if the error output port is specified or not if (context.getNumberOfStreamingOutputs() == 2) { hasErrorPort = true; errorOutputPort = getOutput(1); } // set the data output port dataOutputPort = getOutput(0); StreamSchema streamSchema = getOutput(0).getStreamSchema(); // check if connections file is valid, if any of the below checks fail, // the operator throws a runtime error and abort connectionDocumentParser.parseAndValidateConnectionDocument( getConnectionDocument(), connection, access, streamSchema, false, context.getPE().getApplicationDirectory()); // codepage parameter can come only if message class is bytes if (connectionDocumentParser.getMessageType() != MessageClass.bytes && context.getParameterNames().contains("codepage")) { //$NON-NLS-1$ throw new ParseConnectionDocumentException( Messages.getString("CODEPAGE_APPEARS_ONLY_WHEN_MSG_CLASS_IS_BYTES")); //$NON-NLS-1$ } // populate the message type and isAMQ messageType = connectionDocumentParser.getMessageType(); isAMQ = connectionDocumentParser.isAMQ(); // parsing connection document is successful, we can go ahead and create // connection // isProducer, false implies Consumer PropertyProvider propertyProvider = null; if(getAppConfigName() != null) { propertyProvider = new PropertyProvider(context.getPE(), getAppConfigName()); } jmsConnectionHelper = new JMSConnectionHelper(connectionDocumentParser, reconnectionPolicy, reconnectionBound, period, false, 0, 0, nReconnectionAttempts, logger, (consistentRegionContext != null), messageSelector, propertyProvider, userPropName , passwordPropName, null); // Create the appropriate JMS message handlers as specified by the // messageType. switch (connectionDocumentParser.getMessageType()) { case map: messageHandlerImpl = new MapMessageHandler( connectionDocumentParser.getNativeSchemaObjects()); break; case stream: messageHandlerImpl = new StreamMessageHandler( connectionDocumentParser.getNativeSchemaObjects()); break; case bytes: messageHandlerImpl = new BytesMessageHandler( connectionDocumentParser.getNativeSchemaObjects(), codepage); break; case empty: messageHandlerImpl = new EmptyMessageHandler( connectionDocumentParser.getNativeSchemaObjects()); break; case text: messageHandlerImpl = new TextMessageHandler(connectionDocumentParser.getNativeSchemaObjects()); break; default: throw new RuntimeException(Messages.getString("NO_VALID_MSG_CLASS_SPECIFIED")); //$NON-NLS-1$ } // register for data governance registerForDataGovernance(connectionDocumentParser.getProviderURL(), connectionDocumentParser.getDestination()); } protected String getAbsolutePath(String filePath) { if(filePath == null) return null; Path p = Paths.get(filePath); if(p.isAbsolute()) { return filePath; } else { File f = new File (getOperatorContext().getPE().getApplicationDirectory(), filePath); return f.getAbsolutePath(); } } private void registerForDataGovernance(String providerURL, String destination) { logger.log(TraceLevel.INFO, "JMSSource - Registering for data governance with providerURL: " + providerURL //$NON-NLS-1$ + " destination: " + destination); //$NON-NLS-1$ DataGovernanceUtil.registerForDataGovernance(this, destination, IGovernanceConstants.ASSET_JMS_MESSAGE_TYPE, providerURL, IGovernanceConstants.ASSET_JMS_SERVER_TYPE, true, "JMSSource"); //$NON-NLS-1$ } @Override protected void process() throws UnsupportedEncodingException, InterruptedException, ConnectionException, Exception { boolean isInConsistentRegion = consistentRegionContext != null; boolean isTriggerOperator = isInConsistentRegion && consistentRegionContext.isTriggerOperator(); int msgIDAttrIndex = -1; if(this.getMessageIDOutAttrName() != null) { StreamSchema streamSchema = getOutput(0).getStreamSchema(); msgIDAttrIndex = streamSchema.getAttributeIndex(this.getMessageIDOutAttrName()); } // create the initial connection. try { jmsConnectionHelper.createInitialConnection(); if(isInConsistentRegion) { notifyResetLock(true); } } catch (Exception e1) { if(isInConsistentRegion) { notifyResetLock(false); } // Initial connection fails to be created. // throw the exception. throw e1; } long timeout = isInConsistentRegion ? JMSSource.RECEIVE_TIMEOUT : 0; long sessionCreationTime = 0; while (!Thread.interrupted()) { // read a message from the consumer try { if(isInConsistentRegion) { consistentRegionContext.acquirePermit(); // A checkpoint has been made, thus acknowledging the last sent message if(crState.isCheckpointPerformed()) { try { crState.acknowledgeMsg(); } catch (Exception e) { consistentRegionContext.reset(); } finally { crState.reset(); } } } Message m = jmsConnectionHelper.receiveMessage(timeout); if(m == null) { continue; } if(isInConsistentRegion) { // following section takes care of possible duplicate messages // i.e connection re-created due to failure causing unacknowledged message to be delivered again // we don't want to process duplicate messages again. if(crState.getLastMsgSent() == null) { sessionCreationTime = jmsConnectionHelper.getSessionCreationTime(); } else { // if session has been re-created and message is duplicate,ignore if(jmsConnectionHelper.getSessionCreationTime() > sessionCreationTime && isDuplicateMsg(m, crState.getLastMsgSent().getJMSTimestamp(), crState.getMsgIDWIthSameTS())) { logger.log(LogLevel.INFO, "IGNORED_DUPLICATED_MSG", m.getJMSMessageID()); //$NON-NLS-1$ continue; } } } // nMessagesRead indicates the number of messages which we have // read from the JMS Provider successfully nMessagesRead.incrementValue(1); OutputTuple dataTuple = dataOutputPort.newTuple(); // convert the message to the output Tuple using the appropriate // message handler MessageAction returnVal = messageHandlerImpl .convertMessageToTuple(m, dataTuple); // take an action based on the return type switch (returnVal) { // the message type is incorrect case DISCARD_MESSAGE_WRONG_TYPE: nMessagesDropped.incrementValue(1); logger.log(LogLevel.WARN, "DISCARD_WRONG_MESSAGE_TYPE", //$NON-NLS-1$ new Object[] { messageType }); // If the error output port is defined, redirect the error // to error output port if (hasErrorPort) { sendOutputErrorMsg(Messages.getString("DISCARD_WRONG_MESSAGE_TYPE", new Object[] { messageType })); //$NON-NLS-1$ } break; // if unexpected end of message has been reached case DISCARD_MESSAGE_EOF_REACHED: nMessagesDropped.incrementValue(1); logger.log(LogLevel.WARN, "DISCARD_MSG_TOO_SHORT"); //$NON-NLS-1$ // If the error output port is defined, redirect the error // to error output port if (hasErrorPort) { sendOutputErrorMsg(Messages.getString("DISCARD_MSG_TOO_SHORT")); //$NON-NLS-1$ } break; // Mesage is read-only case DISCARD_MESSAGE_UNREADABLE: nMessagesDropped.incrementValue(1); logger.log(LogLevel.WARN, "DISCARD_MSG_UNREADABLE"); //$NON-NLS-1$ // If the error output port is defined, redirect the error // to error output port if (hasErrorPort) { sendOutputErrorMsg(Messages.getString("DISCARD_MSG_UNREADABLE")); //$NON-NLS-1$ } break; case DISCARD_MESSAGE_MESSAGE_FORMAT_ERROR: nMessagesDropped.incrementValue(1); logger.log(LogLevel.WARN, "DISCARD_MESSAGE_MESSAGE_FORMAT_ERROR"); //$NON-NLS-1$ // If the error output port is defined, redirect the error // to error output port if (hasErrorPort) { sendOutputErrorMsg(Messages.getString("DISCARD_MESSAGE_MESSAGE_FORMAT_ERROR")); //$NON-NLS-1$ } break; // the message was read successfully case SUCCESSFUL_MESSAGE: if(msgIDAttrIndex != -1 && m.getJMSMessageID() != null) { dataTuple.setObject(msgIDAttrIndex, new RString(m.getJMSMessageID())); } dataOutputPort.submit(dataTuple); break; } // set last processed message if(isInConsistentRegion) { crState.setLastMsgSent(m); } // If the consistent region is driven by operator, then // 1. increase message counter // 2. Call make consistent region if message counter reached the triggerCounter specified by user if(isTriggerOperator) { crState.increaseMsgCounterByOne(); if(crState.getMsgCounter() == getTriggerCount()){ consistentRegionContext.makeConsistent(); } } } catch (Exception Ex) { } finally { if(consistentRegionContext != null) { consistentRegionContext.releasePermit(); } } } } // Send the error message on to the error output port if one is specified private void sendOutputErrorMsg(String errorMessage) throws Exception { OutputTuple errorTuple = errorOutputPort.newTuple(); String consolidatedErrorMessage = errorMessage; // set the error message errorTuple.setString(0, consolidatedErrorMessage); // submit the tuple. errorOutputPort.submit(errorTuple); } @Override public void shutdown() throws Exception { // close the connection. if (isAMQ) { super.shutdown(); jmsConnectionHelper.closeConnection(); } else { jmsConnectionHelper.closeConnection(); super.shutdown(); } } private boolean isInitialConnectionEstablished() throws InterruptedException { synchronized(resetLock) { if(initalConnectionEstablished) { return true; } resetLock.wait(); return initalConnectionEstablished; } } private void notifyResetLock(boolean result) { if(consistentRegionContext != null) { synchronized(resetLock) { initalConnectionEstablished = result; resetLock.notifyAll(); } } } @Override public void close() throws IOException { } @Override public void checkpoint(Checkpoint checkpoint) throws Exception { logger.log(LogLevel.INFO, "CHECKPOINT", checkpoint.getSequenceId()); //$NON-NLS-1$ crState.setCheckpointPerformed(true); ObjectOutputStream stream = checkpoint.getOutputStream(); stream.writeBoolean(crState.getLastMsgSent() != null); if(crState.getLastMsgSent() != null) { stream.writeLong(crState.getLastMsgSent().getJMSTimestamp()); stream.writeObject(crState.getMsgIDWIthSameTS()); } } @Override public void drain() throws Exception { logger.log(LogLevel.INFO, "DRAIN"); //$NON-NLS-1$ } @SuppressWarnings("unchecked") @Override public void reset(Checkpoint checkpoint) throws Exception { logger.log(LogLevel.INFO, "RESET_CHECKPOINT", checkpoint.getSequenceId()); //$NON-NLS-1$ if(!isInitialConnectionEstablished()) { throw new ConnectionException(Messages.getString("CONNECTION_TO_JMS_FAILED", new Object[]{})); //$NON-NLS-1$ } // Reset consistent region variables and recover JMS session to make re-delivery of // unacknowledged message jmsConnectionHelper.recoverSession(); ObjectInputStream stream = checkpoint.getInputStream(); boolean hasMsg = stream.readBoolean(); if(hasMsg) { long lastSentMsgTS = stream.readLong(); List<String> lastSentMsgIDs = (List<String>) stream.readObject(); deduplicateMsg(lastSentMsgTS, lastSentMsgIDs); } crState.reset(); } private boolean isDuplicateMsg(Message msg, long lastSentMsgTs, List<String> lastSentMsgIDs) throws JMSException { boolean res = false; if(msg.getJMSTimestamp() < lastSentMsgTs) { res = true; } else if(msg.getJMSTimestamp() == lastSentMsgTs) { if(lastSentMsgIDs.contains(msg.getJMSMessageID())) { res = true; } } return res; } private void deduplicateMsg(long lastSentMsgTs, List<String> lastSentMsgIDs) throws JMSException, ConnectionException, InterruptedException { logger.log(LogLevel.INFO, "DEDUPLICATE_MESSAGES"); //$NON-NLS-1$ boolean stop = false; while(!stop) { Message msg = jmsConnectionHelper.receiveMessage(JMSSource.RECEIVE_TIMEOUT); if(msg == null) { return; } if(isDuplicateMsg(msg, lastSentMsgTs, lastSentMsgIDs)) { msg.acknowledge(); logger.log(LogLevel.INFO, "IGNORED_DUPLICATED_MSG", msg.getJMSMessageID()); //$NON-NLS-1$ } else { jmsConnectionHelper.recoverSession(); stop = true; } } } @Override public void resetToInitialState() throws Exception { logger.log(LogLevel.INFO, "RESET_TO_INITIAL_STATE"); //$NON-NLS-1$ if(!isInitialConnectionEstablished()) { throw new ConnectionException(Messages.getString("CONNECTION_TO_JMS_FAILED", new Object[]{})); //$NON-NLS-1$ } jmsConnectionHelper.recoverSession(); crState.reset(); } @Override public void retireCheckpoint(long id) throws Exception { logger.log(LogLevel.INFO, "RETIRE_CHECKPOINT", id); //$NON-NLS-1$ } }