Tuesday, July 3, 2012

Spring scheduled tasks - cron expression format

I was doing some work with scheduled tasks using Spring 3.1, and was having a lot of trouble getting my cron expressions right.  Turns out they don't follow the same format as UNIX cron expressions, though I'm not sure if the Spring ones would work in UNIX.

Anyway, here's what I learned:

  • There are 6 fields: second, minute, hour, day of month, month, day(s) of week.
  • Asterisk (*) means match any.
  • */X means "every X" (see examples).
  • I don't think numeric days of the week worked for me.  Besides, "MON-FRI" is much easier to read.
Here are some example expressions:

  • "0 0 18 * * MON-FRI" means every weekday at 6:00 PM.
  • "0 0 */1 * * *" means every hour on the hour.
  • "0 0 */8 * * *" means every 8 hours on the hour.
  • "0 0 12 1 * *" means 12:00 PM on the first day of every month.

Using JavaMail to download and send a BIRT report, compatible with MS Exchange

I needed to solve a couple of problems:  1) My simple JavaMail code worked fine for standard SMTP servers like gmail, but not for MS Exchange.  2) I was already using the free BIRT web viewer to run reports.  Now I needed to also send scheduled, automated emails with PDF versions of the reports.

 

I created the class below which is set up as a Spring scheduled task.  The Spring bean config looks like this:

 

<bean id="DailyReportTask" class="com.example.web.reports.ReportSender">

<property name="recipients" value="someguy@example.com"/>

       <property name="reportPath" value="Daily Report.rptdesign"/>

       <property name="subject" value="Daily Report"/>

       <property name="body" value="Today's Daily Report is attached below."/>

       <property name="attachmentFileName" value="DailyReport.pdf"/>

       <property name="reportParameters">

              <map key-type="java.lang.String" value-type="java.lang.Object">

                     <entry key="Date">

                             <bean class="com.example.web.factory.AddToCurrentDateString">

                                    <property name="datePattern" value="M/d/yyyy"/>

                                    <property name="numberOfUnits" value="0"/>

                             </bean>

                     </entry>

              </map>

       </property>

</bean>

 

As you can see, all the settings that might change from one report to the next are specified in Spring config.  I can now set up as many different reports as I want.

 

The “AddToCurrentDateString” class is a simple class that does date arithmetic and spits out the result in its toString() method.  It lets me specify report parameters dynamically, like “today” or “today – 7 days”.

 

Below is the meat of the code.  The bit that allows it to work with MS Exchange (at least in my case, I know Exchange implementations vary) is the DomainAuthenticator bit which concatenates the Windows domain name with the username, and setting the JavaMail property mail.smtp.auth to true.

 

As far as attaching the report is concerned, there was no need to actually download the bytes of the report.  I just needed to construct the full URL that would generate a PDF from the BIRT web viewer, and MimeBodyPart.setDataHandler(URL) took care of the rest.

 

package com.example.web.reports;

 

import java.io.IOException;

import java.io.UnsupportedEncodingException;

import java.net.URL;

import java.net.URLEncoder;

import java.util.Map;

import java.util.Properties;

 

import javax.activation.DataHandler;

import javax.mail.Authenticator;

import javax.mail.BodyPart;

import javax.mail.MessagingException;

import javax.mail.Multipart;

import javax.mail.Part;

import javax.mail.PasswordAuthentication;

import javax.mail.Session;

import javax.mail.Transport;

import javax.mail.internet.InternetAddress;

import javax.mail.internet.MimeBodyPart;

import javax.mail.internet.MimeMessage;

import javax.mail.internet.MimeMessage.RecipientType;

import javax.mail.internet.MimeMultipart;

 

import org.apache.commons.lang.StringUtils;

import org.apache.log4j.Logger;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.mail.MailException;

 

import com.example.integration.ConfigService;

 

public class ReportSender {

      private static final Logger LOG = Logger.getLogger(ReportSender.class);

 

      private String reportPath;

     

      // comma-separated list of recipient email addresses

      private String recipients;

     

      private String subject = "Automated Report";

     

      private Object body = "";

     

      private String attachmentFileName = "Report.pdf";

     

      // map of BIRT report parameter names to their values - will be included

      // as parameters in the URL

      private Map<String, Object> reportParameters;

     

      @Autowired

      private ConfigService configService;

     

