package pl.net.bluesoft.rnd.pt.ext.bpmnotifications; import static pl.net.bluesoft.util.lang.Strings.hasText; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.activation.DataHandler; import javax.activation.URLDataSource; import javax.mail.Message; import javax.mail.Multipart; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import org.hibernate.Session; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import pl.net.bluesoft.rnd.processtool.ProcessToolContext; import pl.net.bluesoft.rnd.processtool.ProcessToolContextCallback; import pl.net.bluesoft.rnd.processtool.bpm.ProcessToolBpmSession; import pl.net.bluesoft.rnd.processtool.model.BpmTask; import pl.net.bluesoft.rnd.processtool.model.ProcessInstance; import pl.net.bluesoft.rnd.processtool.model.UserData; import pl.net.bluesoft.rnd.processtool.plugins.ProcessToolRegistry; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.facade.NotificationsFacade; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.model.BpmNotification; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.model.BpmNotificationConfig; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.model.BpmNotificationTemplate; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.service.BpmNotificationService; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.service.TemplateArgumentDescription; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.service.TemplateArgumentProvider; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.sessions.DatabaseMailSessionProvider; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.sessions.IMailSessionProvider; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.sessions.JndiMailSessionProvider; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.templates.MailTemplateProvider; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.util.NotificationHistory; import pl.net.bluesoft.rnd.pt.ext.bpmnotifications.service.NotificationHistoryEntry; import pl.net.bluesoft.rnd.util.i18n.I18NSource; import pl.net.bluesoft.util.lang.Strings; /** * E-mail notification engine. * * @author tlipski@bluesoft.net.pl * @author mpawlak@bluesoft.net.pl */ public class BpmNotificationEngine implements BpmNotificationService { private static final long CONFIG_DEFAULT_CACHE_REFRESH_INTERVAL = 5* 1000; private static final String SUBJECT_TEMPLATE_SUFFIX = "_subject"; private static final String PROVIDER_TYPE = "mail.settings.provider.type"; private static final String REFRESH_INTERVAL = "mail.settings.refresh.interval"; /** Mail body encoding */ private static final String MAIL_ENCODING = "UTF-8"; private static final Logger logger = Logger.getLogger(BpmNotificationEngine.class.getName()); private Collection<BpmNotificationConfig> configCache = new HashSet<BpmNotificationConfig>(); private long cacheUpdateTime; private long refrshInterval; private ProcessToolBpmSession bpmSession; /** Data provider for standard e-mail template */ private TemplateDataProvider templateDataProvider; private ProcessToolRegistry registry; /** Provider for mail main session and mail connection properties */ private IMailSessionProvider mailSessionProvider; /** Provider for email templates */ private MailTemplateProvider templateProvider; private NotificationHistory history = new NotificationHistory(1000); public BpmNotificationEngine(ProcessToolRegistry registry) { this.registry = registry; try {init();}catch (Exception e){} } /** Initialize all providers and configurations */ private void init() { if(ProcessToolContext.Util.getThreadProcessToolContext() != null) { initComponents(); } else { registry.withProcessToolContext(new ProcessToolContextCallback() { @Override public void withContext(ProcessToolContext ctx) { ProcessToolContext.Util.setThreadProcessToolContext(ctx); initComponents(); } }); } } private void initComponents() { /* Register simple providers */ templateProvider = new MailTemplateProvider(); templateDataProvider = new TemplateDataProvider(); readRefreshIntervalFromSettings(); registerMailSettingProvider(); /* Refresh config for providers */ templateProvider.refreshConfig(); mailSessionProvider.refreshConfig(); logger.info("[NOTIFICATIONS] Notifications engine initialized"); } /** The method check if there are any new notifications in database to be sent */ public void handleNotifications() { registry.withProcessToolContext(new ProcessToolContextCallback() { @Override public void withContext(ProcessToolContext ctx) { ProcessToolContext.Util.setThreadProcessToolContext(ctx); handleNotificationsWithContext(); } }); } /** The method check if there are any new notifications in database to be sent */ public void handleNotificationsWithContext() { logger.info("[NOTIFICATIONS JOB] Checking awaiting notifications... "); try { /* Get all notifications waiting to be sent */ Collection<BpmNotification> notificationsToSend = NotificationsFacade.getNotificationsToSend(); logger.info("[NOTIFICATIONS JOB] "+notificationsToSend.size()+" notifications waiting to be sent..."); for(BpmNotification notification: notificationsToSend) { try { sendNotification(notification); /* Notification was sent, so remove it from te queue */ NotificationsFacade.removeNotification(notification); } catch(Exception ex) { logger.log(Level.SEVERE, "[NOTIFICATIONS JOB] Problem during notification sending", ex); } } } /* Table is locked, end transation */ catch(Exception ex) { } } public void onProcessStateChange(BpmTask task, ProcessInstance pi, UserData userData, boolean processStarted, boolean processEnded, boolean enteringStep) { refreshConfigIfNecessary(); ProcessToolContext ctx = ProcessToolContext.Util.getThreadProcessToolContext(); // logger.log(Level.INFO, "BpmNotificationEngine processes " + configCache.size() + " rules"); for (BpmNotificationConfig cfg : configCache) { try { if(!((enteringStep & cfg.isOnEnteringStep())||(processStarted & cfg.isNotifyOnProcessStart())||(processEnded & cfg.isNotifyOnProcessEnd()))) { // logger.info("Not matched notification #" + cfg.getId() + ": enteringStep=" + enteringStep ); continue; } if (cfg.isNotifyOnProcessEnd() && (task != null && task.getProcessInstance().getParent() != null)) { continue; } if (hasText(cfg.getProcessTypeRegex()) && !pi.getDefinitionName().toLowerCase().matches(cfg.getProcessTypeRegex().toLowerCase())) { // logger.info("Not matched notification #" + cfg.getId() + ":pra pi.getDefinitionName()=" + pi.getDefinitionName() ); continue; } if (!( (!hasText(cfg.getStateRegex()) || (task != null && task.getTaskName().toLowerCase().matches(cfg.getStateRegex().toLowerCase()))) )) { // logger.info("Not matched notification #" + cfg.getId() + ": task.getTaskName()=" + task.getTaskName() ); continue; } if (hasText(cfg.getLastActionRegex())) { String lastAction = pi.getSimpleAttributeValue("ACTION"); if (lastAction == null || !lastAction.toLowerCase().matches(cfg.getLastActionRegex().toLowerCase())) { // logger.info("Not matched notification #" + cfg.getId() + ": lastAction=" + lastAction ); continue; } } logger.info("Matched notification #" + cfg.getId() + " for process state change #" + pi.getInternalId()); List<String> emailsToNotify = new LinkedList<String>(); if (task != null && cfg.isNotifyTaskAssignee()) { UserData owner = task.getOwner(); if (cfg.isSkipNotificationWhenTriggeredByAssignee() && owner != null && owner.getLogin() != null && owner.getLogin().equals(userData.getLogin())) { logger.info("Not notifying user " + owner.getLogin() + " - this user has initiated processed action"); continue; } if (owner != null && hasText(owner.getEmail())) { emailsToNotify.add(owner.getEmail()); logger.info("Notification will be sent to " + owner.getEmail()); } } if (hasText(cfg.getNotifyEmailAddresses())) { emailsToNotify.addAll(Arrays.asList(cfg.getNotifyEmailAddresses().split(","))); } if (hasText(cfg.getNotifyUserAttributes())) { emailsToNotify.addAll(extractUserEmails(cfg.getNotifyUserAttributes(), ctx, pi)); } if (emailsToNotify.isEmpty()) { logger.info("Despite matched rules, no emails qualify to notify for cfg #" + cfg.getId()); continue; } String templateName = cfg.getTemplateName(); String profileName = cfg.getProfileName(); BpmNotificationTemplate template = templateProvider.getBpmNotificationTemplate(templateName); Map<String, Object> data = templateDataProvider.prepareData(bpmSession, task, pi, userData, cfg, ctx); String body = processTemplate(templateName, data); String subject = processTemplate(templateName + SUBJECT_TEMPLATE_SUFFIX, data); /* Add all notification to queue */ for (String rcpt : new HashSet<String>(emailsToNotify)) { addNotificationToSend(profileName, template.getSender(), rcpt, subject, body, cfg.isSendHtml()); } } catch (Exception e) { logger.log(Level.SEVERE, e.getMessage(), e); } } } /** Register mail session provider. There is support for: * <li> Database configuration (mail.settings.provider.type = database) * <li> JNDI resource configuration (mail.settings.provider.type = jndi) * * If configuration in pt_settings is not set, default is database */ private void registerMailSettingProvider() { /* Look for configuration for mail provider. If none exists, default is database */ String providerName = ProcessToolContext.Util.getThreadProcessToolContext().getSetting(PROVIDER_TYPE); if(providerName == null) { logger.warning("Mail session provider type is not set, using default database provider"); mailSessionProvider = new DatabaseMailSessionProvider(); } else if(providerName.equals("database")) { logger.info("Mail session provider set to database"); mailSessionProvider = new DatabaseMailSessionProvider(); } else if(providerName.equals("jndi")) { logger.info("Mail session provider set to jndi resources"); mailSessionProvider = new JndiMailSessionProvider(); } else { logger.severe("Unknown provider ["+providerName+"]! Service will be stopped"); //throw new IllegalArgumentException("Unknown provider ["+providerName+"]! Service will be stopped"); } } /** Read config refresh rate */ private void readRefreshIntervalFromSettings() { String refreshIntervalString = ProcessToolContext.Util.getThreadProcessToolContext().getSetting(REFRESH_INTERVAL); if(refreshIntervalString == null) { refrshInterval = CONFIG_DEFAULT_CACHE_REFRESH_INTERVAL; } else { refrshInterval = Long.parseLong(refreshIntervalString); } } private Collection<String> extractUserEmails(String notifyUserAttributes, ProcessToolContext ctx, ProcessInstance pi) { pi = ctx.getProcessInstanceDAO().refresh(pi); Set<String> emails = new HashSet<String>(); for (String attribute : notifyUserAttributes.split(",")) { attribute = attribute.trim(); if(attribute.matches("#\\{.*\\}")){ String loginKey = attribute.replaceAll("#\\{(.*)\\}", "$1"); attribute = pi.getInheritedSimpleAttributeValue(loginKey); if(attribute != null && attribute.matches("#\\{.*\\}")) { continue; } } if (hasText(attribute)) { UserData user = ctx.getUserDataDAO().loadUserByLogin(attribute); emails.add(user.getEmail()); } } return emails; } @Override public void registerTemplateArgumentProvider(TemplateArgumentProvider provider) { templateDataProvider.registerTemplateArgumentProvider(provider); } @Override public void unregisterTemplateArgumentProvider(TemplateArgumentProvider provider) { templateDataProvider.unregisterTemplateArgumentProvider(provider); } @Override public Collection<TemplateArgumentProvider> getTemplateArgumentProviders() { return templateDataProvider.getTemplateArgumentProviders(); } @Override public List<TemplateArgumentDescription> getDefaultArgumentDescriptions(I18NSource i18NSource) { return templateDataProvider.getDefaultArgumentDescriptions(i18NSource); } @Override public List<NotificationHistoryEntry> getNotificationHistoryEntries() { return history.getRecentEntries(); } @Override public synchronized void invalidateCache() { cacheUpdateTime = 0; } @SuppressWarnings("unchecked") public synchronized void refreshConfigIfNecessary() { if (cacheUpdateTime + refrshInterval < System.currentTimeMillis()) { Session session = ProcessToolContext.Util.getThreadProcessToolContext().getHibernateSession(); configCache = session .createCriteria(BpmNotificationConfig.class) .add(Restrictions.eq("active", true)) .addOrder(Order.asc("id")) .list(); cacheUpdateTime = System.currentTimeMillis(); /* Update cache refresh rate 8 */ readRefreshIntervalFromSettings(); registerMailSettingProvider(); /* Refresh config for providers */ templateProvider.refreshConfig(); mailSessionProvider.refreshConfig(); bpmSession = ProcessToolContext.Util.getThreadProcessToolContext().getProcessToolSessionFactory().createAutoSession(); logger.info("Mail configuration updated. Interval is set to "+refrshInterval); } } /** Methods add notification to queue for notifications to be sent in the * next scheduler job run * */ public void addNotificationToSend(String profileName, String sender, String recipient, String subject, String body, boolean sendAsHtml, String ... attachments) throws Exception { Collection<String> attachmentsCollection = new ArrayList<String>(); for(String attachment: attachments) attachmentsCollection.add(attachment); addNotificationToSend(profileName, sender, recipient, subject, body, sendAsHtml, attachmentsCollection); } /** Methods add notification to queue for notifications to be sent in the * next scheduler job run * */ public void addNotificationToSend(String profileName, String sender, String recipient, String subject, String body, boolean sendAsHtml, Collection<String> attachments) throws Exception { if (!Strings.hasText(sender)) { UserData autoUser = ProcessToolContext.Util.getThreadProcessToolContext().getAutoUser(); sender = autoUser.getEmail(); } if (!Strings.hasText(recipient)) { throw new IllegalArgumentException("Cannot send email: Recipient is null!"); } BpmNotification notification = new BpmNotification(); notification.setSender(sender); notification.setRecipient(recipient); notification.setSubject(subject); notification.setBody(body); notification.setSendAsHtml(sendAsHtml); notification.setProfileName(profileName); StringBuilder attachmentsString = new StringBuilder(); int attachmentsSize = attachments.size(); for(String attachment: attachments) { attachmentsString.append(attachment); attachmentsSize--; if(attachmentsSize > 0) attachmentsString.append(","); } notification.setAttachments(attachmentsString.toString()); NotificationsFacade.addNotificationToBeSent(notification); history.notificationEnqueued(notification); } private void sendNotification(BpmNotification notification) throws Exception { javax.mail.Session mailSession = mailSessionProvider.getSession(notification.getProfileName()); /* Create javax mail message from notification bean */ Message message = createMessageFromNotification(notification, mailSession); try { /* If smtps is required, force diffrent transport properties */ if(isSmtpsRequired(mailSession)) { Properties emailPrtoperties = mailSession.getProperties(); String secureHost = emailPrtoperties.getProperty("mail.smtp.host"); String securePort = emailPrtoperties.getProperty("mail.smtp.port"); String userName = emailPrtoperties.getProperty("mail.smtp.user"); String userPassword = emailPrtoperties.getProperty("mail.smtp.password"); Transport transport = mailSession.getTransport("smtps"); transport.connect(secureHost, Integer.parseInt(securePort), userName, userPassword); transport.sendMessage(message, message.getAllRecipients()); transport.close(); } /* Default transport mechanism */ else { Transport.send(message); } history.notificationSent(notification); logger.info("Emails sent"); } catch (Exception e) { history.errorWhileSendingNotification(notification, e); logger.log(Level.SEVERE, e.getMessage(), e); } } public static Message createMessageFromNotification(BpmNotification notification, javax.mail.Session mailSession) throws Exception { Message message = new MimeMessage(mailSession); message.setFrom(new InternetAddress(notification.getSender())); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(notification.getRecipient())); message.setSubject(notification.getSubject()); message.setSentDate(new Date()); //body MimeBodyPart messagePart = new MimeBodyPart(); messagePart.setContent(notification.getBody(), (notification.getSendAsHtml() ? "text/html" : "text/plain") + "; charset=\""+MAIL_ENCODING+"\""); Multipart multipart = new MimeMultipart("alternative"); multipart.addBodyPart(messagePart); //zalaczniki int counter = 0; URL url; if(notification.getAttachments() != null && !notification.getAttachments().isEmpty()) { String[] attachments = notification.getAttachments().split(","); for (String u : attachments) { if (!Strings.hasText(u)) continue; url = new URL(u); MimeBodyPart attachmentPart = new MimeBodyPart(); URLDataSource urlDs = new URLDataSource(url); attachmentPart.setDataHandler(new DataHandler(urlDs)); attachmentPart.setFileName("file" + counter++); multipart.addBodyPart(attachmentPart); logger.info("Added attachment " + u); } } message.setContent(multipart); message.setSentDate(new Date()); return message; } /** Check if tranport protocol is set to smtps */ private boolean isSmtpsRequired(javax.mail.Session mailSession) { Properties emailPrtoperties = mailSession.getProperties(); String transportProtocol = emailPrtoperties.getProperty("mail.transport.protocol"); return "smtps".equals(transportProtocol); } @Override public String findTemplate(String templateName) { refreshConfigIfNecessary(); return templateProvider.findTemplate(templateName); } @Override public String processTemplate(String templateName, Map<String, Object> data) { refreshConfigIfNecessary(); return templateProvider.processTemplate(templateName,data); } }