package org.marketcetera.photon.notification; import java.util.Date; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.marketcetera.core.notifications.INotification; import org.marketcetera.core.notifications.Notification; import org.marketcetera.core.notifications.INotification.Severity; import org.marketcetera.util.misc.ClassVersion; /* $License$ */ /** * A {@link Job} that processes {@link INotification} objects asynchronously. * Clients call {@link #enqueueNotification(INotification)} to queue a * notifications to be processed when this job runs. * * When multiple notifications are enqueued between runs, they will be replaced * by a single {@link SummaryNotification}. * * If too many notifications arrive in a short period of time, a * {@link ThresholdReachedNotification} will be issued and the job will stop * running, ignoring any later notifications. * * Subclasses must define how a notification should processed by overriding * {@link #showPopup(INotification, IProgressMonitor)}. * * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: AbstractNotificationJob.java 16843 2014-02-24 14:27:14Z milos $ * @since 0.8.0 */ @ClassVersion("$Id: AbstractNotificationJob.java 16843 2014-02-24 14:27:14Z milos $")//$NON-NLS-1$ public abstract class AbstractNotificationJob extends Job { /** * The frequency in milliseconds at which the job checks for new * notifications. */ /* default for test access */static final long FREQUENCY = 2000; /** * When too many notifications arrive, a single summary notification is * generated. Before the summary is shown, there must be a break in the * notification stream. This constant is the minimum time in milliseconds * that the stream must be "quiet". */ /* default for test access */static final long SUMMARY_DELAY = 1000; /** * When the notification queue surpasses the threshold, the job will shut * down and a single popup will be displayed to inform the user. */ /* default for test access */static final int THRESHOLD = 50; /** * The queue which provides notifications. */ private final Queue<INotification> mQueue; /** * Constructor. * * @param name * the name of the job, see {@link Job#Job(String)} */ protected AbstractNotificationJob(String name) { super(name); this.mQueue = new ConcurrentLinkedQueue<INotification>(); } /** * This implementation of {@link Job#run(IProgressMonitor)} processes * notifications in the queue, summarizing them if necessary. Unless the * threshold is exceeded or the job is interrupted/canceled, this method * will reschedule the job. */ @Override protected final IStatus run(IProgressMonitor monitor) { long delay = 0; INotification notification = null; int size = mQueue.size(); if (size == 0) { if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } // Wait a bit before running again delay = FREQUENCY; } else if (size == 1) { // Show popup for the single notification on the queue if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } notification = mQueue.poll(); if (notification != null) { showPopup(notification, monitor); } } else { if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } // Too many notifications, summarize into one Severity max = Severity.LOW; int count = 0; while (!monitor.isCanceled() && !mQueue.isEmpty()) { while (!mQueue.isEmpty()) { INotification n = mQueue.poll(); if (n != null) { count++; // End job if threshold is reached if (count > THRESHOLD) { showPopup(new ThresholdReachedNotification(), monitor); return Status.CANCEL_STATUS; } // Update max severity max = max.compareTo(n.getSeverity()) > 0 ? max : n .getSeverity(); } } try { // The idea here is to wait a bit for more notifications. In // other words, the summary notification isn't generated // until there is a break in the notification stream. Thread.sleep(SUMMARY_DELAY); } catch (InterruptedException e) { // Cancelling is the proper way to notify an interrupt. // The framework ignores // Thread.currentThread().isInterrupted() return Status.CANCEL_STATUS; } } if (count > 0 && !monitor.isCanceled()) showPopup(new SummaryNotification(count, max), monitor); } if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } schedule(delay); return Status.OK_STATUS; } /** * Enqueues an {@link INotification} to be processed by this job. If the job * is not scheduled, this method does nothing. * * @param notification * notification for this job to process asynchronously. */ public void enqueueNotification(INotification notification) { if (getState() != Job.NONE) { mQueue.add(notification); } } /** * Process the notification. * * This method should not return until the notification has been completely * processed and the user is ready to receive another, e.g. when the popup * has been closed. * * @param notification * notification to process * @param monitor * the monitor to be used for reporting progress and responding * to cancellation. The monitor is never <code>null</code> */ protected abstract void showPopup(INotification notification, IProgressMonitor monitor); /** * A notification that summarizes a set of notifications when they arrive * too quickly. * * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: AbstractNotificationJob.java 16843 2014-02-24 14:27:14Z milos $ * @since 0.8.0 */ @ClassVersion("$Id: AbstractNotificationJob.java 16843 2014-02-24 14:27:14Z milos $")//$NON-NLS-1$ public static class SummaryNotification extends Notification { private static final long serialVersionUID = 1L; /** * Constructor. * * @param count * the number of notifications summarized * @param severity * the severity of the summary notification */ public SummaryNotification(int count, Severity severity) { super(Messages.SUMMARY_NOTIFICATION_SUBJECT.getText(), Messages.SUMMARY_NOTIFICATION_BODY.getText(count), new Date(), severity, AbstractNotificationJob.class.getName()); } } /** * A notification that indicates popups have been disabled. * * @author <a href="mailto:will@marketcetera.com">Will Horn</a> * @version $Id: AbstractNotificationJob.java 16843 2014-02-24 14:27:14Z milos $ * @since 0.8.0 */ @ClassVersion("$Id: AbstractNotificationJob.java 16843 2014-02-24 14:27:14Z milos $")//$NON-NLS-1$ public static class ThresholdReachedNotification extends Notification { private static final long serialVersionUID = 1L; /** * Constructor. */ public ThresholdReachedNotification() { super(Messages.THRESHOLD_NOTIFICATION_SUBJECT.getText(), Messages.THRESHOLD_NOTIFICATION_BODY.getText(), new Date(), Severity.HIGH, AbstractNotificationJob.class.toString()); } } private boolean canceled; public void cancelPopupJob(){ canceled = true; super.cancel(); super.getThread().interrupt(); } @Override public boolean shouldRun() { if(canceled) return false; return super.shouldRun(); } @Override public boolean shouldSchedule() { if(canceled) return false; return super.shouldSchedule(); } }