Skip to Content
Technical Articles

Mail notification of JMS queue content

Inside of my blog “Using JMS queue inside of an aBPM scenario” was shown how JMS queues can be used inside of an aBPM scenario. Surely you can use external tool like HermesJMS to look inside of queues and copy JMS message/s from an error queue to the non-error queue, but in some environments it is not allowed to connect your HermesJMS directly to the productive system. The access is restricted and only allowed from so-called hopping systems. The task to connect to those remote systems and opening the HermesJMS studio is very circumstantial and time intensive. For a better monitoring or a better operation maintenance it would be very helpfully if the PO system sends notification mails to a group of administrators to inform if the administrator has something to do. In case the administrator doesn’t get any notification the system and the JMS queues are fine.

In this blog I show an very easy approach for using the JMS queue browser API, application properties and a Job implementation to build and prepare notification mails that will be send to a specific group of users. Hint: The mail implementation for mail templates and sending via AEX and/or Java mail service is not part of this article.

First some prerequisites and helpfully links about Job implementation and using JMS:

Developing and Scheduling Jobs gives information about Job implementation.

The job defintion of this job contains two job import parameter for the recipient/s of the notification. The first parameter is the unique name, the second parameter is the type (user or group) of the recipient/s.

Adding Configuration Capabilities to an Application gives information to add Java system properties to an application

This is relevant to extract environment specific properties that can be configured by an administrator. In context of this article the JMS virtual provider/JMS queue name/s, etc. can be extracted into a single point of configuration outside of the source code.

Using Java Message Service gives much background information about JMS inside SAP AS Java.

The important fact of the JMS queue browser is that this class does not acknowledge the JMS message from the JMS queue. That means the original message will be read and not consumed from the queue like other message consumers does, e.g. a MDB (message driven bean). So with the JMS browser you get access to the queue for monitoring and investigation purposes.

Now lets look into some implementation details:

DC structure

Inside of package scheduler exist some classes that contains the Job implementation and a JobHelper implementation to scan the message inside of the queues.

Job implementation (JMSQueueCheckJob.java):

package <Vendor>.posystem.scheduler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.ejb.ActivationConfigProperty;
import javax.ejb.EJB;
import javax.ejb.MessageDriven;

import org.apache.commons.lang3.StringUtils;

import <Vendor>.common.mail.service.SendEmailServiceInvokerLocal;
import <Vendor>.common.PrincipalType;
import <Vendor>.common.mailmanager.helper.MailManager;
import <Vendor>.common.utility.PropertyProviderLocal;
import <Vendor>.posystem.properties.ApplicationPropertyReaderLocal;
import <Vendor>.tasknotification.service.TaskNotificationServiceLocal;
import com.sap.scheduler.runtime.JobContext;
import com.sap.scheduler.runtime.mdb.MDBJobImplementation;

