/** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a * copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jasig.schedassist.impl.reminder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import net.fortuna.ical4j.model.Parameter; import net.fortuna.ical4j.model.Property; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.parameter.PartStat; import net.fortuna.ical4j.model.property.Location; import net.fortuna.ical4j.model.property.Summary; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.schedassist.ICalendarAccountDao; import org.jasig.schedassist.SchedulingAssistantService; import org.jasig.schedassist.impl.owner.OwnerDao; import org.jasig.schedassist.model.AvailableBlock; import org.jasig.schedassist.model.ICalendarAccount; import org.jasig.schedassist.model.IEventUtils; import org.jasig.schedassist.model.IScheduleOwner; import org.jasig.schedassist.model.Reminders; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.mail.MailSendException; import org.springframework.mail.MailSender; import org.springframework.mail.SimpleMailMessage; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; /** * Default {@link ReminderService} implementation. * * @author Nicholas Blair * @version $Id: DefaultReminderServiceImpl.java 3070 2011-02-09 13:53:34Z npblair $ */ @Service public class DefaultReminderServiceImpl implements ReminderService, Runnable { private static final String NEWLINE = System.getProperty("line.separator"); private ReminderDao reminderDao; private MailSender mailSender; private IEventUtils eventUtils; private OwnerDao ownerDao; private SchedulingAssistantService schedulingAssistantService; private ICalendarAccountDao calendarAccountDao; private MessageSource messageSource; private String noReplyFromAddress; private EmailAddressValidator emailAddressValidator = new DefaultEmailAddressValidatorImpl(); private final Log LOG = LogFactory.getLog(this.getClass()); /** * @param reminderDao the reminderDao to set */ @Autowired public void setReminderDao(ReminderDao reminderDao) { this.reminderDao = reminderDao; } /** * @param mailSender the mailSender to set */ @Autowired public void setMailSender(MailSender mailSender) { this.mailSender = mailSender; } /** * @param eventUtils the eventUtils to set */ @Autowired public void setEventUtils(IEventUtils eventUtils) { this.eventUtils = eventUtils; } /** * @param ownerDao the ownerDao to set */ @Autowired public void setOwnerDao(OwnerDao ownerDao) { this.ownerDao = ownerDao; } /** * @param schedulingAssistantService the schedulingAssistantService to set */ @Autowired public void setSchedulingAssistantService( SchedulingAssistantService schedulingAssistantService) { this.schedulingAssistantService = schedulingAssistantService; } /** * @param calendarAccountDao the calendarAccountDao to set */ @Autowired public void setCalendarAccountDao(@Qualifier("composite") ICalendarAccountDao calendarAccountDao) { this.calendarAccountDao = calendarAccountDao; } /** * @param messageSource the messageSource to set */ @Autowired public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } /** * @param emailAddressValidator the emailAddressValidator to set */ @Autowired(required=false) public void setEmailAddressValidator(EmailAddressValidator emailAddressValidator) { this.emailAddressValidator = emailAddressValidator; } /** * @param noReplyFromAddress the noReplyFromAddress to set */ @Value("${reminder.noReplyFromAddress}") public void setNoReplyFromAddress(String noReplyFromAddress) { this.noReplyFromAddress = noReplyFromAddress; } /* * (non-Javadoc) * @see org.jasig.schedassist.impl.reminder.ReminderService#createEventReminder(org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.ICalendarAccount, org.jasig.schedassist.model.AvailableBlock, net.fortuna.ical4j.model.component.VEvent, java.util.Date) */ @Override public IReminder createEventReminder(IScheduleOwner owner, ICalendarAccount recipient, AvailableBlock appointmentBlock, VEvent event, Date sendTime) { return reminderDao.createEventReminder(owner, recipient, appointmentBlock, event, sendTime); } /* * (non-Javadoc) * @see org.jasig.schedassist.impl.reminder.ReminderService#deleteEventReminder(org.jasig.schedassist.impl.reminder.IReminder) */ @Override public void deleteEventReminder(IReminder reminder) { reminderDao.deleteEventReminder(reminder); } /* * (non-Javadoc) * @see org.jasig.schedassist.impl.reminder.ReminderService#getPendingReminders() */ @Override public List<IReminder> getPendingReminders() { List<PersistedReminderImpl> persisted = reminderDao.getPendingReminders(); List<IReminder> results = new ArrayList<IReminder>(persisted.size()); for(PersistedReminderImpl p: persisted) { ReminderImpl reminder = complete(p); if(reminder != null) { results.add(reminder); } } return results; } /* * (non-Javadoc) * @see org.jasig.schedassist.impl.reminder.ReminderService#getReminder(org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.ICalendarAccount, org.jasig.schedassist.model.AvailableBlock) */ @Override public IReminder getReminder(IScheduleOwner owner, ICalendarAccount recipient, AvailableBlock appointmentBlock) { PersistedReminderImpl persisted = reminderDao.getReminder(owner, recipient, appointmentBlock); ReminderImpl result = complete(persisted); return result; } /* (non-Javadoc) * @see org.jasig.schedassist.impl.reminder.ReminderService#getReminders(org.jasig.schedassist.model.IScheduleOwner, org.jasig.schedassist.model.AvailableBlock) */ @Override public List<IReminder> getReminders(IScheduleOwner owner, AvailableBlock appointmentBlock) { List<PersistedReminderImpl> persisted = reminderDao.getReminders(owner, appointmentBlock); List<IReminder> reminders = new ArrayList<IReminder>(); int count = persisted.size(); if(count > 0) { // get the event, owner from the first in the list and pass it into overloaded complete method ReminderImpl first = complete(persisted.get(0)); reminders.add(first); if(count > 1) { // we've already added the first, iterate through the rest List<PersistedReminderImpl> rest = persisted.subList(1, count); for(PersistedReminderImpl p: rest) { reminders.add(complete(p, first.getScheduleOwner(), first.getEvent())); } } } return reminders; } /** * Complete a {@link PersistedReminderImpl} by consulting this instance's * {@link OwnerDao}, {@link ICalendarAccountDao}, and {@link SchedulingAssistantService}. * * @param p * @return a complete {@link ReminderImpl} */ protected ReminderImpl complete(PersistedReminderImpl p) { if(p == null) { return null; } final IScheduleOwner scheduleOwner = this.ownerDao.locateOwnerByAvailableId(p.getOwnerId()); final ICalendarAccount recipient = this.calendarAccountDao.getCalendarAccount(p.getRecipientId()); final VEvent event = this.schedulingAssistantService.getExistingAppointment(p.getTargetBlock(), scheduleOwner); ReminderImpl reminder = new ReminderImpl(p.getReminderId(), scheduleOwner, recipient, p.getSendTime(), event); return reminder; } /** * Overloaded version of {@link #complete(PersistedReminderImpl)} that can skip the call * to {@link SchedulingAssistantService#getExistingAppointment(AvailableBlock, IScheduleOwner)} and * the call to {@link OwnerDao#locateOwnerByAvailableId(long)} (which * are intentionally never cached, where as the {@link ICalendarAccountDao} methods are). * * This is really useful in {@link #getReminders(IScheduleOwner, AvailableBlock)} where all of the * returned values have the same event and schedule owner. * * @param p * @param event * @return a complete {@link ReminderImpl} */ protected ReminderImpl complete(PersistedReminderImpl p, IScheduleOwner owner, VEvent event) { if(p == null) { return null; } final ICalendarAccount recipient = this.calendarAccountDao.getCalendarAccount(p.getRecipientId()); ReminderImpl reminder = new ReminderImpl(p.getReminderId(), owner, recipient, p.getSendTime(), event); return reminder; } /* * (non-Javadoc) * @see org.jasig.schedassist.impl.reminder.ReminderService#processPendingReminders() */ @Override public void processPendingReminders() { final String propertyValue = System.getProperty("org.jasig.schedassist.runScheduledTasks", "true"); if(Boolean.parseBoolean(propertyValue)) { final List<IReminder> pending = getPendingReminders(); final int size = pending.size(); if(size == 0) { return; } LOG.info("begin processing " + size + " pending reminders"); for(IReminder reminder : pending) { try { sendEmail(reminder); } finally { deleteEventReminder(reminder); } } LOG.info("completed processing " + size + " reminders"); } else { LOG.debug("ignoring processPendingReminders as 'org.jasig.schedassist.runScheduledTasks' set to false"); } } /** * Send an email message for this {@link IReminder}. * * @param reminder */ protected void sendEmail(IReminder reminder) { if(shouldSend(reminder)) { final IScheduleOwner owner = reminder.getScheduleOwner(); final ICalendarAccount recipient = reminder.getRecipient(); final VEvent event = reminder.getEvent(); Reminders reminderPrefs = owner.getRemindersPreference(); final boolean includeOwner = reminderPrefs.isIncludeOwner(); SimpleMailMessage message = new SimpleMailMessage(); final boolean canSendToOwner = emailAddressValidator.canSendToEmailAddress(owner.getCalendarAccount()); if(canSendToOwner) { message.setFrom(owner.getCalendarAccount().getEmailAddress()); } else { message.setFrom(noReplyFromAddress); } if(includeOwner && canSendToOwner) { message.setTo(new String[] { owner.getCalendarAccount().getEmailAddress(), recipient.getEmailAddress() }); } else { message.setTo(new String[] { recipient.getEmailAddress() }); } message.setSubject("Reminder: " + event.getSummary().getValue()); final String messageBody = createMessageBody(event, owner); message.setText(messageBody); LOG.debug("sending message: " + message.toString()); try { mailSender.send(message); LOG.debug("message successfully sent"); } catch (MailSendException e) { LOG.error("caught MailSendException for " + owner + ", " + recipient + ", " + reminder, e); } } else { LOG.debug("skipping sendEmail for reminder that should not be sent: " + reminder); } } /** * Verify that this reminder is still valid: * <ul> * <li>Owner and recipient exist.</li> * <li>event still exists.</li> * <li>recipient is attending the event.</li> * </ul> * @param reminder * @return true if this reminder should be sent. */ protected boolean shouldSend(IReminder reminder) { final IScheduleOwner owner = reminder.getScheduleOwner(); if(owner == null) { LOG.debug("owner null, should not send " + reminder); return false; } final ICalendarAccount recipient = reminder.getRecipient(); if(recipient == null) { LOG.debug("recipient null, should not send " + reminder); return false; } if(!emailAddressValidator.canSendToEmailAddress(recipient)) { LOG.debug("validator claims we cannot send to recipient's email, not send " + reminder); return false; } final VEvent event = reminder.getEvent(); if(event == null) { LOG.debug("event null, should not send " + reminder); return false; } boolean recipientAttending = this.eventUtils.isAttendingAsVisitor(event, recipient); if(!recipientAttending) { LOG.debug("recipient not attending, should not send " + reminder); return false; } Property attendee = this.eventUtils.getAttendeeForUserFromEvent(event, recipient); Parameter partstat = attendee.getParameter(PartStat.PARTSTAT); boolean participating = PartStat.ACCEPTED.equals(partstat); LOG.debug("last check is participation, value is " + participating + " for " + reminder); return participating; } /** * Construct the body of the email reminder message from the specified {@link VEvent}. * * @param event * @param owner * @return */ protected String createMessageBody(final VEvent event, IScheduleOwner owner) { StringBuilder messageBody = new StringBuilder(); messageBody.append(this.messageSource.getMessage("reminder.email.introduction", new String[] { owner.getCalendarAccount().getDisplayName() }, null)); messageBody.append(NEWLINE); messageBody.append(NEWLINE); Summary summary = event.getSummary(); if(summary != null) { messageBody.append(this.messageSource.getMessage("reminder.email.title", new String[] { summary.getValue() }, null)); messageBody.append(NEWLINE); } SimpleDateFormat df = new SimpleDateFormat("EEE, MMM d, yyyy"); SimpleDateFormat tf = new SimpleDateFormat("h:mm a"); messageBody.append(df.format(event.getStartDate().getDate())); messageBody.append(NEWLINE); messageBody.append( this.messageSource.getMessage("reminder.email.time", new String[] { tf.format(event.getStartDate().getDate()), tf.format(event.getEndDate(true).getDate())}, null)); Location location = event.getLocation(); if(location != null) { messageBody.append(NEWLINE); messageBody.append(this.messageSource.getMessage("reminder.email.location", new String [] { location.getValue() }, null)); } messageBody.append(NEWLINE); messageBody.append(NEWLINE); messageBody.append(this.messageSource.getMessage("reminder.email.footer", null, null)); return messageBody.toString(); } /* (non-Javadoc) * @see java.lang.Runnable#run() */ @Scheduled(fixedDelay=60000) @Override public void run() { processPendingReminders(); } }