001    /**
002     * Copyright 2005-2014 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     */
016    package org.kuali.rice.ken.web.spring;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.log4j.Logger;
020    import org.kuali.rice.core.api.criteria.QueryByCriteria;
021    import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
022    import org.kuali.rice.core.framework.persistence.dao.GenericDao;
023    import org.kuali.rice.coreservice.api.namespace.Namespace;
024    import org.kuali.rice.coreservice.api.namespace.NamespaceService;
025    import org.kuali.rice.ken.bo.NotificationBo;
026    import org.kuali.rice.ken.bo.NotificationChannelBo;
027    import org.kuali.rice.ken.bo.NotificationChannelReviewerBo;
028    import org.kuali.rice.ken.bo.NotificationPriorityBo;
029    import org.kuali.rice.ken.bo.NotificationProducerBo;
030    import org.kuali.rice.ken.bo.NotificationRecipientBo;
031    import org.kuali.rice.ken.bo.NotificationSenderBo;
032    import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument;
033    import org.kuali.rice.ken.exception.ErrorList;
034    import org.kuali.rice.ken.service.NotificationChannelService;
035    import org.kuali.rice.ken.service.NotificationMessageContentService;
036    import org.kuali.rice.ken.service.NotificationRecipientService;
037    import org.kuali.rice.ken.service.NotificationService;
038    import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
039    import org.kuali.rice.ken.util.NotificationConstants;
040    import org.kuali.rice.ken.util.Util;
041    import org.kuali.rice.kew.api.WorkflowDocument;
042    import org.kuali.rice.kew.rule.GenericAttributeContent;
043    import org.kuali.rice.kim.api.KimConstants;
044    import org.kuali.rice.kim.api.group.Group;
045    import org.kuali.rice.kim.api.group.GroupService;
046    import org.kuali.rice.kim.api.identity.IdentityService;
047    import org.kuali.rice.kim.api.identity.principal.Principal;
048    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
049    import org.kuali.rice.coreservice.api.CoreServiceApiServiceLocator;
050    import org.kuali.rice.krad.data.DataObjectService;
051    import org.springframework.web.servlet.ModelAndView;
052    import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
053    
054    import javax.servlet.ServletException;
055    import javax.servlet.http.HttpServletRequest;
056    import java.io.IOException;
057    import java.sql.Timestamp;
058    import java.text.ParseException;
059    import java.util.ArrayList;
060    import java.util.Date;
061    import java.util.HashMap;
062    import java.util.List;
063    import 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     */
071    public 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    }