/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/email/impl/BaseDigestService.java $
* $Id: BaseDigestService.java 105669 2012-03-12 11:56:47Z matthew.buckett@oucs.ox.ac.uk $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.email.impl;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Stack;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.authz.api.SecurityService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.email.api.Digest;
import org.sakaiproject.email.api.DigestEdit;
import org.sakaiproject.email.api.DigestMessage;
import org.sakaiproject.email.api.DigestService;
import org.sakaiproject.email.api.EmailService;
import org.sakaiproject.entity.api.Edit;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.event.api.EventTrackingService;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.IdUsedException;
import org.sakaiproject.exception.InUseException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.time.api.Time;
import org.sakaiproject.time.api.TimeBreakdown;
import org.sakaiproject.time.api.TimeRange;
import org.sakaiproject.time.api.TimeService;
import org.sakaiproject.tool.api.SessionBindingEvent;
import org.sakaiproject.tool.api.SessionBindingListener;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.util.BaseResourcePropertiesEdit;
import org.sakaiproject.util.Resource;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.util.SingleStorageUser;
import org.sakaiproject.util.Xml;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* <p>
* BaseDigestService is the base service for DigestService.
* </p>
*/
public abstract class BaseDigestService implements DigestService, SingleStorageUser
{
/** Our logger. */
private static Log M_log = LogFactory.getLog(BasicEmailService.class);
/** localized tool properties **/
private static final String DEFAULT_RESOURCECLASS = "org.sakaiproject.localization.util.EmailImplProperties";
private static final String DEFAULT_RESOURCEBUNDLE = "org.sakaiproject.localization.bundle.emailimpl.email-impl";
private static final String RESOURCECLASS = "resource.class.emailimpl";
private static final String RESOURCEBUNDLE = "resource.bundle.emailimpl";
private ResourceLoader rb = null;
// private ResourceLoader rb = new ResourceLoader("email-impl");
/** Storage manager for this service. */
protected Storage m_storage = null;
/** The initial portion of a relative access point URL. */
protected String m_relativeAccessPoint = null;
/** The queue of digests waiting to be added (DigestMessage). */
protected List m_digestQueue = new Vector();
/** The thread I run my periodic clean and report on. */
// protected Thread m_thread = null;
/** My thread's quit flag. */
// protected boolean m_threadStop = false;
/** How long to wait between digest checks (seconds) */
private int DIGEST_PERIOD = 3600;
/** How long to wait between digest checks (seconds) */
public void setDIGEST_PERIOD(int digest_period) {
DIGEST_PERIOD = digest_period;
}
/** How long to wait before the first digest check (seconds) */
private int DIGEST_DELAY = 300;
/** How long to wait before the first digest check (seconds) */
public void setDIGEST_DELAY(int digest_delay) {
DIGEST_DELAY = digest_delay;
}
protected boolean m_debugBypass = false;
/** True if we are in the mode of sending out digests, false if we are waiting. */
protected boolean m_sendDigests = true;
/** The time period last time the sendDigests() was called. */
protected String m_lastSendPeriod = null;
/**
* Use a timer for repeating actions
*/
private Timer digestTimer = new Timer(true);
/**
* This is the name of the sakai.properties property for the DIGEST_PERIOD,
* this is how long (in seconds) the digest service will wait between checking to see if there
* are digests that need to be sent (they are always only sent once per day), default=3600
*/
public static final String EMAIL_DIGEST_CHECK_PERIOD_PROPERTY = "email.digest.check.period";
/**
* This is the name of the sakai.properties property for the DIGEST_DELAY,
* this is how long (in seconds) the digest service will wait after starting up
* before it does the first check for sending out digests, default=300
*/
public static final String EMAIL_DIGEST_START_DELAY_PROPERTY = "email.digest.start.delay";
public static final String BY_PASS_FOR_DEBUG = "digest.email.bypass.for.debug";
/**********************************************************************************************************************************************************************************************************************************************************
* Runnable
*********************************************************************************************************************************************************************************************************************************************************/
/**
* Start the clean and report thread.
*/
// protected void start()
// {
// m_threadStop = false;
//
// m_thread = new Thread(this, getClass().getName());
// m_thread.start();
// }
/**
* Stop the clean and report thread.
*/
// protected void stop()
// {
// if (m_thread == null) return;
//
// // signal the thread to stop
// m_threadStop = true;
//
// // wake up the thread
// m_thread.interrupt();
//
// m_thread = null;
// }
/**
* Run the clean and report thread.
*/
// public void run()
// {
// // since we might be running while the component manager is still being created and populated, such as at server
// // startup, wait here for a complete component manager
// ComponentManager.waitTillConfigured();
//
// // loop till told to stop
// while ((!m_threadStop) && (!Thread.currentThread().isInterrupted()))
// {
// try
// {
// // process the queue of digest requests
// processQueue();
//
// // check for a digest mailing time
// sendDigests();
// }
// catch (Exception e)
// {
// M_log.warn(": exception: ", e);
// }
//
// // take a small nap
// try
// {
// Thread.sleep(PERIOD);
// }
// catch (Exception ignore)
// {
// }
// }
// }
/**
* Attempt to process all the queued digest requests. Ones that cannot be processed now will be returned to the queue.
*/
protected void processQueue()
{
M_log.debug("Processing mail digest queue...");
// setup a re-try queue
List retry = new Vector();
// grab the queue - any new stuff will be processed next time
List queue = new Vector();
synchronized (m_digestQueue)
{
queue.addAll(m_digestQueue);
m_digestQueue.clear();
}
for (Iterator iQueue = queue.iterator(); iQueue.hasNext();)
{
DigestMessage message = (DigestMessage) iQueue.next();
try
{
DigestEdit edit = edit(message.getTo());
edit.add(message);
commit(edit);
// %%% could do this by pulling all for id from the queue in one commit -ggolden
}
catch (InUseException e)
{
M_log.warn("digest in use, will try send again at next digest attempt: " + e.getMessage());
// retry next time
retry.add(message);
}
}
// requeue the retrys
if (retry.size() > 0)
{
synchronized (m_digestQueue)
{
m_digestQueue.addAll(retry);
}
}
}
/**
* If it's time, send out any digested messages. Send once daily, after a certiain time of day (local time).
*/
protected void sendDigests()
{
if (M_log.isDebugEnabled()) M_log.debug("checking for sending digests");
// compute the current period
String curPeriod = computeRange(timeService.newTime()).toString();
// if we are in a new period, start sending again
if (!curPeriod.equals(m_lastSendPeriod) || m_debugBypass)
{
m_sendDigests = true;
// remember this period for next check
m_lastSendPeriod = curPeriod;
}
// if we are not sending, early out
if (!m_sendDigests) return;
M_log.info("Preparing to send the mail digests for "+curPeriod);
// count send candidate digests
int count = 0;
// process each digest
List digests = getDigests();
for (Iterator iDigests = digests.iterator(); iDigests.hasNext();)
{
Digest digest = (Digest) iDigests.next();
// see if this one has any prior periods
List periods = digest.getPeriods();
if (periods.size() == 0) continue;
boolean found = false;
for (Iterator iPeriods = periods.iterator(); iPeriods.hasNext();)
{
String period = (String) iPeriods.next();
if (!curPeriod.equals(period) || m_debugBypass)
{
found = true;
break;
}
}
if (!found) {
continue;
}
// this digest is a send candidate
count++;
// get a lock
DigestEdit edit = null;
try
{
boolean changed = false;
edit = edit(digest.getId());
// process each non-current period
for (Iterator iPeriods = edit.getPeriods().iterator(); iPeriods.hasNext();)
{
String period = (String) iPeriods.next();
// process if it's not the current period
if (!curPeriod.equals(period) || m_debugBypass)
{
TimeRange periodRange = timeService.newTimeRange(period);
Time timeInPeriod = periodRange.firstTime();
// any messages?
List msgs = edit.getMessages(timeInPeriod);
if (msgs.size() > 0)
{
// send this one
send(edit.getId(), msgs, periodRange);
}
// clear this period
edit.clear(timeInPeriod);
changed = true;
}
}
// commit, release the lock
if (changed)
{
// delete it if empty
if (edit.getPeriods().size() == 0)
{
remove(edit);
}
else
{
commit(edit);
}
edit = null;
}
else
{
cancel(edit);
edit = null;
}
}
// if in use, missing, whatever, skip on
catch (Exception any)
{
}
finally
{
if (edit != null)
{
cancel(edit);
edit = null;
}
}
} // for (Iterator iDigests = digests.iterator(); iDigests.hasNext();)
// if we didn't see any send candidates, we will stop sending till next period
if (count == 0)
{
m_sendDigests = false;
}
}
/**
* Send a single digest message
*
* @param id
* The use id to send the message to.
* @param msgs
* The List (DigestMessage) of message to digest.
* @param period
* The time period of the digested messages.
*/
protected void send(String id, List msgs, TimeRange period)
{
// sanity check
if (msgs.size() == 0) return;
try
{
String to = userDirectoryService.getUser(id).getEmail();
// if use has no email address we can't send it
if ((to == null) || (to.length() == 0)) return;
String from = "postmaster@" + serverConfigurationService.getServerName();
String subject = serverConfigurationService.getString("ui.service", "Sakai") + " " + rb.getString("notif") + " "
+ period.firstTime().toStringLocalDate();
StringBuilder body = new StringBuilder();
body.append(subject);
body.append("\n\n");
// toc
int count = 1;
for (Iterator iMsgs = msgs.iterator(); iMsgs.hasNext();)
{
DigestMessage msg = (DigestMessage) iMsgs.next();
body.append(Integer.toString(count));
body.append(". ");
body.append(msg.getSubject());
body.append("\n");
count++;
}
body.append("\n----------------------\n\n");
// for each msg
count = 1;
for (Iterator iMsgs = msgs.iterator(); iMsgs.hasNext();)
{
DigestMessage msg = (DigestMessage) iMsgs.next();
// repeate toc entry
body.append(Integer.toString(count));
body.append(". ");
body.append(msg.getSubject());
body.append("\n\n");
// message body
body.append(msg.getBody());
body.append("\n----------------------\n\n");
count++;
}
// tag
body.append(rb.getString("thiaut") + " " + serverConfigurationService.getString("ui.service", "Sakai") + " " + "("
+ serverConfigurationService.getServerUrl() + ")" + "\n" + rb.getString("youcan") + "\n");
if (M_log.isDebugEnabled()) M_log.debug(this + " sending digest email to: " + to);
emailService.send(from, to, subject, body.toString(), to, null, null);
}
catch (Exception any)
{
M_log.warn(".send: digest to: " + id + " not sent: " + any.toString());
}
}
/**********************************************************************************************************************************************************************************************************************************************************
* Abstractions, etc.
*********************************************************************************************************************************************************************************************************************************************************/
/**
* Construct storage for this service.
*/
protected abstract Storage newStorage();
/**
* Access the partial URL that forms the root of resource URLs.
*
* @param relative
* if true, form within the access path only (i.e. starting with /content)
* @return the partial URL that forms the root of resource URLs.
*/
protected String getAccessPoint(boolean relative)
{
return (relative ? "" : serverConfigurationService.getAccessUrl()) + m_relativeAccessPoint;
}
/**
* Access the internal reference which can be used to access the resource from within the system.
*
* @param id
* The digest id string.
* @return The the internal reference which can be used to access the resource from within the system.
*/
public String digestReference(String id)
{
return getAccessPoint(true) + Entity.SEPARATOR + id;
}
/**
* Access the digest id extracted from a digest reference.
*
* @param ref
* The digest reference string.
* @return The the digest id extracted from a digest reference.
*/
protected String digestId(String ref)
{
String start = getAccessPoint(true) + Entity.SEPARATOR;
int i = ref.indexOf(start);
if (i == -1) return ref;
String id = ref.substring(i + start.length());
return id;
}
/**
* Check security permission.
*
* @param lock
* The lock id string.
* @param resource
* The resource reference string, or null if no resource is involved.
* @return true if allowd, false if not
*/
protected boolean unlockCheck(String lock, String resource)
{
if (!securityService.unlock(lock, resource))
{
return false;
}
return true;
}
/**
* Check security permission.
*
* @param lock
* The lock id string.
* @param resource
* The resource reference string, or null if no resource is involved.
* @exception PermissionException
* Thrown if the user does not have access
*/
protected void unlock(String lock, String resource) throws PermissionException
{
if (!unlockCheck(lock, resource))
{
throw new PermissionException(sessionManager.getCurrentSessionUserId(), lock, resource);
}
}
/**********************************************************************************************************************************************************************************************************************************************************
* Dependencies
*********************************************************************************************************************************************************************************************************************************************************/
protected TimeService timeService;
protected ServerConfigurationService serverConfigurationService;
protected EmailService emailService;
protected EventTrackingService eventTrackingService;
protected SecurityService securityService;
protected UserDirectoryService userDirectoryService;
protected SessionManager sessionManager;
/**
* @return the TimeService collaborator.
*/
public void setTimeService(TimeService timeService)
{
this.timeService = timeService;
}
/**
* @return the ServerConfigurationService collaborator.
*/
public void setServerConfigurationService(ServerConfigurationService serverConfigurationService)
{
this.serverConfigurationService = serverConfigurationService;
}
/**
* @return the EmailService collaborator.
*/
public void setEmailService(EmailService emailService)
{
this.emailService = emailService;
}
/**
* @return the EventTrackingService collaborator.
*/
public void setEventTrackingService(EventTrackingService eventTrackingService)
{
this.eventTrackingService = eventTrackingService;
}
/**
* @return the MemoryServiSecurityServicece collaborator.
*/
public void setSecurityService(SecurityService securityService)
{
this.securityService = securityService;
}
/**
* @return the UserDirectoryService collaborator.
*/
public void setUserDirectoryService(UserDirectoryService userDirectoryService)
{
this.userDirectoryService = userDirectoryService;
}
/**
* @return the SessionManager collaborator.
*/
public void setSessionManager(SessionManager sessionManager)
{
this.sessionManager = sessionManager;
}
/**********************************************************************************************************************************************************************************************************************************************************
* Init and Destroy
*********************************************************************************************************************************************************************************************************************************************************/
/**
* Final initialization, once all dependencies are set.
*/
public void init()
{
m_relativeAccessPoint = REFERENCE_ROOT;
// construct storage and read
m_storage = newStorage();
m_storage.open();
// setup the queue
m_digestQueue.clear();
// Resource Bundle
String resourceClass = serverConfigurationService.getString(RESOURCECLASS, DEFAULT_RESOURCECLASS);
String resourceBundle = serverConfigurationService.getString(RESOURCEBUNDLE, DEFAULT_RESOURCEBUNDLE);
rb = new Resource().getLoader(resourceClass, resourceBundle);
// USE A TIMER INSTEAD OF CREATING A NEW THREAD -AZ
// start();
int digestPeriod = serverConfigurationService.getInt(EMAIL_DIGEST_CHECK_PERIOD_PROPERTY, DIGEST_PERIOD);
int digestDelay = serverConfigurationService.getInt(EMAIL_DIGEST_START_DELAY_PROPERTY, DIGEST_DELAY);
m_debugBypass = serverConfigurationService.getBoolean(BY_PASS_FOR_DEBUG, false);
digestDelay += new Random().nextInt(60); // add some random delay to get the servers out of sync
digestTimer.schedule(new DigestTimerTask(), (digestDelay * 1000), (digestPeriod * 1000) );
M_log.info("init(): email digests will be checked in " + digestDelay + " seconds and then every "
+ digestPeriod + " seconds while the server is running" );
}
/**
* This timer task is run by the timer thread based on the period set above
*
* @author Aaron Zeckoski (aaron@caret.cam.ac.uk)
*/
private class DigestTimerTask extends TimerTask {
@Override
public void run() {
try {
M_log.debug("running timer task");
// process the queue of digest requests
processQueue();
// check for a digest mailing time
sendDigests();
} catch (Exception e) {
M_log.error("Digest failure: " + e.getMessage(), e);
}
}
}
/**
* Returns to uninitialized state.
*/
public void destroy()
{
// stop();
digestTimer.cancel();
m_storage.close();
m_storage = null;
if (m_digestQueue.size() > 0)
{
M_log.warn(".shutdown: with items in digest queue"); // %%%
}
m_digestQueue.clear();
M_log.info("destroy()");
}
/**********************************************************************************************************************************************************************************************************************************************************
* DigestService implementation
*********************************************************************************************************************************************************************************************************************************************************/
/**
* @inheritDoc
*/
public Digest getDigest(String id) throws IdUnusedException
{
Digest digest = findDigest(id);
if (digest == null) throw new IdUnusedException(id);
return digest;
}
/**
* @inheritDoc
*/
public List getDigests()
{
List digests = m_storage.getAll();
return digests;
}
/**
* @inheritDoc
*/
public void digest(String to, String subject, String body)
{
DigestMessage message = new org.sakaiproject.email.impl.DigestMessage(to, subject, body);
// queue this for digesting
synchronized (m_digestQueue)
{
m_digestQueue.add(message);
}
}
/**
* @inheritDoc
*/
public DigestEdit edit(String id) throws InUseException
{
// security
// unlock(SECURE_EDIT_DIGEST, digestReference(id));
// one add/edit at a time, please, to make sync. only one digest per user
// TODO: I don't link sync... could just do the add and let it fail if it already exists -ggolden
synchronized (m_storage)
{
// check for existance
if (!m_storage.check(id))
{
try
{
return add(id);
}
catch (IdUsedException e)
{
M_log.warn(".edit: from the add: " + e);
}
}
// ignore the cache - get the user with a lock from the info store
DigestEdit edit = m_storage.edit(id);
if (edit == null) throw new InUseException(id);
((BaseDigest) edit).setEvent(SECURE_EDIT_DIGEST);
return edit;
}
}
/**
* @inheritDoc
*/
public void commit(DigestEdit edit)
{
// check for closed edit
if (!edit.isActiveEdit())
{
try
{
throw new Exception();
}
catch (Exception e)
{
M_log.warn(".commit(): closed DigestEdit", e);
}
return;
}
// update the properties
// addLiveUpdateProperties(user.getPropertiesEdit());
// complete the edit
m_storage.commit(edit);
// track it
eventTrackingService.post(eventTrackingService.newEvent(((BaseDigest) edit).getEvent(), edit.getReference(), true));
// close the edit object
((BaseDigest) edit).closeEdit();
}
/**
* @inheritDoc
*/
public void cancel(DigestEdit edit)
{
// check for closed edit
if (!edit.isActiveEdit())
{
try
{
throw new Exception();
}
catch (Exception e)
{
M_log.warn(".cancel(): closed DigestEdit", e);
}
return;
}
// release the edit lock
m_storage.cancel(edit);
// close the edit object
((BaseDigest) edit).closeEdit();
}
/**
* @inheritDoc
*/
public void remove(DigestEdit edit)
{
// check for closed edit
if (!edit.isActiveEdit())
{
try
{
throw new Exception();
}
catch (Exception e)
{
M_log.warn(".remove(): closed DigestEdit", e);
}
return;
}
// complete the edit
m_storage.remove(edit);
// track it
eventTrackingService.post(eventTrackingService.newEvent(SECURE_REMOVE_DIGEST, edit.getReference(), true));
// close the edit object
((BaseDigest) edit).closeEdit();
}
/**
* @inheritDoc
*/
protected BaseDigest findDigest(String id)
{
BaseDigest digest = (BaseDigest) m_storage.get(id);
return digest;
}
/**
* @inheritDoc
*/
public DigestEdit add(String id) throws IdUsedException
{
// check security (throws if not permitted)
// unlock(SECURE_ADD_DIGEST, digestReference(id));
// one add/edit at a time, please, to make sync. only one digest per user
synchronized (m_storage)
{
// reserve a user with this id from the info store - if it's in use, this will return null
DigestEdit edit = m_storage.put(id);
if (edit == null)
{
throw new IdUsedException(id);
}
return edit;
}
}
/**********************************************************************************************************************************************************************************************************************************************************
* Digest implementation
*********************************************************************************************************************************************************************************************************************************************************/
public class BaseDigest implements DigestEdit, SessionBindingListener
{
/** The user id. */
protected String m_id = null;
/** The properties. */
protected ResourcePropertiesEdit m_properties = null;
/** The digest time ranges (Map TimeRange string to List of DigestMessage). */
protected Map m_ranges = null;
/**
* Construct.
*
* @param id
* The user id.
*/
public BaseDigest(String id)
{
m_id = id;
// setup for properties
ResourcePropertiesEdit props = new BaseResourcePropertiesEdit();
m_properties = props;
// setup for ranges
m_ranges = new Hashtable();
// if the id is not null (a new user, rather than a reconstruction)
// and not the anon (id == "") user,
// add the automatic (live) properties
// %%% if ((m_id != null) && (m_id.length() > 0)) addLiveProperties(props);
}
/**
* Construct from another Digest object.
*
* @param user
* The user object to use for values.
*/
public BaseDigest(Digest digest)
{
setAll(digest);
}
/**
* Construct from information in XML.
*
* @param el
* The XML DOM Element definining the user.
*/
public BaseDigest(Element el)
{
// setup for properties
m_properties = new BaseResourcePropertiesEdit();
// setup for ranges
m_ranges = new Hashtable();
m_id = el.getAttribute("id");
// the children (properties, messages)
NodeList children = el.getChildNodes();
final int length = children.getLength();
for (int i = 0; i < length; i++)
{
Node child = children.item(i);
if (child.getNodeType() != Node.ELEMENT_NODE) continue;
Element element = (Element) child;
// look for properties
if (element.getTagName().equals("properties"))
{
// re-create properties
m_properties = new BaseResourcePropertiesEdit(element);
}
// look for a messages
else if (element.getTagName().equals("messages"))
{
String period = element.getAttribute("period");
// find the range
List msgs = (List) m_ranges.get(period);
if (msgs == null)
{
msgs = new Vector();
m_ranges.put(period, msgs);
}
// do these children for messages
NodeList msgChildren = element.getChildNodes();
final int msgChildrenLen = msgChildren.getLength();
for (int m = 0; m < msgChildrenLen; m++)
{
Node msgChild = msgChildren.item(m);
if (msgChild.getNodeType() != Node.ELEMENT_NODE) continue;
Element msgChildEl = (Element) msgChild;
if (msgChildEl.getTagName().equals("message"))
{
String subject = Xml.decodeAttribute(msgChildEl, "subject");
String body = Xml.decodeAttribute(msgChildEl, "body");
msgs.add(new org.sakaiproject.email.impl.DigestMessage(m_id, subject, body));
}
}
}
}
}
/**
* Take all values from this object.
*
* @param user
* The user object to take values from.
*/
protected void setAll(Digest digest)
{
m_id = digest.getId();
m_properties = new BaseResourcePropertiesEdit();
m_properties.addAll(digest.getProperties());
m_ranges = new Hashtable();
// %%% deep enough? -ggolden
m_ranges.putAll(((BaseDigest) digest).m_ranges);
}
/**
* @inheritDoc
*/
public Element toXml(Document doc, Stack stack)
{
Element digest = doc.createElement("digest");
if (stack.isEmpty())
{
doc.appendChild(digest);
}
else
{
((Element) stack.peek()).appendChild(digest);
}
stack.push(digest);
digest.setAttribute("id", getId());
// properties
m_properties.toXml(doc, stack);
// for each message range
for (Iterator it = m_ranges.entrySet().iterator(); it.hasNext();)
{
Map.Entry entry = (Map.Entry) it.next();
Element messages = doc.createElement("messages");
digest.appendChild(messages);
messages.setAttribute("period", (String) entry.getKey());
// for each message
for (Iterator iMsgs = ((List) entry.getValue()).iterator(); iMsgs.hasNext();)
{
DigestMessage msg = (DigestMessage) iMsgs.next();
Element message = doc.createElement("message");
messages.appendChild(message);
Xml.encodeAttribute(message, "subject", msg.getSubject());
Xml.encodeAttribute(message, "body", msg.getBody());
}
}
stack.pop();
return digest;
}
/**
* @inheritDoc
*/
public String getId()
{
if (m_id == null) return "";
return m_id;
}
/**
* @inheritDoc
*/
public String getUrl()
{
return getAccessPoint(false) + m_id;
}
/**
* @inheritDoc
*/
public String getReference()
{
return digestReference(m_id);
}
/**
* @inheritDoc
*/
public String getReference(String rootProperty)
{
return getReference();
}
/**
* @inheritDoc
*/
public String getUrl(String rootProperty)
{
return getUrl();
}
/**
* @inheritDoc
*/
public ResourceProperties getProperties()
{
return m_properties;
}
/**
* @inheritDoc
*/
public List getMessages(Time period)
{
synchronized (m_ranges)
{
// find the range
String range = computeRange(period).toString();
/*
* http://jira.sakaiproject.org/jira/browse/SAK-11841
* If the current date/time gets out of sync with the stored date/time periods then
* messages will sit in the queue forever and cause looping in the code which will
* never resolve itself, to keep this from happening I am adding in an extra
* check as a stopgap which will do the reverse check in the case that nothing
* is retrieved, this really ugly and needs to be done a better way
* (which means a way that is not so fragile) -AZ
*/
List msgs = (List) m_ranges.get(range);
if (msgs == null) {
// nothing found so go through all ranges and hack the range strings
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHHmm");
for (Iterator<String> iterator = m_ranges.keySet().iterator(); iterator.hasNext();) {
String rangeKey = (String) iterator.next();
Date startDate;
try {
startDate = formatter.parse( range.substring(0, 12) );
long difference = Math.abs( period.getTime() - startDate.getTime() );
if (difference < (12 * 60 * 60 * 1000)) {
// within 12 hours of the correct period so use this one
msgs = (List) m_ranges.get(rangeKey);
break;
}
} catch (ParseException e) {
M_log.warn("Failed to parse first 12 chars from '"+rangeKey+"' into a date, aborting the attempt to find close data matches", e);
}
}
}
List rv = new Vector();
if (msgs != null) {
rv.addAll(msgs);
}
return rv;
}
}
/**
* @inheritDoc
*/
public List getPeriods()
{
synchronized (m_ranges)
{
List rv = new Vector();
rv.addAll(m_ranges.keySet());
return rv;
}
}
/**
* @inheritDoc
*/
public boolean equals(Object obj)
{
if (!(obj instanceof Digest)) return false;
return ((Digest) obj).getId().equals(getId());
}
/**
* @inheritDoc
*/
public int hashCode()
{
return getId().hashCode();
}
/**
* @inheritDoc
*/
public int compareTo(Object obj)
{
if (!(obj instanceof Digest)) throw new ClassCastException();
// if the object are the same, say so
if (obj == this) return 0;
// sort based on (unique) id
int compare = getId().compareTo(((Digest) obj).getId());
return compare;
}
/******************************************************************************************************************************************************************************************************************************************************
* Edit implementation
*****************************************************************************************************************************************************************************************************************************************************/
/** The event code for this edit. */
protected String m_event = null;
/** Active flag. */
protected boolean m_active = false;
/**
* @inheritDoc
*/
public void add(DigestMessage msg)
{
synchronized (m_ranges)
{
// find the current range
String range = computeRange(timeService.newTime()).toString();
List msgs = (List) m_ranges.get(range);
if (msgs == null)
{
msgs = new Vector();
m_ranges.put(range, msgs);
}
msgs.add(msg);
}
}
/**
* @inheritDoc
*/
public void add(String to, String subject, String body)
{
DigestMessage msg = new org.sakaiproject.email.impl.DigestMessage(to, subject, body);
synchronized (m_ranges)
{
// find the current range
String range = computeRange(timeService.newTime()).toString();
List msgs = (List) m_ranges.get(range);
if (msgs == null)
{
msgs = new Vector();
m_ranges.put(range, msgs);
}
msgs.add(msg);
}
}
/**
* @inheritDoc
*/
public void clear(Time period)
{
synchronized (m_ranges)
{
// find the range
String range = computeRange(period).toString();
List msgs = (List) m_ranges.get(range);
if (msgs != null)
{
m_ranges.remove(range);
}
}
}
/**
* Clean up.
*/
protected void finalize()
{
// catch the case where an edit was made but never resolved
if (m_active)
{
cancel(this);
}
}
/**
* Take all values from this object.
*
* @param user
* The user object to take values from.
*/
protected void set(Digest digest)
{
setAll(digest);
}
/**
* Access the event code for this edit.
*
* @return The event code for this edit.
*/
protected String getEvent()
{
return m_event;
}
/**
* Set the event code for this edit.
*
* @param event
* The event code for this edit.
*/
protected void setEvent(String event)
{
m_event = event;
}
/**
* @inheritDoc
*/
public ResourcePropertiesEdit getPropertiesEdit()
{
return m_properties;
}
/**
* Enable editing.
*/
protected void activate()
{
m_active = true;
}
/**
* @inheritDoc
*/
public boolean isActiveEdit()
{
return m_active;
}
/**
* Close the edit object - it cannot be used after this.
*/
protected void closeEdit()
{
m_active = false;
}
/******************************************************************************************************************************************************************************************************************************************************
* SessionBindingListener implementation
*****************************************************************************************************************************************************************************************************************************************************/
/**
* @inheritDoc
*/
public void valueBound(SessionBindingEvent event)
{
}
/**
* @inheritDoc
*/
public void valueUnbound(SessionBindingEvent event)
{
if (M_log.isDebugEnabled()) M_log.debug(this + ".valueUnbound()");
// catch the case where an edit was made but never resolved
if (m_active)
{
cancel(this);
}
}
}
/**********************************************************************************************************************************************************************************************************************************************************
* Storage
*********************************************************************************************************************************************************************************************************************************************************/
protected interface Storage
{
/**
* Open.
*/
public void open();
/**
* Close.
*/
public void close();
/**
* Check if a digest by this id exists.
*
* @param id
* The user id.
* @return true if a digest for this id exists, false if not.
*/
public boolean check(String id);
/**
* Get the digest with this id, or null if not found.
*
* @param id
* The digest id.
* @return The digest with this id, or null if not found.
*/
public Digest get(String id);
/**
* Get all digests.
*
* @return The list of all digests.
*/
public List getAll();
/**
* Add a new digest with this id.
*
* @param id
* The digest id.
* @return The locked Digest object with this id, or null if the id is in use.
*/
public DigestEdit put(String id);
/**
* Get a lock on the digest with this id, or null if a lock cannot be gotten.
*
* @param id
* The digest id.
* @return The locked Digest with this id, or null if this records cannot be locked.
*/
public DigestEdit edit(String id);
/**
* Commit the changes and release the lock.
*
* @param user
* The edit to commit.
*/
public void commit(DigestEdit edit);
/**
* Cancel the changes and release the lock.
*
* @param user
* The edit to commit.
*/
public void cancel(DigestEdit edit);
/**
* Remove this edit and release the lock.
*
* @param user
* The edit to remove.
*/
public void remove(DigestEdit edit);
}
/**********************************************************************************************************************************************************************************************************************************************************
* StorageUser implementation (no container)
*********************************************************************************************************************************************************************************************************************************************************/
/**
* @inheritDoc
*/
public Entity newResource(Entity container, String id, Object[] others)
{
return new BaseDigest(id);
}
/**
* @inheritDoc
*/
public Entity newResource(Entity container, Element element)
{
return new BaseDigest(element);
}
/**
* @inheritDoc
*/
public Entity newResource(Entity container, Entity other)
{
return new BaseDigest((Digest) other);
}
/**
* @inheritDoc
*/
public Edit newResourceEdit(Entity container, String id, Object[] others)
{
BaseDigest e = new BaseDigest(id);
e.activate();
return e;
}
/**
* @inheritDoc
*/
public Edit newResourceEdit(Entity container, Element element)
{
BaseDigest e = new BaseDigest(element);
e.activate();
return e;
}
/**
* @inheritDoc
*/
public Edit newResourceEdit(Entity container, Entity other)
{
BaseDigest e = new BaseDigest((Digest) other);
e.activate();
return e;
}
/**
* @inheritDoc
*/
public Object[] storageFields(Entity r)
{
return null;
}
/**
* Compute a time range based on a specific time.
*
* @return The time range that encloses the specific time.
*/
protected TimeRange computeRange(Time time)
{
// set the period to "today" (local!) from day start to next day start, not end inclusive
TimeBreakdown brk = time.breakdownLocal();
brk.setMs(0);
brk.setSec(0);
brk.setMin(0);
brk.setHour(0);
Time start = timeService.newTimeLocal(brk);
Time end = timeService.newTime(start.getTime() + 24 * 60 * 60 * 1000);
return timeService.newTimeRange(start, end, true, false);
}
}