@MessageDriven(activationConfig = { @ActivationConfigProperty(propertyName = "messageSelector", propertyValue = "JobDefinition='JMSQueueCheckJob'"),
        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") })
public class JMSQueueCheckJob extends MDBJobImplementation {

    private static final long serialVersionUID = 1L;
    private static final String PARAM_DELIMITER = ",";
    private static final String APP_PROPERTY_JMS_VPQUEUES = "jms.vpqueues";
    private static final String APP_PROPERTY_ENTRY_DELIMITER = "/";

    @EJB
    private static PropertyProviderLocal commonPropertyProvider;
    @EJB
    private TaskNotificationServiceLocal taskNotificationService;
    @EJB
    private SendEmailServiceInvokerLocal emailService;
    @EJB
    private <Vendor>.common.mailmanager.properties.ApplicationPropertyReaderLocal mailApplicationPropertyReaderLocal;
    @EJB
    private ApplicationPropertyReaderLocal applicationPropertyReader;

    @Override
    public void onJob(JobContext ctx) throws Exception {

        // get and check the job input parameter
        String principalIDs = ctx.getJobParameter("MailPrincipalIDs").getStringValue();
        if (null == principalIDs || StringUtils.isEmpty(principalIDs)) {
            throw new IllegalArgumentException("Parameter 'MailPrincipalIDs' was '" + principalIDs + "' but must be given and not empty");
        }
        String principalTypes = ctx.getJobParameter("MailPrincipalTypes").getStringValue();
        if (null == principalTypes || StringUtils.isEmpty(principalTypes)) {
            throw new IllegalArgumentException("Parameter 'MailPrincipalTypes' was '" + principalTypes + "' but must be given and not empty");
        }

        String[] principalParts = principalIDs.split(PARAM_DELIMITER);
        String[] principalTypeParts = principalTypes.split(PARAM_DELIMITER);
        if (principalParts.length != principalTypeParts.length) {
            throw new IllegalArgumentException("Inconsistency of entries between 'MailPrincipalIDs' and 'MailPrincipalTypes', the size is not equal");
        }
        final Logger jobLogger = ctx.getLogger();
        // map the principal into a map
        Map<String, PrincipalType> principalMap = new HashMap<String, PrincipalType>();
        for (int i = 0; i < principalParts.length; i++) {
            try {
                principalMap.put(principalParts[i], PrincipalType.fromValue(principalTypeParts[i]));
            } catch (IllegalArgumentException e) {
                jobLogger.info("Principal entry " + i + " with MailPrincipalID " + principalParts[i] + " and MailPrincipalTypes " + principalTypeParts[i]
                        + " will be ignored regarding wrong PrincipalType (only 'user' or 'group' are allowed)");
            }
        }
        if (!principalMap.isEmpty()) {
            // 1. get virtual providers and queue name from the application
            // property, the data must not be maintained in every scheduled job
            HashMap<String, List<String>> jmsVPWithQueuesMap = new HashMap<String, List<String>>();
            Properties appProperties = applicationPropertyReader.getApplicationProperties();
            // get application property
            String jmsVPQueueProperty = appProperties.getProperty(APP_PROPERTY_JMS_VPQUEUES);
            // split the property into entries list
            String[] vpQueueEntries = jmsVPQueueProperty.split(PARAM_DELIMITER);
            // split every entry into parts, index 0 = virtual provider, index 1
            // = JMS Queue name
            for (String aVPQueueEntry : vpQueueEntries) {
                String[] vpQueueEntryParts = aVPQueueEntry.split(APP_PROPERTY_ENTRY_DELIMITER);
                // put VP into JMS VP Queue map
                List<String> vpQueueList = jmsVPWithQueuesMap.get(vpQueueEntryParts[0]);
                if (null == vpQueueList) {
                    // create new queue list and put it into JMS VP Queue map
                    // (key = vp name)
                    vpQueueList = new ArrayList<String>();
                    jmsVPWithQueuesMap.put(vpQueueEntryParts[0], vpQueueList);
                }
                vpQueueList.add(vpQueueEntryParts[1]);
            }
            // 2. get the messages overview and send notification if necessary
            if (!jmsVPWithQueuesMap.isEmpty()) {
                HashMap<String, Integer> resultMap = JMSQueueCheckJobHelper.getJMSQueueMessagesMap(jmsVPWithQueuesMap, commonPropertyProvider);
                if (!resultMap.isEmpty()) {
                    Integer sumMsgCounter = 0;
                    for (Integer aMsgCounter : resultMap.values()) {
                        sumMsgCounter += aMsgCounter;
                    }
                    if (0 < sumMsgCounter) {
                        // prepare param list
                        List<String> params = new ArrayList<String>();
                        // param 0 = number of affected JMS messages
                        params.add(sumMsgCounter.toString());
                        for (Entry<String, Integer> aResultEntry : resultMap.entrySet()) {
                            // all params with odd-numbered indexes (1,3,...)
                            // contains the JMS queue name
                            params.add(aResultEntry.getKey());
                            // all params with even numbered indexes (2,4,...)
                            // contains the numbers of messages inside the JMS
                            // queue
                            params.add(aResultEntry.getValue().toString());
                        }
                        // send mails via MailManager
                        MailManager mailManager = new MailManager(mailApplicationPropertyReaderLocal.getApplicationProperties(), taskNotificationService, emailService);
                        for (Entry<String, PrincipalType> aPrincipalMapEntry : principalMap.entrySet()) {
                            mailManager.prepareAndSendTaskMail(JMSQueueCheckJobHelper.buildMailDto(aPrincipalMapEntry.getKey(), aPrincipalMapEntry.getValue(), params));
                        }
                        jobLogger.log(Level.INFO, "{0} Notifications were sent", principalMap.size());
                    } else {
                        jobLogger.info("No notifications were sent regarding MsgCounter is 0");
                    }
                } else {
                    jobLogger.info("No notifications were sent regarding result map of queue and counters is empty");
                }
            } else {
                jobLogger.info("No notifications were sent regarding virtual provider with queue map is empty");
            }
        } else {
            jobLogger.info("No notifications were sent regarding no mail recipients");
        }
        jobLogger.info("BPMProcessesCheckJob executed!");
    }

}

This class contains the message driven bean for the Job implementation. The execution can be planned via NWA Java scheduler. The onJob() operation trigger the read of the application properties, triggers the JMSQueueCheckJobHelper.java to scan the queue content. In case minimum one message is inside one JMS queue a MailDto with parameters will be prepared. The prepared notification will be send via Mailmanager (an implementation class that persist or send mails – alternatively an implemenattion can be called that send mails via Javamail service).

Helper implementation (JMSQueueCheckJobHelper.java):

package <Vendor>.posystem.scheduler;

import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import <Vendor>.common.BusinessUnit;
import <Vendor>.common.PrincipalType;
import <Vendor>.common.mailmanager.helper.MailManagerDto;
import <Vendor>.common.mailmanager.properties.TemplateDefinitionsEnum;
import <Vendor>.common.utility.PropertyProviderLocal;
import <Vendor>.posystem.exception.PosystemException;
import com.sap.tc.logging.Severity;
import com.sap.tc.logging.SimpleLogger;

public final class JMSQueueCheckJobHelper {
    private static final com.sap.tc.logging.Location logger = com.sap.tc.logging.Location.getLocation(JMSQueueCheckJobHelper.class);

    private JMSQueueCheckJobHelper() {
        // standard constructor
    }

    static MailManagerDto buildMailDto(String principalID, PrincipalType principalType, List<String> params) {
        MailManagerDto mailDto = new MailManagerDto();
        mailDto.setActualOwnerID(null);
        mailDto.setAttachments(null);
        mailDto.setBusinessUnit(BusinessUnit.INVOICE_VERIFICATION);
        mailDto.setDiscount(false);
        mailDto.setDocumentType(null);
        mailDto.setDueDate(null);
        mailDto.setHTML(true);
        mailDto.setInclusiveSubstitutions(false);
        mailDto.setInvoiceID(null);
        mailDto.setNetAmount(null);
        mailDto.setOriginalPotentialOwnerID(null);
        mailDto.setOriginalPotentialOwnerType(null);
        mailDto.setParams(params);
        mailDto.setPrincipalID(principalID);
        mailDto.setPrincipalSubstitution(null);
        mailDto.setPrincipalType(principalType);
        mailDto.setReplytoAddress(null);
        Date sentDate = new Date();
        mailDto.setSentDate(sentDate);
        mailDto.setStageable(false);
        mailDto.setSupplierName(null);
        mailDto.setTaskID(null);
        mailDto.setTaskSubjectText(null);
        mailDto.setTaskURL(null);
        mailDto.setTemplateName(TemplateDefinitionsEnum.GLOBAL_BUSINESSADMIN_JMSERRORMESSAGES_MAIL.getName());
        return mailDto;
    }

    static HashMap<String, Integer> getJMSQueueMessagesMap(HashMap<String, List<String>> jmsVPWithQueuesMap, PropertyProviderLocal commonPropertyProvider) {
        HashMap<String, Integer> resultMap = new HashMap<String, Integer>();
        QueueConnection queueConnection = null;
        QueueSession queueSession = null;
        try {
            InitialContext context = new InitialContext();
            // JNDI lookup of the connection factory and jms queues
            for (Entry<String, List<String>> aVPWithQueueEntry : jmsVPWithQueuesMap.entrySet()) {
                try {
                    // get connection factory
                    QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory) context.lookup("jmsfactory/" + aVPWithQueueEntry.getKey() + "/QueueConnectionFactory");
                    queueConnection = queueConnectionFactory.createQueueConnection(commonPropertyProvider.readJMSPrincipal(), commonPropertyProvider.readJMSCredentials());
                    queueSession = queueConnection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
                    for (String aJMSQueue : aVPWithQueueEntry.getValue()) {
                        QueueBrowser queueBrowser = null;
                        try {
                            // get the queue
                            Queue investigationQueue = (Queue) context.lookup("jmsqueues/" + aVPWithQueueEntry.getKey() + "/" + aJMSQueue);
                            // queue connection must be explicitly started, only
                            // createQueueConnection() or createQueueSession()
                            // is not enough
                            queueConnection.start();
                            // create a queue browser
                            queueBrowser = queueSession.createBrowser(investigationQueue);
                            // get the messages
                            Enumeration<Message> msgEnumeration = queueBrowser.getEnumeration();

                            if (msgEnumeration.hasMoreElements()) {
                                resultMap.put(aVPWithQueueEntry.getKey() + "/" + aJMSQueue, Integer.valueOf(Collections.list(msgEnumeration).size()));
                            } else {
                                resultMap.put(aVPWithQueueEntry.getKey() + "/" + aJMSQueue, 0);
                            }
                        } catch (JMSException jmsExc) {
                            SimpleLogger.traceThrowable(Severity.ERROR, logger, "JMSException occurred by creating or using queue browser", jmsExc);
                            continue;
                        } catch (NamingException nameExc) {
                            SimpleLogger.traceThrowable(Severity.ERROR, logger, "NamingException occurred by lookup queue: jmsqueues/" + aVPWithQueueEntry.getKey() + "/" + aJMSQueue,
                                    nameExc);
                            continue;
                        } finally {
                            if (queueBrowser != null) {
                                JMSQueueHelper.closeQueueBrowser(queueBrowser, aVPWithQueueEntry, aJMSQueue);
                            }
                        }
                    }
                } catch (JMSException jmsExc) {
                    SimpleLogger.traceThrowable(Severity.ERROR, logger, "JMSException occurred by creating queue connection or create queue session", jmsExc);
                    continue;
                } catch (NamingException nameExc) {
                    SimpleLogger.traceThrowable(Severity.ERROR, logger, "NamingException occurred by lookup connection factory: jmsfactory/" + aVPWithQueueEntry.getKey()
                            + "/QueueConnectionFactory", nameExc);
                    continue;
                } finally {
                    if (queueConnection != null) {
                        JMSQueueHelper.closeQueueConnection(queueConnection, aVPWithQueueEntry);
                    }
                }
            }
        } catch (NamingException nameExc) {
            SimpleLogger.traceThrowable(Severity.ERROR, logger, "NamingException occurred by creating new InitialContext()", nameExc);
            throw new PosystemException(nameExc);
        } finally {
            if (queueConnection != null) {
                JMSQueueHelper.closeQueueConnection(queueConnection);

            }
        }
        return resultMap;
    }
}

This class creates access to the JMS queue via queue browser and checks if the JMS queue has elements. The result will be filled into a HashMap.

Job definition (job-definition.xml):

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<job-definitions>
	<job-definition
		description="Job to check messages inside JMS queues and send notifications to mail recipients"
		name="JMSQueueCheckJob" retention-period="14">
		<job-definition-parameter name="MailPrincipalIDs"
			data-type="String"
			description="The MailPrincipalIDs are the unique names (logon names) of the recipients who gets the notification mail, multiple ids are allowed, the delimeter is a comma without blank spaces (e.g. 'id1','id2')"
			direction="IN" />
		<job-definition-parameter name="MailPrincipalTypes"
			data-type="String"
			description="List of types for the corresponding principal ids. Allowed values: user, group. Multiple types are allowed, delimeter is a comma without blanks (e.g. 'type1','type2'), size must be equal"
			direction="IN" />
	</job-definition>
</job-definitions>

After building and deploying the DC the job must be scheduled via NWA Java scheduler. In case of sending a notification the recipient gets the following HTML email:

Be the first to leave a comment
You must be Logged on to comment or reply to a post.