Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
0 Kudos
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: