View Javadoc

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