      public void sendReport() {

            try {

                  // load settings from config

                  String host = configService.getConfig("smtp.host");

                  String port = configService.getConfig("smtp.port", "25");

                 

                  // for MS Exchange, specify the name of the Windows domain

                  String domain = configService.getConfig("smtp.domain", "");

                 

                  String username = configService.getConfig("smtp.user");

                  String password = configService.getConfig("smtp.password");

 

                  // for MS Exchange, this should be true

                  String authenticate = configService.getConfig("smtp.authenticate", "true");

                  String from = configService.getConfig("smtp.fromAddress");

                 

                  // DomainAuthenticator will concatenate the domain and username,

                  // separated by "\" - see below

                  DomainAuthenticator auth = new DomainAuthenticator(domain, username, password);

 

                  // set up JavaMail properties

                  Properties properties = new Properties();

                  properties.setProperty("mail.smtp.submitter", auth.getPasswordAuthentication().getUserName());

                  properties.setProperty("mail.smtp.auth", authenticate);

                  properties.setProperty("mail.smtp.host", host);

                  properties.setProperty("mail.smtp.port", port);

 

                  Session session = Session.getInstance(properties, auth);

                 

                  // start constructing the message to send

                  MimeMessage message = new MimeMessage(session);

 

                  String[] recipientList = StringUtils.split(recipients, ',');

                  for(String toAddr : recipientList) {

                        message.addRecipient(RecipientType.TO, new InternetAddress(toAddr));

                  }

                 

                  message.addFrom(new InternetAddress[] { new InternetAddress(from) });

 

                  message.setSubject(subject);

 

                  // this will be a multipart message - plain text, plus a PDF attachment

                  Multipart multipart = new MimeMultipart();

 

                  BodyPart textPart = new MimeBodyPart();

                  textPart.setText(body != null ? body.toString() : "");

                  multipart.addBodyPart(textPart);

 

                  // build a URL that would result in a PDF file generated by the BIRT web viewer

                  String reportViewerUrl = configService.getConfig("reports.viewer.url");

                  StringBuilder fullUrl = new StringBuilder(reportViewerUrl)

                        .append("/preview?__format=pdf&__report=")

                        .append(URLEncoder.encode(reportPath, "UTF-8"));

                 

                  // add report parameters, if any, to the URL

                  if(reportParameters != null && !reportParameters.isEmpty()) {

                        for(String key : reportParameters.keySet()) {

                              final Object val = reportParameters.get(key);

                              fullUrl.append('&').append(URLEncoder.encode(key, "UTF-8"))

                                    .append('=').append(URLEncoder.encode(val == null ? "" : val.toString(), "UTF-8"));

                        }

                  }

                 

                  if(LOG.isDebugEnabled()) LOG.debug("Downloading report URL: " + fullUrl);

 

                  // add the PDF attachment - no need to read it into memory, just open

                  // a stream using the URL

                  BodyPart attachmentPart = new MimeBodyPart();

                  attachmentPart.setDataHandler(new DataHandler(new URL(fullUrl.toString())));

                  attachmentPart.setFileName(attachmentFileName);

                  attachmentPart.setDisposition(Part.ATTACHMENT);

                  attachmentPart.setHeader("Content-Type", "application/pdf");

                  multipart.addBodyPart(attachmentPart);

 

                  message.setContent(multipart);

                   

                  Transport.send(message);

                 

            } catch (MailException e) {

                  LOG.error("MailException while sending report email. Report path: " + reportPath, e);

            } catch (MessagingException e) {

                  LOG.error("MessagingException while sending report email. Report path: " + reportPath, e);

            } catch (UnsupportedEncodingException e) {

                  LOG.error("UnsupportedEncodingException while sending report email. Report path: " + reportPath, e);

            } catch (IOException e) {

                  LOG.error("IOException while sending report email. Report path: " + reportPath, e);

            }

      }

 

 

      public void setReportPath(String reportPath) {

            this.reportPath = reportPath;

      }

 

      public void setRecipients(String recipients) {

            this.recipients = recipients;

      }

 

      public void setSubject(String subject) {

            this.subject = subject;

      }

 

      public void setBody(Object body) {

            this.body = body;

      }

 

      public void setAttachmentFileName(String attachmentFileName) {

            this.attachmentFileName = attachmentFileName;

      }

 

 

      public void setReportParameters(Map<String, Object> reportParameters) {

            this.reportParameters = reportParameters;

      }

     

      private static class DomainAuthenticator extends Authenticator {

           

            private String domain;

            private String username;

            private String password;

           

            public DomainAuthenticator(String domain, String username,

                        String password) {

                  super();

                  this.domain = domain;

                  this.username = username;

                  this.password = password;

            }

 

            public PasswordAuthentication getPasswordAuthentication() {

                  StringBuilder user = new StringBuilder();

                  if(!StringUtils.isBlank(this.domain)) user.append(this.domain).append('\\');

                  user.append(username);

                  return new PasswordAuthentication(user.toString(), this.password);

            }

      }

}