001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.ken.web.spring;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.criteria.QueryByCriteria;
021import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
022import org.kuali.rice.core.framework.persistence.dao.GenericDao;
023import org.kuali.rice.coreservice.api.namespace.Namespace;
024import org.kuali.rice.coreservice.api.namespace.NamespaceService;
025import org.kuali.rice.ken.bo.NotificationBo;
026import org.kuali.rice.ken.bo.NotificationChannelBo;
027import org.kuali.rice.ken.bo.NotificationChannelReviewerBo;
028import org.kuali.rice.ken.bo.NotificationPriorityBo;
029import org.kuali.rice.ken.bo.NotificationProducerBo;
030import org.kuali.rice.ken.bo.NotificationRecipientBo;
031import org.kuali.rice.ken.bo.NotificationSenderBo;
032import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument;
033import org.kuali.rice.ken.exception.ErrorList;
034import org.kuali.rice.ken.service.NotificationChannelService;
035import org.kuali.rice.ken.service.NotificationMessageContentService;
036import org.kuali.rice.ken.service.NotificationRecipientService;
037import org.kuali.rice.ken.service.NotificationService;
038import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
039import org.kuali.rice.ken.util.NotificationConstants;
040import org.kuali.rice.ken.util.Util;
041import org.kuali.rice.kew.api.WorkflowDocument;
042import org.kuali.rice.kew.rule.GenericAttributeContent;
043import org.kuali.rice.kim.api.KimConstants;
044import org.kuali.rice.kim.api.group.Group;
045import org.kuali.rice.kim.api.group.GroupService;
046import org.kuali.rice.kim.api.identity.IdentityService;
047import org.kuali.rice.kim.api.identity.principal.Principal;
048import org.kuali.rice.kim.api.services.KimApiServiceLocator;
049import org.kuali.rice.coreservice.api.CoreServiceApiServiceLocator;
050import org.kuali.rice.krad.data.DataObjectService;
051import org.springframework.web.servlet.ModelAndView;
052import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
053
054import javax.servlet.ServletException;
055import javax.servlet.http.HttpServletRequest;
056import java.io.IOException;
057import java.sql.Timestamp;
058import java.text.ParseException;
059import java.util.ArrayList;
060import java.util.Date;
061import java.util.HashMap;
062import java.util.List;
063import java.util.Map;
064
065/**
066 * Base class for KEN controllers for sending notifications
067 *
068 * @author Kuali Rice Team (rice.collab@kuali.org)
069 *
070 */
071public class BaseSendNotificationController extends MultiActionController {
072    private static final Logger LOG = Logger.getLogger(BaseSendNotificationController.class);
073
074    private static final String USER_RECIPS_PARAM = "userRecipients";
075    private static final String WORKGROUP_RECIPS_PARAM = "workgroupRecipients";
076    private static final String WORKGROUP_NAMESPACE_CODES_PARAM = "workgroupNamespaceCodes";
077    private static final String SPLIT_REGEX = "(%2C|,)";
078
079    private static final String NONE_CHANNEL = "___NONE___";
080    private static final long REASONABLE_IMMEDIATE_TIME_THRESHOLD = 1000 * 60 * 5; // <= 5 minutes is "immediate"
081
082    private static IdentityService identityService;
083    private static GroupService groupService;
084    private static NamespaceService namespaceService;
085
086    protected NotificationService notificationService;
087    protected NotificationWorkflowDocumentService notificationWorkflowDocService;
088    protected NotificationChannelService notificationChannelService;
089    protected NotificationRecipientService notificationRecipientService;
090    protected NotificationMessageContentService notificationMessageContentService;
091    protected DataObjectService dataObjectService;
092
093    protected static IdentityService getIdentityService() {
094        if ( identityService == null ) {
095            identityService = KimApiServiceLocator.getIdentityService();
096        }
097        return identityService;
098    }
099
100    protected static GroupService getGroupService() {
101        if ( groupService == null ) {
102            groupService = KimApiServiceLocator.getGroupService();
103        }
104        return groupService;
105    }
106
107    protected static NamespaceService getNamespaceService() {
108        if ( namespaceService == null ) {
109            namespaceService = CoreServiceApiServiceLocator.getNamespaceService();
110        }
111        return namespaceService;
112    }
113
114    /**
115     * Sets the {@link NotificationService}.
116     *
117     * @param notificationService the service to set
118     */
119    public void setNotificationService(NotificationService notificationService) {
120        this.notificationService = notificationService;
121    }
122
123    /**
124     * Sets the {@link NotificationWorkflowDocumentService}.
125     *
126     * @param notificationWorkflowDocService the service to set
127     */
128    public void setNotificationWorkflowDocumentService(NotificationWorkflowDocumentService notificationWorkflowDocService) {
129        this.notificationWorkflowDocService = notificationWorkflowDocService;
130    }
131
132    /**
133     * Sets the {@link NotificationChannelService}.
134     *
135     * @param notificationChannelService the service to set
136     */
137    public void setNotificationChannelService(NotificationChannelService notificationChannelService) {
138        this.notificationChannelService = notificationChannelService;
139    }
140
141    /**
142     * Sets the {@link NotificationRecipientService}.
143     *
144     * @param notificationRecipientService the service to set
145     */
146    public void setNotificationRecipientService(NotificationRecipientService notificationRecipientService) {
147        this.notificationRecipientService = notificationRecipientService;
148    }
149
150    /**
151     * Sets the {@link NotificationMessageContentService}.
152     *
153     * @param notificationMessageContentService the service to set
154     */
155    public void setNotificationMessageContentService(NotificationMessageContentService notificationMessageContentService) {
156        this.notificationMessageContentService = notificationMessageContentService;
157    }
158
159    /**
160     * Sets the businessObjectDao attribute value.
161     * @param dataObjectService the service to set
162     */
163    public void setDataObjectService(DataObjectService dataObjectService) {
164        this.dataObjectService = dataObjectService;
165    }
166
167
168    protected String getParameter(HttpServletRequest request, String parameterName, Map<String, Object> model, ErrorList errors, String errorMessage) {
169        String parameter = request.getParameter(parameterName);
170
171        if (StringUtils.isNotEmpty(parameter)) {
172            model.put(parameterName, parameter);
173        } else {
174            errors.addError(errorMessage);
175        }
176
177        return parameter;
178    }
179
180    protected String getParameter(HttpServletRequest request, String parameterName, Map<String, Object> model, ErrorList errors, String errorMessage, String defaultValue) {
181        String parameter = StringUtils.defaultIfBlank(request.getParameter(parameterName), defaultValue);
182
183        if (StringUtils.isNotEmpty(parameter)) {
184            model.put(parameterName, parameter);
185        } else {
186            errors.addError(errorMessage);
187        }
188
189        return parameter;
190    }
191
192    protected String[] getParameterList(HttpServletRequest request, String parameterName, Map<String, Object> model, ErrorList errors, String errorMessage) {
193        String parameter = request.getParameter(parameterName);
194        String[] senders = null;
195
196        if (StringUtils.isNotEmpty(parameter)) {
197            senders = StringUtils.split(parameter, ",");
198            model.put(parameterName, parameter);
199        } else {
200            errors.addError(errorMessage);
201        }
202
203        return senders;
204    }
205
206    protected Date getDate(String parameter, ErrorList errors, String errorMessage) {
207        Date date = null;
208
209        try {
210            date = Util.parseUIDateTime(parameter);
211        } catch (ParseException pe) {
212            errors.addError(errorMessage);
213        }
214
215        return date;
216    }
217    
218    protected String[] parseUserRecipients(HttpServletRequest request) {
219        return parseCommaSeparatedValues(request, USER_RECIPS_PARAM);
220    }
221
222    protected String[] parseWorkgroupRecipients(HttpServletRequest request) {
223        return parseCommaSeparatedValues(request, WORKGROUP_RECIPS_PARAM);
224    }
225
226    protected String[] parseWorkgroupNamespaceCodes(HttpServletRequest request) {
227        return parseCommaSeparatedValues(request, WORKGROUP_NAMESPACE_CODES_PARAM);
228    }
229    
230    protected String[] parseCommaSeparatedValues(HttpServletRequest request, String param) {
231        String vals = request.getParameter(param);
232        if (vals != null) {
233            String[] split = vals.split(SPLIT_REGEX);
234            List<String> strs = new ArrayList<String>();
235            for (String component: split) {
236                if (StringUtils.isNotBlank(component)) {
237                    strs.add(component.trim());
238                }
239            }
240            return strs.toArray(new String[strs.size()]);
241        } else {
242            return new String[0];
243        }
244    }
245
246    protected boolean isUserRecipientValid(String user, ErrorList errors) {
247        boolean valid = true;
248        Principal principal = getIdentityService().getPrincipalByPrincipalName(user);
249        if (principal == null) {
250                valid = false;
251                errors.addError("'" + user + "' is not a valid principal name");
252        }
253
254        return valid;
255    }
256
257    protected boolean isWorkgroupRecipientValid(String groupName, String namespaceCode, ErrorList errors) {
258        Namespace nSpace = getNamespaceService().getNamespace(namespaceCode);
259        if (nSpace == null) {
260                errors.addError((new StringBuilder()).append('\'').append(namespaceCode).append("' is not a valid namespace code").toString());
261                return false;
262        } else {
263                Group i = getGroupService().getGroupByNamespaceCodeAndName(namespaceCode, groupName);
264                if (i == null) {
265                        errors.addError((new StringBuilder()).append('\'').append(groupName).append(
266                                        "' is not a valid group name for namespace code '").append(namespaceCode).append('\'').toString());
267                        return false;
268                } else {
269                        return true;
270                }
271        }
272    }
273    protected String getPrincipalIdFromIdOrName(String principalIdOrName) {
274        Principal principal = KimApiServiceLocator.getIdentityService().getPrincipal(principalIdOrName);
275        if (principal == null) {
276            principal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(principalIdOrName);
277        }
278        if (principal == null) {
279            throw new RiceIllegalArgumentException("Could not locate a principal as initiator with the given remoteUser of " + principalIdOrName);
280        }
281        return principal.getPrincipalId();
282    }
283
284    /**
285     * Submits the actual event notification message.
286     *
287     * @param request the servlet request
288     * @param routeMessage the message to attach to the route action
289     * @param viewName the name of the view to forward to after completion
290     *
291     * @return the next view to show
292     * @throws javax.servlet.ServletException
293     * @throws java.io.IOException
294     */
295    protected ModelAndView submitNotificationMessage(HttpServletRequest request, String routeMessage, String viewName)
296            throws ServletException, IOException {
297        LOG.debug("remoteUser: " + request.getRemoteUser());
298
299        // obtain a workflow user object first
300        //WorkflowIdDTO initiator = new WorkflowIdDTO(request.getRemoteUser());
301        String initiatorId = getPrincipalIdFromIdOrName( request.getRemoteUser());
302        LOG.debug("initiatorId: " + initiatorId);
303
304        // now construct the workflow document, which will interact with workflow
305        Map<String, Object> model = new HashMap<String, Object>();
306
307        try {
308            WorkflowDocument document = createNotificationWorkflowDocument(request, initiatorId, model);
309
310            document.route(routeMessage + initiatorId);
311
312            // This ain't pretty, but it gets the job done for now.
313            ErrorList el = new ErrorList();
314            el.addError("Notification(s) sent.");
315            model.put("errors", el);
316        } catch (ErrorList el) {
317            // route back to the send form again
318            Map<String, Object> model2 = setupModelForSendNotification(request);
319            model.putAll(model2);
320            model.put("errors", el);
321        } catch (Exception e) {
322            throw new RuntimeException(e);
323        }
324
325        return new ModelAndView(viewName, model);
326    }
327
328    /**
329     * Creates a notification {@link WorkflowDocument}.
330     *
331     * @param request the servlet request
332     * @param initiatorId the user sending the notification
333     * @param model the Spring MVC model
334     *
335     * @return a {@link WorkflowDocument} for the notification
336     * @throws java.lang.IllegalArgumentException
337     * @throws org.kuali.rice.ken.exception.ErrorList
338     */
339    protected WorkflowDocument createNotificationWorkflowDocument(HttpServletRequest request, String initiatorId,
340            Map<String, Object> model) throws IllegalArgumentException, ErrorList {
341        WorkflowDocument document = NotificationWorkflowDocument.createNotificationDocument(initiatorId,
342                NotificationConstants.KEW_CONSTANTS.SEND_NOTIFICATION_REQ_DOC_TYPE);
343
344        //parse out the application content into a Notification BO
345        NotificationBo notification = populateNotificationInstance(request, model);
346
347        // now get that content in an understandable XML format and pass into document
348        String notificationAsXml = notificationMessageContentService.generateNotificationMessage(notification);
349
350        Map<String, String> attrFields = new HashMap<String,String>();
351        List<NotificationChannelReviewerBo> reviewers = notification.getChannel().getReviewers();
352        int ui = 0;
353        int gi = 0;
354        for (NotificationChannelReviewerBo reviewer: reviewers) {
355            String prefix;
356            int index;
357            if (KimConstants.KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode().equals(reviewer.getReviewerType())) {
358                prefix = "user";
359                index = ui;
360                ui++;
361            } else if (KimConstants.KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode().equals(reviewer.getReviewerType())) {
362                prefix = "group";
363                index = gi;
364                gi++;
365            } else {
366                LOG.error("Invalid type for reviewer " + reviewer.getReviewerId() + ": " + reviewer.getReviewerType());
367                continue;
368            }
369            attrFields.put(prefix + index, reviewer.getReviewerId());
370        }
371        GenericAttributeContent gac = new GenericAttributeContent("channelReviewers");
372        document.setApplicationContent(notificationAsXml);
373        document.setAttributeContent("<attributeContent>" + gac.generateContent(attrFields) + "</attributeContent>");
374
375        document.setTitle(notification.getTitle());
376
377        return document;
378    }
379
380    /**
381     * Creates a new {@link NotificationBo} instance.
382     *
383     * @param request the servlet request
384     * @param model the Spring MVC model
385     *
386     * @return a new notification
387     * @throws java.lang.IllegalArgumentException
388     * @throws org.kuali.rice.ken.exception.ErrorList
389     */
390    protected NotificationBo populateNotificationInstance(HttpServletRequest request, Map<String, Object> model)
391            throws IllegalArgumentException, ErrorList {
392        return createNotification(request, model, new ErrorList());
393    }
394
395    /**
396     * Provides an overridable method in which to customize a created {@link NotificationBo} instance.
397     *
398     * @param request the servlet request
399     * @param model the Spring MVC model
400     * @param errors the error list
401     *
402     * @return a new notification
403     * @throws ErrorList
404     */
405    protected NotificationBo createNotification(HttpServletRequest request, Map<String, Object> model, ErrorList errors)
406            throws ErrorList {
407        String channelName = getChannelName(request, model, errors);
408        String priorityName = getParameter(request, "priorityName", model, errors, "You must choose a priority.");
409        String[] senders = getParameterList(request, "senderNames", model, errors, "You must enter at least one sender.");
410        String deliveryType = getDeliveryType(request, model, errors);
411
412        Date originalDate = getDate(request.getParameter("originalDateTime"), errors, "Original date is invalid.");
413
414        String sendDateTime = StringUtils.defaultIfBlank(request.getParameter("sendDateTime"), Util.getCurrentDateTime());
415        Date sendDate = getDate(sendDateTime, errors, "You specified an invalid Send Date/Time.  Please use the calendar picker.");
416        if (sendDate != null && sendDate.before(originalDate)) {
417            errors.addError("Send Date/Time cannot be in the past.");
418        }
419        model.put("sendDateTime", sendDateTime);
420
421        String autoRemoveDateTime = request.getParameter("autoRemoveDateTime");
422        Date removeDate = getDate(autoRemoveDateTime, errors, "You specified an invalid Auto-Remove Date/Time.  Please use the calendar picker.");
423        if (removeDate != null) {
424            if (removeDate.before(originalDate)) {
425                errors.addError("Auto-Remove Date/Time cannot be in the past.");
426            } else if (sendDate != null && removeDate.before(sendDate)) {
427                errors.addError("Auto-Remove Date/Time cannot be before the Send Date/Time.");
428            }
429        }
430        model.put("autoRemoveDateTime", autoRemoveDateTime);
431
432        // user recipient names
433        String[] userRecipients = parseUserRecipients(request);
434
435        // workgroup recipient names
436        String[] workgroupRecipients = parseWorkgroupRecipients(request);
437
438        // workgroup namespace codes
439        String[] workgroupNamespaceCodes = parseWorkgroupNamespaceCodes(request);
440
441        String title = getParameter(request, "title", model, errors, "You must fill in a title.");
442
443        // check to see if there were any errors
444        if (!errors.getErrors().isEmpty()) {
445            throw errors;
446        }
447
448        return createNotification(title, deliveryType, sendDate, removeDate, channelName, priorityName,
449                senders, userRecipients, workgroupRecipients, workgroupNamespaceCodes, errors);
450    }
451
452    private NotificationBo createNotification(String title, String deliveryType, Date sendDate, Date removeDate,
453            String channelName, String priorityName, String[] senders, String[] userRecipients,
454            String[] workgroupRecipients, String[] workgroupNamespaceCodes, ErrorList errors) throws ErrorList {
455        NotificationBo notification = new NotificationBo();
456        notification.setTitle(title);
457        notification.setDeliveryType(deliveryType);
458        notification.setSendDateTimeValue(new Timestamp(sendDate.getTime()));
459        notification.setAutoRemoveDateTimeValue(new Timestamp(removeDate.getTime()));
460
461        NotificationChannelBo channel = Util.retrieveFieldReference("channel", "name", channelName,
462                NotificationChannelBo.class, dataObjectService);
463        notification.setChannel(channel);
464
465        NotificationPriorityBo priority = Util.retrieveFieldReference("priority", "name", priorityName,
466                NotificationPriorityBo.class, dataObjectService);
467        notification.setPriority(priority);
468
469        NotificationProducerBo producer = Util.retrieveFieldReference("producer", "name",
470                NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME, NotificationProducerBo.class,
471                dataObjectService);
472        notification.setProducer(producer);
473
474        for (String senderName : senders) {
475            if (StringUtils.isEmpty(senderName)) {
476                errors.addError("A sender's name cannot be blank.");
477            } else {
478                NotificationSenderBo ns = new NotificationSenderBo();
479                ns.setSenderName(senderName.trim());
480                notification.addSender(ns);
481            }
482        }
483
484        if (userRecipients != null && userRecipients.length > 0) {
485            for (String userRecipientId : userRecipients) {
486                if (isUserRecipientValid(userRecipientId, errors)) {
487                    NotificationRecipientBo recipient = new NotificationRecipientBo();
488                    recipient.setRecipientType(KimConstants.KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
489                    recipient.setRecipientId(userRecipientId);
490                    notification.addRecipient(recipient);
491                }
492            }
493        }
494
495        if (workgroupRecipients != null && workgroupRecipients.length > 0) {
496            if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
497                if (workgroupNamespaceCodes.length == workgroupRecipients.length) {
498                    for (int i = 0; i < workgroupRecipients.length; i++) {
499                        if (isWorkgroupRecipientValid(workgroupRecipients[i], workgroupNamespaceCodes[i], errors)) {
500                            NotificationRecipientBo recipient = new NotificationRecipientBo();
501                            recipient.setRecipientType(KimConstants.KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
502                            recipient.setRecipientId(
503                                    getGroupService().getGroupByNamespaceCodeAndName(workgroupNamespaceCodes[i],
504                                            workgroupRecipients[i]).getId());
505                            notification.addRecipient(recipient);
506                        }
507                    }
508                } else {
509                    errors.addError("The number of groups must match the number of namespace codes");
510                }
511            } else {
512                errors.addError("You must specify a namespace code for every group name");
513            }
514        } else if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
515            errors.addError("You must specify a group name for every namespace code");
516        }
517
518        if (!recipientsExist(userRecipients, workgroupRecipients) && !hasPotentialRecipients(notification)) {
519            errors.addError("You must specify at least one user or group recipient.");
520        }
521
522        notification.setContent(NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_SIMPLE_OPEN
523                + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_OPEN
524                + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_CLOSE
525                + NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_CLOSE);
526
527        return notification;
528    }
529
530    private String getChannelName(HttpServletRequest request, Map<String, Object> model, ErrorList errors) {
531        String channelName = request.getParameter("channelName");
532
533        if (StringUtils.isEmpty(channelName) || StringUtils.equals(channelName, NONE_CHANNEL)) {
534            errors.addError("You must choose a channel.");
535        } else {
536            model.put("channelName", channelName);
537        }
538
539        return channelName;
540    }
541
542    private String getDeliveryType(HttpServletRequest request, Map<String, Object> model, ErrorList errors) {
543        String deliveryType = request.getParameter("deliveryType");
544
545        if (StringUtils.isNotEmpty(deliveryType)) {
546            if (deliveryType.equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
547                deliveryType = NotificationConstants.DELIVERY_TYPES.FYI;
548            } else {
549                deliveryType = NotificationConstants.DELIVERY_TYPES.ACK;
550            }
551            model.put("deliveryType", deliveryType);
552        } else {
553            errors.addError("You must choose a delivery type.");
554        }
555
556        return deliveryType;
557    }
558
559    /**
560     * Prepares the model used for sending the notification.
561     *
562     * @param request the servlet request
563     *
564     * @return the Spring MVC model
565     */
566    protected Map<String, Object> setupModelForSendNotification(HttpServletRequest request) {
567        Map<String, Object> model = new HashMap<String, Object>();
568
569        model.put("defaultSender", request.getRemoteUser());
570        model.put("channels", notificationChannelService.getAllNotificationChannels());
571        model.put("priorities", dataObjectService.findMatching(NotificationPriorityBo.class,
572                QueryByCriteria.Builder.create().build()).getResults());
573
574        // set sendDateTime to current datetime if not provided
575        String sendDateTime = request.getParameter("sendDateTime");
576        String currentDateTime = Util.getCurrentDateTime();
577        if (StringUtils.isEmpty(sendDateTime)) {
578            sendDateTime = currentDateTime;
579        }
580        model.put("sendDateTime", sendDateTime);
581
582        // retain the original date time or set to current if it was not in the request
583        if (request.getParameter("originalDateTime") == null) {
584            model.put("originalDateTime", currentDateTime);
585        } else {
586            model.put("originalDateTime", request.getParameter("originalDateTime"));
587        }
588
589        model.put("userRecipients", request.getParameter("userRecipients"));
590        model.put("workgroupRecipients", request.getParameter("workgroupRecipients"));
591        model.put("workgroupNamespaceCodes", request.getParameter("workgroupNamespaceCodes"));
592
593        return model;
594    }
595
596    /**
597     * Returns whether the specified time is considered "in the future", based on some reasonable threshold.
598     *
599     * @param time the time to test
600     *
601     * @return true if the specified time is considered "in the future", false otherwise
602     */
603    private boolean timeIsInTheFuture(long time) {
604        boolean future = (time - System.currentTimeMillis()) > REASONABLE_IMMEDIATE_TIME_THRESHOLD;
605        LOG.info("Time: " + new Date(time) + " is in the future? " + future);
606        return future;
607    }
608
609    /**
610     * Returns whether recipients exist either, from users or workgroups.
611     *
612     * @param userRecipients the list of user recipients
613     * @param workgroupRecipients the list of workgroup recipients
614     *
615     * @return true if there are any recipients, false otherwise
616     */
617    private boolean recipientsExist(String[] userRecipients, String[] workgroupRecipients) {
618        return (userRecipients != null && userRecipients.length > 0)
619            || (workgroupRecipients != null && workgroupRecipients.length > 0);
620    }
621
622    /**
623     * Returns whether the specified Notification can be reasonably expected to have recipients.
624     *
625     * This is determined on whether the channel has default recipients, is subscribable, and whether the send date time
626     * is far enough in the future to expect that if there are no subscribers, there may actually be some by the time
627     * the notification is sent.
628     * @param notification the notification to test
629     *
630     * @return whether the specified Notification can be reasonably expected to have recipients
631     */
632    private boolean hasPotentialRecipients(NotificationBo notification) {
633        LOG.info("notification channel " + notification.getChannel() + " is subscribable: " + notification.getChannel().isSubscribable());
634        return !notification.getChannel().getRecipientLists().isEmpty() ||
635               !notification.getChannel().getSubscriptions().isEmpty() ||
636                (notification.getChannel().isSubscribable() && timeIsInTheFuture(notification.getSendDateTimeValue().getTime()));
637    }
638}