View Javadoc

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