package com.mckessonaps.jspwiki.plugin.emailnotifier; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.text.DateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import javax.mail.Message; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import org.apache.log4j.Logger; import com.ecyrd.jspwiki.WikiEngine; import com.ecyrd.jspwiki.WikiPage; import com.ecyrd.jspwiki.plugin.PluginManager; import com.ecyrd.jspwiki.providers.ProviderException; import com.mckessonaps.jspwiki.plugin.util.ParamUtil; /** * This is the task that does all the work. (Scant documentation I know, but it's time to move on.) */ class NotifierTask { private static final Logger log = Logger.getLogger( NotifierTask.class ); /** * If desired the plugin can specify an offset number of minutes from the top of the hour. * This is very useful if you have multiple wiki's so you can stagger the load on the server. */ private static final String PARAM_MINUTE_OFFSET = "minuteOffset"; private static final int DEFAULT_MINUTE_OFFSET = 0; private int m_minuteOffset = DEFAULT_MINUTE_OFFSET; /** * If desired the plugin can specify how many hours between checking for notification. */ private static final String PARAM_HOUR_INTERVAL = "hourInterval"; private static final int DEFAULT_HOUR_INTERVAL = 1; private int m_hourInterval = DEFAULT_HOUR_INTERVAL; /** * The smtp server that we will use for sending emails. Required for correct operation unless * your servers name happens to match the default value. */ private static final String PARAM_MAIL_SERVER = "server"; private static final String DEFAULT_MAIL_SERVER = "mailServer"; private String m_mailServer; /** * Email address that the notification message will appear to be from. */ private static final String PARAM_MAIL_FROM = "from"; private static final String DEFAULT_MAIL_FROM = "wiki"; private String m_mailFrom = DEFAULT_MAIL_FROM; /** * Subject line to be used on the notification message. */ private static final String PARAM_MAIL_SUBJECT = "subject"; private static final String DEFAULT_MAIL_SUBJECT = "Notification of Wiki Changes"; private String m_mailSubject = DEFAULT_MAIL_SUBJECT; /** * Subject line to be used on the notification message. */ private static final String PARAM_SUBSCRIPTION_PAGE = "subscriptionPage"; private static final String DEFAULT_SUBSCRIPTION_PAGE = "NotificationList"; private String m_subscriptionPage = DEFAULT_SUBSCRIPTION_PAGE; /** * Name of the subscription page specificed by the plugin parameter. Must be found in the plugin's * body or else a default message body will be used. */ public static final String MARKER_SUBSCRIPTION_PAGE = "SUBSCRIPTION_PAGE"; /** * Location where the list of changed pages will be inserted. Must be found in the plugin's * body or else a default message body will be used. */ public static final String MARKER_CHANGED_PAGE_LIST = "CHANGED_PAGE_LIST"; /** * The name of the changed page, this marker can be used in the PARAM_CHANGED_PAGE_PATTERN as desired. */ public static final String MARKER_CHANGED_PAGE_NAME = "CHANGED_PAGE_NAME"; /** * Some 'info' about the changed page (author and version), this marker can be used in the PARAM_CHANGED_PAGE_PATTERN as desired. */ public static final String MARKER_CHANGED_PAGE_INFO = "CHANGED_PAGE_INFO"; /** * Subject line to be used on the notification message. */ private static final String PARAM_CHANGED_PAGE_PATTERN = "changedPagePattern"; private static final String DEFAULT_CHANGED_PAGE_PATTERN = "* "+ MARKER_CHANGED_PAGE_NAME +"\n"; private String m_changedPagePattern = DEFAULT_CHANGED_PAGE_PATTERN; /** * Default message body is ised if the markers are not present in the plugin's body. */ private static final String HELLO_TEXT = "The following pages have changed in the last 24 hours.\n"; private static final String PAGE_LIST_TEXT = "----\n"+ MARKER_CHANGED_PAGE_LIST + "\n----\n"; private static final String UNSUBSCRIBE_TEXT = "To add or remove your email address from this list, edit the "+MARKER_SUBSCRIPTION_PAGE+" page."; private static final String DEFAULT_MESSAGE_BODY = HELLO_TEXT + PAGE_LIST_TEXT + UNSUBSCRIBE_TEXT; private String m_messageBody = DEFAULT_MESSAGE_BODY; private final WikiEngine m_wikiEngine; private final String m_appName; private Date m_nextScheduledDate; private boolean m_executing = false; /** * Basic constructor, accepts the app name that we serve as notifier for, schedules this task to run. * @param appName */ NotifierTask(WikiEngine engine) { m_wikiEngine = engine; m_appName = engine.getApplicationName(); scheduleNextDate(); } /** * Returns message akin to "AppName next notification at DateTime". */ String getStatusMessage() { String subscribeMessage = "Subscribe at "+m_subscriptionPage+"."; return m_appName + " next notification at " + DateFormat.getDateTimeInstance().format( m_nextScheduledDate) + ". " + subscribeMessage; } /** * Expose the next scheduled date to callers. * Don't allow anyone to read the date unless monitor is acquired. */ synchronized Date getNextScheduledDate() { return m_nextScheduledDate; } /** * NOTES: synchronized for no good reason other than I do not feel like thinking it thru. * We're busy, period, the monitor is mine. */ synchronized void execute() { m_executing = true; try { sendNotifications(); scheduleNextDate(); } catch (Exception e) { log.warn( "Unexpected exception whie executing the notifier task, swallowing.", e); } finally { m_executing = false; } } /** * Update the Notifier task to use the current (possibly new) parameters if it's not activly * executing. If it is activly executing just get out. * @param params */ void updateParameters(Map paramMap) { //Do not distrub our member values if we are running with them, they can get updated next time. if (m_executing) return; synchronized(this) //Don't let the execute() method get kicking if we are changing these... { m_minuteOffset = ParamUtil.getInteger(log, paramMap, PARAM_MINUTE_OFFSET, DEFAULT_MINUTE_OFFSET); if (m_minuteOffset < 0) m_minuteOffset = 0; m_hourInterval = ParamUtil.getInteger(log, paramMap, PARAM_HOUR_INTERVAL, DEFAULT_HOUR_INTERVAL); if (m_hourInterval< 0) m_hourInterval = 0; m_mailFrom = ParamUtil.getString(log, paramMap, PARAM_MAIL_FROM, DEFAULT_MAIL_FROM); m_mailSubject = ParamUtil.getString(log, paramMap, PARAM_MAIL_SUBJECT, DEFAULT_MAIL_SUBJECT); m_mailServer = ParamUtil.getString(log, paramMap, PARAM_MAIL_SERVER, DEFAULT_MAIL_SERVER); m_subscriptionPage = ParamUtil.getString(log, paramMap, PARAM_SUBSCRIPTION_PAGE, DEFAULT_SUBSCRIPTION_PAGE); updatePagePatternParam(paramMap); updateMessageBodyParam(paramMap); //The minute offset might have changed, reschedule ourself... scheduleNextDate(); } } private void updateMessageBodyParam(Map paramMap) { m_messageBody = ParamUtil.getString(log, paramMap, PluginManager.PARAM_BODY, DEFAULT_MESSAGE_BODY ); boolean missingMarker = false; missingMarker |= (-1 == m_messageBody.indexOf( MARKER_SUBSCRIPTION_PAGE )); missingMarker |= (-1 == m_messageBody.indexOf( MARKER_CHANGED_PAGE_LIST )); if (missingMarker) { log.warn( "Parameter: " + PluginManager.PARAM_BODY+ " does not contain all required markers, using default." ); m_messageBody = DEFAULT_MESSAGE_BODY; } } private void updatePagePatternParam(Map paramMap) { m_changedPagePattern = ParamUtil.getString(log, paramMap, PARAM_CHANGED_PAGE_PATTERN, DEFAULT_CHANGED_PAGE_PATTERN ); if (-1 == m_changedPagePattern.indexOf( MARKER_CHANGED_PAGE_NAME )) { log.warn( "Parameter: " + PARAM_CHANGED_PAGE_PATTERN + " does not contain required marker: " + MARKER_CHANGED_PAGE_NAME + " using default." ); m_changedPagePattern = DEFAULT_CHANGED_PAGE_PATTERN; } } /** * Take the current time, get the next hour boundary add the offset minutes and use * that as the next schedule date. */ private void scheduleNextDate() { Calendar c = Calendar.getInstance(); c.set( Calendar.MILLISECOND, 0); c.set( Calendar.SECOND , 0); c.set( Calendar.MINUTE, 0); c.add( Calendar.HOUR_OF_DAY, m_hourInterval); c.add( Calendar.MINUTE, m_minuteOffset ); if (c.getTimeInMillis() < System.currentTimeMillis() ) c.add( Calendar.HOUR_OF_DAY, 1); m_nextScheduledDate = c.getTime(); log.debug( getStatusMessage() ); } /** * Compose the notification message and send it off to the subscribers. */ private void sendNotifications() { log.debug("sendNotifications"); //If not subscribers for this notification time, just leave don't do needless work... List subscriberList = readSubscribers(); if ( subscriberList.isEmpty() ) { log.debug( "No subscribers, nothing to do." ); return; } //If no changed pages in the past 24 hours, just leave don't do needless work... Map changedPageMap = yesterdaysChangedPageMap(); if (changedPageMap.isEmpty() ) { log.debug( "No changed pages in last 24 hours, nothing to do." ); return; } //Prepare the body once... String preparedBody = m_messageBody.replaceAll(MARKER_SUBSCRIPTION_PAGE, m_subscriptionPage); //Send each subscriber an email... for (Iterator subscriberIter = subscriberList.iterator(); subscriberIter.hasNext();) { Subscriber s = (Subscriber)subscriberIter.next(); //If the subscriber was not interested in any of the changed pages, just skip his email, no need to spam him... String subscribedPages = s.getListOfChangedSubscribedPages(changedPageMap, m_changedPagePattern); if (subscribedPages.equals( "" )) { log.debug( "No subscribed pages for " + s.getEmailAddress() + " have changed, skip sending email." ); continue; } String message = preparedBody.replaceAll( MARKER_CHANGED_PAGE_LIST, subscribedPages); sendEmail(message, s.getEmailAddress()); } } /** * Given an email text and an email address, sends that email to all of the people in mAddresses. */ public void sendEmail(String message, String address) { log.debug("sendEmail() to: " + address); try { // create some properties and get the default Session Properties props = new Properties(); props.put("mail.smtp.host", m_mailServer ); Session session = Session.getDefaultInstance(props, null); session.setDebug(false); // create a Message Message msg = new MimeMessage(session); msg.setFrom(new InternetAddress( m_mailFrom )); msg.setRecipient(Message.RecipientType.TO, new InternetAddress(address)); msg.setSubject( m_mailSubject ); msg.setSentDate(new Date()); if (message.startsWith( "")) msg.setContent( message, "text/html"); else msg.setContent( message, "text/plain"); Transport.send(msg); } catch (Exception e) { log.error( "Exception occured while trying to send notification to: " + address, e); } } /** * Return a Map of the WikiPages that changed in the immediatly preceeding * 24 hour period (yesterday). The key into the map is the WikiPage name, the * associated value is a list of the versions of the WikiPage that were * created in the 24 hour period. */ private Map yesterdaysChangedPageMap() { Map yesterdaysChangeMap = new HashMap(); Date cuttoffDate = twentyFourHoursBefore(); Iterator allChangesIter = m_wikiEngine.getRecentChanges().iterator() ; while (allChangesIter.hasNext()) { WikiPage page = (WikiPage)allChangesIter.next(); if ( page.getLastModified().after( cuttoffDate) ) yesterdaysChangeMap.put( page.getName(), yesterdaysVersions(page, cuttoffDate)); } log.debug("yesterdaysChangedPageMap() found: " + yesterdaysChangeMap.size() ); return yesterdaysChangeMap; } private List yesterdaysVersions(WikiPage latestPage, Date cuttoffDate) { List yesterdaysList = new ArrayList(); List versionList = null; try { versionList = m_wikiEngine.getPageManager().getVersionHistory( latestPage.getName() ); } catch (ProviderException e) { log.warn("yesterdaysVersions() got ProviderException: " + e.getMessage() ); versionList = new ArrayList(); versionList.add(latestPage);//provider coundn't give us versions? So just the last page. } //FIX: for NPE if (versionList != null) { for (Iterator iter = versionList.iterator(); iter.hasNext();) { WikiPage page = (WikiPage) iter.next(); if (page.getLastModified().after(cuttoffDate)) yesterdaysList.add( page ); } log.debug( "yesterdaysList.size():" + yesterdaysList.size() ); } return yesterdaysList; } /** * Return a Date that is precisely 24 hours before our scheduled notification date. */ private Date twentyFourHoursBefore() { Calendar c = Calendar.getInstance(); c.setTime( m_nextScheduledDate ); c.add( Calendar.HOUR, -24); return c.getTime(); } /** * Return a list of the Subscribers who are due deleviery of thier notifications. */ private List readSubscribers() { List l = new ArrayList(); try { String subscriptionText = subscriptionPageText(); BufferedReader br = new BufferedReader( new StringReader( subscriptionText )); String line = br.readLine(); while (null != line) { Subscriber s = Subscriber.parse(line); if (null != s) { if (s.shouldSendAt( m_nextScheduledDate )) l.add( s ); } line = br.readLine(); } } catch (IOException ioe) { log.warn( "Unexpected exception, swallowing it and continuing.", ioe ); } log.debug( "readSubscribers() found: " + l.size() ); return l; } /** * Return the (raw, untranslated) wiki text of the designated subscription page. */ String subscriptionPageText() { WikiPage p = m_wikiEngine.getPage( m_subscriptionPage ); if (null == p) { log.warn("Designated subscription page was not found: " + m_subscriptionPage); return ""; } try { return m_wikiEngine.getPageManager().getPageText( p.getName(), p.getVersion() ); } catch (ProviderException e) { log.warn( "Exception while trying to get the subscription page text, swallowing the exception, returning an empty string.", e); return ""; } } }