View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.ken.web.spring;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
21  import org.kuali.rice.core.framework.persistence.dao.GenericDao;
22  import org.kuali.rice.ken.bo.NotificationBo;
23  import org.kuali.rice.ken.bo.NotificationChannelBo;
24  import org.kuali.rice.ken.bo.NotificationChannelReviewerBo;
25  import org.kuali.rice.ken.bo.NotificationContentTypeBo;
26  import org.kuali.rice.ken.bo.NotificationPriorityBo;
27  import org.kuali.rice.ken.bo.NotificationProducerBo;
28  import org.kuali.rice.ken.bo.NotificationRecipientBo;
29  import org.kuali.rice.ken.bo.NotificationSenderBo;
30  import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument;
31  import org.kuali.rice.ken.exception.ErrorList;
32  import org.kuali.rice.ken.service.NotificationChannelService;
33  import org.kuali.rice.ken.service.NotificationMessageContentService;
34  import org.kuali.rice.ken.service.NotificationRecipientService;
35  import org.kuali.rice.ken.service.NotificationService;
36  import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
37  import org.kuali.rice.ken.util.NotificationConstants;
38  import org.kuali.rice.ken.util.Util;
39  import org.kuali.rice.kew.api.WorkflowDocument;
40  import org.kuali.rice.kew.rule.GenericAttributeContent;
41  import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
42  import org.kuali.rice.kim.api.identity.principal.Principal;
43  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
44  import org.springframework.web.servlet.ModelAndView;
45  
46  import javax.servlet.ServletException;
47  import javax.servlet.http.HttpServletRequest;
48  import javax.servlet.http.HttpServletResponse;
49  import java.io.IOException;
50  import java.sql.Timestamp;
51  import java.text.ParseException;
52  import java.util.Date;
53  import java.util.HashMap;
54  import java.util.List;
55  import java.util.Map;
56  
57  /**
58   * This class is the controller for sending Simple notification messages via an end user interface.
59   * @author Kuali Rice Team (rice.collab@kuali.org)
60   */
61  public class SendNotificationMessageController extends BaseSendNotificationController {
62      /** Logger for this class and subclasses */
63      private static final Logger LOG = Logger
64              .getLogger(SendNotificationMessageController.class);
65  
66      private static final String NONE_CHANNEL = "___NONE___";
67      private static final long REASONABLE_IMMEDIATE_TIME_THRESHOLD = 1000 * 60 * 5; // <= 5 minutes is "immediate"
68  
69      /**
70       * Returns whether the specified time is considered "in the future", based on some reasonable
71       * threshold
72       * @param time the time to test
73       * @return whether the specified time is considered "in the future", based on some reasonable
74       *         threshold
75       */
76      private boolean timeIsInTheFuture(long time) {
77          boolean future = (time - System.currentTimeMillis()) > REASONABLE_IMMEDIATE_TIME_THRESHOLD;
78          LOG.info("Time: " + new Date(time) + " is in the future? " + future);
79          return future;
80      }
81  
82      /**
83       * Returns whether the specified Notification can be reasonably expected to have recipients.
84       * This is determined on whether the channel has default recipients, is subscribably, and
85       * whether the send date time is far enough in the future to expect that if there are no
86       * subscribers, there may actually be some by the time the notification is sent.
87       * @param notification the notification to test
88       * @return whether the specified Notification can be reasonably expected to have recipients
89       */
90      private boolean hasPotentialRecipients(NotificationBo notification) {
91          LOG.info("notification channel " + notification.getChannel() + " is subscribable: "
92                  + notification.getChannel().isSubscribable());
93          return notification.getChannel().getRecipientLists().size() > 0
94                  ||
95                  notification.getChannel().getSubscriptions().size() > 0
96                  ||
97                  (notification.getChannel().isSubscribable() && timeIsInTheFuture(notification.getSendDateTimeValue()
98                          .getTime()));
99      }
100 
101     protected NotificationService notificationService;
102 
103     protected NotificationWorkflowDocumentService notificationWorkflowDocService;
104 
105     protected NotificationChannelService notificationChannelService;
106 
107     protected NotificationRecipientService notificationRecipientService;
108 
109     protected NotificationMessageContentService messageContentService;
110 
111     protected GenericDao businessObjectDao;
112 
113     /**
114      * Set the NotificationService
115      * @param notificationService
116      */
117     public void setNotificationService(NotificationService notificationService) {
118         this.notificationService = notificationService;
119     }
120 
121     /**
122      * This method sets the NotificationWorkflowDocumentService
123      * @param s
124      */
125     public void setNotificationWorkflowDocumentService(
126             NotificationWorkflowDocumentService s) {
127         this.notificationWorkflowDocService = s;
128     }
129 
130     /**
131      * Sets the notificationChannelService attribute value.
132      * @param notificationChannelService The notificationChannelService to set.
133      */
134     public void setNotificationChannelService(
135             NotificationChannelService notificationChannelService) {
136         this.notificationChannelService = notificationChannelService;
137     }
138 
139     /**
140      * Sets the notificationRecipientService attribute value.
141      * @param notificationRecipientService
142      */
143     public void setNotificationRecipientService(
144             NotificationRecipientService notificationRecipientService) {
145         this.notificationRecipientService = notificationRecipientService;
146     }
147 
148     /**
149      * Sets the messageContentService attribute value.
150      * @param messageContentService
151      */
152     public void setMessageContentService(
153             NotificationMessageContentService notificationMessageContentService) {
154         this.messageContentService = notificationMessageContentService;
155     }
156 
157     /**
158      * Sets the businessObjectDao attribute value.
159      * @param businessObjectDao The businessObjectDao to set.
160      */
161     public void setBusinessObjectDao(GenericDao businessObjectDao) {
162         this.businessObjectDao = businessObjectDao;
163     }
164 
165     /**
166      * Handles the display of the form for sending a simple notification message
167      * @param request : a servlet request
168      * @param response : a servlet response
169      * @throws ServletException : an exception
170      * @throws IOException : an exception
171      * @return a ModelAndView object
172      */
173     public ModelAndView sendSimpleNotificationMessage(
174             HttpServletRequest request, HttpServletResponse response)
175             throws ServletException, IOException {
176         String view = "SendSimpleNotificationMessage";
177 
178         LOG.debug("remoteUser: " + request.getRemoteUser());
179 
180         Map<String, Object> model = setupModelForSendSimpleNotification(request);
181         model.put("errors", new ErrorList()); // need an empty one so we don't have an NPE
182 
183         return new ModelAndView(view, model);
184     }
185 
186     /**
187      * This method prepares the model used for the send simple notification message form.
188      * @param request
189      * @return Map<String, Object>
190      */
191     private Map<String, Object> setupModelForSendSimpleNotification(
192             HttpServletRequest request) {
193         Map<String, Object> model = new HashMap<String, Object>();
194         model.put("defaultSender", request.getRemoteUser());
195         model.put("channels", notificationChannelService
196                 .getAllNotificationChannels());
197         model.put("priorities", businessObjectDao
198                 .findAll(NotificationPriorityBo.class));
199         // set sendDateTime to current datetime if not provided
200         String sendDateTime = request.getParameter("sendDateTime");
201         String currentDateTime = Util.getCurrentDateTime();
202         if (StringUtils.isEmpty(sendDateTime)) {
203             sendDateTime = currentDateTime;
204         }
205         model.put("sendDateTime", sendDateTime);
206 
207         // retain the original date time or set to current if
208         // it was not in the request
209         if (request.getParameter("originalDateTime") == null) {
210             model.put("originalDateTime", currentDateTime);
211         } else {
212             model.put("originalDateTime", request.getParameter("originalDateTime"));
213         }
214 
215         model.put("userRecipients", request.getParameter("userRecipients"));
216         model.put("workgroupRecipients", request.getParameter("workgroupRecipients"));
217         model.put("workgroupNamespaceCodes", request.getParameter("workgroupNamespaceCodes"));
218 
219         return model;
220     }
221 
222     /**
223      * This method handles submitting the actual simple notification message.
224      * @param request
225      * @param response
226      * @return ModelAndView
227      * @throws ServletException
228      * @throws IOException
229      */
230     public ModelAndView submitSimpleNotificationMessage(
231             HttpServletRequest request, HttpServletResponse response)
232             throws ServletException, IOException {
233         LOG.debug("remoteUser: " + request.getRemoteUser());
234 
235         // obtain a workflow user object first
236         //WorkflowIdDTO initiator = new WorkflowIdDTO(request.getRemoteUser());
237         String initiatorId = getPrincipalIdFromIdOrName( request.getRemoteUser());
238         LOG.debug("initiatorId="+initiatorId);
239 
240         // now construct the workflow document, which will interact with workflow
241         WorkflowDocument document;
242         Map<String, Object> model = new HashMap<String, Object>();
243         String view;
244         try {
245             document = NotificationWorkflowDocument.createNotificationDocument(
246                     initiatorId,
247                     NotificationConstants.KEW_CONSTANTS.SEND_NOTIFICATION_REQ_DOC_TYPE);
248 
249             //parse out the application content into a Notification BO
250             NotificationBo notification = populateNotificationInstance(request,
251                     model);
252 
253             // now get that content in an understandable XML format and pass into document
254             String notificationAsXml = messageContentService
255                     .generateNotificationMessage(notification);
256 
257             Map<String, String> attrFields = new HashMap<String, String>();
258             List<NotificationChannelReviewerBo> reviewers = notification.getChannel().getReviewers();
259             int ui = 0;
260             int gi = 0;
261             for (NotificationChannelReviewerBo reviewer : reviewers) {
262                 String prefix;
263                 int index;
264                 if (KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
265                     prefix = "user";
266                     index = ui;
267                     ui++;
268                 } else if (KimGroupMemberTypes.GROUP_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
269                     prefix = "group";
270                     index = gi;
271                     gi++;
272                 } else {
273                     LOG.error("Invalid type for reviewer " + reviewer.getReviewerId() + ": "
274                             + reviewer.getReviewerType());
275                     continue;
276                 }
277                 attrFields.put(prefix + index, reviewer.getReviewerId());
278             }
279             GenericAttributeContent gac = new GenericAttributeContent("channelReviewers");
280             document.setApplicationContent(notificationAsXml);
281             document.setAttributeContent("<attributeContent>" + gac.generateContent(attrFields) + "</attributeContent>");
282 
283             document.setTitle(notification.getTitle());
284 
285             document.route("This message was submitted via the simple notification message submission form by user "
286                     + initiatorId);
287 
288             view = "SendSimpleNotificationMessage";
289 
290             // This ain't pretty, but it gets the job done for now.
291             ErrorList el = new ErrorList();
292             el.addError("Notification(s) sent.");
293             model.put("errors", el);
294 
295         } catch (ErrorList el) {
296             // route back to the send form again
297             Map<String, Object> model2 = setupModelForSendSimpleNotification(request);
298             model.putAll(model2);
299             model.put("errors", el);
300 
301             view = "SendSimpleNotificationMessage";
302         } catch (Exception e) {
303             throw new RuntimeException(e);
304         }
305 
306         return new ModelAndView(view, model);
307     }
308 
309     /**
310      * This method creates a new Notification instance from the form values.
311      * @param request
312      * @param model
313      * @return Notification
314      * @throws IllegalArgumentException
315      */
316     private NotificationBo populateNotificationInstance(
317             HttpServletRequest request, Map<String, Object> model)
318             throws IllegalArgumentException, ErrorList {
319         ErrorList errors = new ErrorList();
320 
321         NotificationBo notification = new NotificationBo();
322 
323         // grab data from form
324         // channel name
325         String channelName = request.getParameter("channelName");
326         if (StringUtils.isEmpty(channelName) || StringUtils.equals(channelName, NONE_CHANNEL)) {
327             errors.addError("You must choose a channel.");
328         } else {
329             model.put("channelName", channelName);
330         }
331 
332         // priority name
333         String priorityName = request.getParameter("priorityName");
334         if (StringUtils.isEmpty(priorityName)) {
335             errors.addError("You must choose a priority.");
336         } else {
337             model.put("priorityName", priorityName);
338         }
339 
340         // sender names
341         String senderNames = request.getParameter("senderNames");
342         String[] senders = null;
343         if (StringUtils.isEmpty(senderNames)) {
344             errors.addError("You must enter at least one sender.");
345         } else {
346             senders = StringUtils.split(senderNames, ",");
347 
348             model.put("senderNames", senderNames);
349         }
350 
351         // delivery type
352         String deliveryType = request.getParameter("deliveryType");
353         if (StringUtils.isEmpty(deliveryType)) {
354             errors.addError("You must choose a type.");
355         } else {
356             if (deliveryType
357                     .equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
358                 deliveryType = NotificationConstants.DELIVERY_TYPES.FYI;
359             } else {
360                 deliveryType = NotificationConstants.DELIVERY_TYPES.ACK;
361             }
362             model.put("deliveryType", deliveryType);
363         }
364 
365         // get datetime when form was initially rendered
366         String originalDateTime = request.getParameter("originalDateTime");
367         Date origdate = null;
368         Date senddate = null;
369         Date removedate = null;
370         try {
371             origdate = Util.parseUIDateTime(originalDateTime);
372         } catch (ParseException pe) {
373             errors.addError("Original date is invalid.");
374         }
375         // send date time
376         String sendDateTime = request.getParameter("sendDateTime");
377         if (StringUtils.isBlank(sendDateTime)) {
378             sendDateTime = Util.getCurrentDateTime();
379         }
380 
381         try {
382             senddate = Util.parseUIDateTime(sendDateTime);
383         } catch (ParseException pe) {
384             errors.addError("You specified an invalid Send Date/Time.  Please use the calendar picker.");
385         }
386 
387         if (senddate != null && senddate.before(origdate)) {
388             errors.addError("Send Date/Time cannot be in the past.");
389         }
390 
391         model.put("sendDateTime", sendDateTime);
392 
393         // auto remove date time
394         String autoRemoveDateTime = request.getParameter("autoRemoveDateTime");
395         if (StringUtils.isNotBlank(autoRemoveDateTime)) {
396             try {
397                 removedate = Util.parseUIDateTime(autoRemoveDateTime);
398             } catch (ParseException pe) {
399                 errors.addError("You specified an invalid Auto-Remove Date/Time.  Please use the calendar picker.");
400             }
401 
402             if (removedate != null) {
403                 if (removedate.before(origdate)) {
404                     errors.addError("Auto-Remove Date/Time cannot be in the past.");
405                 } else if (senddate != null && removedate.before(senddate)) {
406                     errors.addError("Auto-Remove Date/Time cannot be before the Send Date/Time.");
407                 }
408             }
409         }
410 
411         model.put("autoRemoveDateTime", autoRemoveDateTime);
412 
413         // user recipient names
414         String[] userRecipients = parseUserRecipients(request);
415 
416         // workgroup recipient names
417         String[] workgroupRecipients = parseWorkgroupRecipients(request);
418 
419         // workgroup namespace codes
420         String[] workgroupNamespaceCodes = parseWorkgroupNamespaceCodes(request);
421 
422         // title
423         String title = request.getParameter("title");
424         if (!StringUtils.isEmpty(title)) {
425             model.put("title", title);
426         } else {
427             errors.addError("You must fill in a title");
428         }
429 
430         // message
431         String message = request.getParameter("message");
432         if (StringUtils.isEmpty(message)) {
433             errors.addError("You must fill in a message.");
434         } else {
435             model.put("message", message);
436         }
437 
438         // stop processing if there are errors
439         if (errors.getErrors().size() > 0) {
440             throw errors;
441         }
442 
443         // now populate the notification BO instance
444         NotificationChannelBo channel = Util.retrieveFieldReference("channel",
445                 "name", channelName, NotificationChannelBo.class,
446                 businessObjectDao);
447         notification.setChannel(channel);
448 
449         NotificationPriorityBo priority = Util.retrieveFieldReference("priority",
450                 "name", priorityName, NotificationPriorityBo.class,
451                 businessObjectDao);
452         notification.setPriority(priority);
453 
454         NotificationContentTypeBo contentType = Util.retrieveFieldReference(
455                 "contentType", "name",
456                 NotificationConstants.CONTENT_TYPES.SIMPLE_CONTENT_TYPE,
457                 NotificationContentTypeBo.class, businessObjectDao);
458         notification.setContentType(contentType);
459 
460         NotificationProducerBo producer = Util
461                 .retrieveFieldReference(
462                         "producer",
463                         "name",
464                         NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME,
465                         NotificationProducerBo.class, businessObjectDao);
466         notification.setProducer(producer);
467 
468         for (String senderName : senders) {
469             if (StringUtils.isEmpty(senderName)) {
470                 errors.addError("A sender's name cannot be blank.");
471             } else {
472                 NotificationSenderBo ns = new NotificationSenderBo();
473                 ns.setSenderName(senderName.trim());
474                 notification.addSender(ns);
475             }
476         }
477 
478         boolean recipientsExist = false;
479 
480         if (userRecipients != null && userRecipients.length > 0) {
481             recipientsExist = true;
482             for (String userRecipientId : userRecipients) {
483                 if (isUserRecipientValid(userRecipientId, errors)) {
484                     NotificationRecipientBo recipient = new NotificationRecipientBo();
485                     recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
486                     recipient.setRecipientId(userRecipientId);
487                     notification.addRecipient(recipient);
488                 }
489             }
490         }
491 
492         if (workgroupRecipients != null && workgroupRecipients.length > 0) {
493             recipientsExist = true;
494             if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
495                 if (workgroupNamespaceCodes.length == workgroupRecipients.length) {
496                     for (int i = 0; i < workgroupRecipients.length; i++) {
497                         if (isWorkgroupRecipientValid(workgroupRecipients[i], workgroupNamespaceCodes[i], errors)) {
498                             NotificationRecipientBo recipient = new NotificationRecipientBo();
499                             recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
500                             recipient.setRecipientId(
501                                     getGroupService().getGroupByNamespaceCodeAndName(workgroupNamespaceCodes[i],
502                                             workgroupRecipients[i]).getId());
503                             notification.addRecipient(recipient);
504                         }
505                     }
506                 } else {
507                     errors.addError("The number of groups must match the number of namespace codes");
508                 }
509             } else {
510                 errors.addError("You must specify a namespace code for every group name");
511             }
512         } else if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
513             errors.addError("You must specify a group name for every namespace code");
514         }
515 
516         // check to see if there were any errors
517         if (errors.getErrors().size() > 0) {
518             throw errors;
519         }
520 
521         notification.setTitle(title);
522 
523         notification.setDeliveryType(deliveryType);
524 
525         // simpledateformat is not threadsafe, have to sync and validate
526         Date d = null;
527         if (StringUtils.isNotBlank(sendDateTime)) {
528             try {
529                 d = Util.parseUIDateTime(sendDateTime);
530             } catch (ParseException pe) {
531                 errors.addError("You specified an invalid send date and time.  Please use the calendar picker.");
532             }
533             notification.setSendDateTimeValue(new Timestamp(d.getTime()));
534         }
535 
536         Date d2 = null;
537         if (StringUtils.isNotBlank(autoRemoveDateTime)) {
538             try {
539                 d2 = Util.parseUIDateTime(autoRemoveDateTime);
540                 if (d2.before(d)) {
541                     errors.addError("Auto Remove Date/Time cannot be before Send Date/Time.");
542                 }
543             } catch (ParseException pe) {
544                 errors.addError("You specified an invalid auto remove date and time.  Please use the calendar picker.");
545             }
546             notification.setAutoRemoveDateTimeValue(new Timestamp(d2.getTime()));
547         }
548 
549         if (!recipientsExist && !hasPotentialRecipients(notification)) {
550             errors.addError("You must specify at least one user or group recipient.");
551         }
552 
553         // check to see if there were any errors
554         if (errors.getErrors().size() > 0) {
555             throw errors;
556         }
557 
558         notification
559                 .setContent(NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_SIMPLE_OPEN
560                         + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_OPEN
561                         + message
562                         + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_CLOSE
563                         + NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_CLOSE);
564 
565         return notification;
566     }
567 }