View Javadoc

1   /*
2    * Copyright 2007 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 java.io.IOException;
19  import java.sql.Timestamp;
20  import java.text.ParseException;
21  import java.util.Date;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  
26  import javax.servlet.ServletException;
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  
30  import org.apache.commons.lang.StringUtils;
31  import org.apache.log4j.Logger;
32  import org.kuali.rice.core.dao.GenericDao;
33  import org.kuali.rice.ken.bo.Notification;
34  import org.kuali.rice.ken.bo.NotificationChannel;
35  import org.kuali.rice.ken.bo.NotificationChannelReviewer;
36  import org.kuali.rice.ken.bo.NotificationContentType;
37  import org.kuali.rice.ken.bo.NotificationPriority;
38  import org.kuali.rice.ken.bo.NotificationProducer;
39  import org.kuali.rice.ken.bo.NotificationRecipient;
40  import org.kuali.rice.ken.bo.NotificationSender;
41  import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument;
42  import org.kuali.rice.ken.exception.ErrorList;
43  import org.kuali.rice.ken.service.NotificationChannelService;
44  import org.kuali.rice.ken.service.NotificationMessageContentService;
45  import org.kuali.rice.ken.service.NotificationRecipientService;
46  import org.kuali.rice.ken.service.NotificationService;
47  import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
48  import org.kuali.rice.ken.util.NotificationConstants;
49  import org.kuali.rice.ken.util.Util;
50  import org.kuali.rice.kew.dto.WorkflowIdDTO;
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 
235 	// now construct the workflow document, which will interact with workflow
236 	NotificationWorkflowDocument document;
237 	Map<String, Object> model = new HashMap<String, Object>();
238         String view;
239 	try {
240 	    document = new NotificationWorkflowDocument(
241 		    initiator,
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.getDocumentContent().setApplicationContent(notificationAsXml);
274             document.getDocumentContent().setAttributeContent("<attributeContent>" + gac.generateContent(attrFields) + "</attributeContent>");
275 
276             document.setTitle(notification.getTitle());
277 
278 	    document.routeDocument("This message was submitted via the event notification message submission form by user "
279 			    + initiator.getWorkflowId());
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);
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);
535 	    				recipient.setRecipientId(
536 	    						getIdentityManagementService().getGroupByName(workgroupNamespaceCodes[i], workgroupRecipients[i]).getGroupId());
537 	    				notification.addRecipient(recipient);
538 	    			}
539 	    		}
540 	    	} else {
541 	    		errors.addError("The number of groups must match the number of namespace codes");
542 	    	}
543 	    } else {
544 			errors.addError("You must specify a namespace code for every group name");
545 		}
546 	} else if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
547 		errors.addError("You must specify a group name for every namespace code");
548 	}
549 
550 	// check to see if there were any errors
551 	if (errors.getErrors().size() > 0) {
552 	    throw errors;
553 	}
554 
555         notification.setTitle(title);
556 
557 	notification.setDeliveryType(deliveryType);
558 
559         Date startDate = null;
560         Date stopDate = null;
561 	// simpledateformat is not threadsafe, have to sync and validate
562         Date d = null;
563         if (StringUtils.isNotBlank(sendDateTime)) {
564             try {
565                 d = Util.parseUIDateTime(sendDateTime);
566             } catch (ParseException pe) {
567                 errors.addError("You specified an invalid send date and time.  Please use the calendar picker.");
568             }
569             notification.setSendDateTime(new Timestamp(d.getTime()));
570         }
571 
572         Date d2 = null;
573         if (StringUtils.isNotBlank(autoRemoveDateTime)) {
574             try {
575                 d2 = Util.parseUIDateTime(autoRemoveDateTime);
576                 if (d2.before(d)) {
577                     errors.addError("Auto Remove Date/Time cannot be before Send Date/Time.");
578                 }
579             } catch (ParseException pe) {
580                 errors.addError("You specified an invalid auto-remove date and time.  Please use the calendar picker.");
581             }
582             notification.setAutoRemoveDateTime(new Timestamp(d2.getTime()));
583         }
584 
585         if (StringUtils.isNotBlank(startDateTime)) {
586             try {
587                 startDate = Util.parseUIDateTime(startDateTime);
588             } catch (ParseException pe) {
589                 errors.addError("You specified an invalid start date and time.  Please use the calendar picker.");
590             }
591         }
592 
593         if (StringUtils.isNotBlank(stopDateTime)) {
594             try {
595                 stopDate = Util.parseUIDateTime(stopDateTime);
596             } catch (ParseException pe) {
597                 errors.addError("You specified an invalid stop date and time.  Please use the calendar picker.");
598             }
599         }
600 
601         if(stopDate != null && startDate != null) {
602             if (stopDate.before(startDate)) {
603                 errors.addError("Event Stop Date/Time cannot be before Event Start Date/Time.");
604             }
605         }
606 
607         if (!recipientsExist && !hasPotentialRecipients(notification)) {
608             errors.addError("You must specify at least one user or group recipient.");
609         }
610 
611 	// check to see if there were any errors
612 	if (errors.getErrors().size() > 0) {
613 	    throw errors;
614 	}
615 
616 	notification
617 		.setContent(NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_EVENT_OPEN
618 			+ NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_OPEN
619 			+ message
620 			+ NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_CLOSE
621                         + "<event>\n"
622                         + "  <summary>" + summary + "</summary>\n"
623                         + "  <description>" + description + "</description>\n"
624                         + "  <location>" + location + "</location>\n"
625                         + "  <startDateTime>" + Util.toUIDateTimeString(startDate) + "</startDateTime>\n"
626                         + "  <stopDateTime>" + Util.toUIDateTimeString(stopDate) + "</stopDateTime>\n"
627                         + "</event>"
628 			+ NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_CLOSE);
629 
630 	return notification;
631     }
632 }