001 /** 002 * Copyright 2005-2014 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.rice.ken.web.spring; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.apache.log4j.Logger; 020 import org.kuali.rice.core.api.exception.RiceIllegalArgumentException; 021 import org.kuali.rice.core.framework.persistence.dao.GenericDao; 022 import org.kuali.rice.ken.bo.NotificationBo; 023 import org.kuali.rice.ken.bo.NotificationChannelBo; 024 import org.kuali.rice.ken.bo.NotificationChannelReviewerBo; 025 import org.kuali.rice.ken.bo.NotificationContentTypeBo; 026 import org.kuali.rice.ken.bo.NotificationPriorityBo; 027 import org.kuali.rice.ken.bo.NotificationProducerBo; 028 import org.kuali.rice.ken.bo.NotificationRecipientBo; 029 import org.kuali.rice.ken.bo.NotificationSenderBo; 030 import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument; 031 import org.kuali.rice.ken.exception.ErrorList; 032 import org.kuali.rice.ken.service.NotificationChannelService; 033 import org.kuali.rice.ken.service.NotificationMessageContentService; 034 import org.kuali.rice.ken.service.NotificationRecipientService; 035 import org.kuali.rice.ken.service.NotificationService; 036 import org.kuali.rice.ken.service.NotificationWorkflowDocumentService; 037 import org.kuali.rice.ken.util.NotificationConstants; 038 import org.kuali.rice.ken.util.Util; 039 import org.kuali.rice.kew.api.WorkflowDocument; 040 import org.kuali.rice.kew.rule.GenericAttributeContent; 041 import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes; 042 import org.kuali.rice.kim.api.identity.principal.Principal; 043 import org.kuali.rice.kim.api.services.KimApiServiceLocator; 044 import org.springframework.web.servlet.ModelAndView; 045 046 import javax.servlet.ServletException; 047 import javax.servlet.http.HttpServletRequest; 048 import javax.servlet.http.HttpServletResponse; 049 import java.io.IOException; 050 import java.sql.Timestamp; 051 import java.text.ParseException; 052 import java.util.Date; 053 import java.util.HashMap; 054 import java.util.List; 055 import java.util.Map; 056 057 058 /** 059 * This class is the controller for sending Event notification messages via an end user interface. 060 * @author Kuali Rice Team (rice.collab@kuali.org) 061 */ 062 public class SendEventNotificationMessageController extends BaseSendNotificationController { 063 /** Logger for this class and subclasses */ 064 private static final Logger LOG = Logger 065 .getLogger(SendEventNotificationMessageController.class); 066 067 private static final String NONE_CHANNEL = "___NONE___"; 068 private static final long REASONABLE_IMMEDIATE_TIME_THRESHOLD = 1000 * 60 * 5; // <= 5 minutes is "immediate" 069 070 /** 071 * Returns whether the specified time is considered "in the future", based on some reasonable threshold 072 * @param time the time to test 073 * @return whether the specified time is considered "in the future", based on some reasonable threshold 074 */ 075 private boolean timeIsInTheFuture(long time) { 076 boolean future = (time - System.currentTimeMillis()) > REASONABLE_IMMEDIATE_TIME_THRESHOLD; 077 LOG.info("Time: " + new Date(time) + " is in the future? " + future); 078 return future; 079 } 080 081 /** 082 * Returns whether the specified Notification can be reasonably expected to have recipients. 083 * This is determined on whether the channel has default recipients, is subscribably, and whether 084 * the send date time is far enough in the future to expect that if there are no subscribers, there 085 * may actually be some by the time the notification is sent. 086 * @param notification the notification to test 087 * @return whether the specified Notification can be reasonably expected to have recipients 088 */ 089 private boolean hasPotentialRecipients(NotificationBo notification) { 090 LOG.info("notification channel " + notification.getChannel() + " is subscribable: " + notification.getChannel().isSubscribable()); 091 return notification.getChannel().getRecipientLists().size() > 0 || 092 notification.getChannel().getSubscriptions().size() > 0 || 093 (notification.getChannel().isSubscribable() && timeIsInTheFuture(notification.getSendDateTimeValue().getTime())); 094 } 095 096 protected NotificationService notificationService; 097 098 protected NotificationWorkflowDocumentService notificationWorkflowDocService; 099 100 protected NotificationChannelService notificationChannelService; 101 102 protected NotificationRecipientService notificationRecipientService; 103 104 protected NotificationMessageContentService messageContentService; 105 106 protected GenericDao businessObjectDao; 107 108 /** 109 * Set the NotificationService 110 * @param notificationService 111 */ 112 public void setNotificationService(NotificationService notificationService) { 113 this.notificationService = notificationService; 114 } 115 116 /** 117 * This method sets the NotificationWorkflowDocumentService 118 * @param s 119 */ 120 public void setNotificationWorkflowDocumentService( 121 NotificationWorkflowDocumentService s) { 122 this.notificationWorkflowDocService = s; 123 } 124 125 /** 126 * Sets the notificationChannelService attribute value. 127 * @param notificationChannelService The notificationChannelService to set. 128 */ 129 public void setNotificationChannelService( 130 NotificationChannelService notificationChannelService) { 131 this.notificationChannelService = notificationChannelService; 132 } 133 134 /** 135 * Sets the notificationRecipientService attribute value. 136 * @param notificationRecipientService 137 */ 138 public void setNotificationRecipientService( 139 NotificationRecipientService notificationRecipientService) { 140 this.notificationRecipientService = notificationRecipientService; 141 } 142 143 /** 144 * Sets the messageContentService attribute value. 145 * @param messageContentService 146 */ 147 public void setMessageContentService( 148 NotificationMessageContentService notificationMessageContentService) { 149 this.messageContentService = notificationMessageContentService; 150 } 151 152 /** 153 * Sets the businessObjectDao attribute value. 154 * @param businessObjectDao The businessObjectDao to set. 155 */ 156 public void setBusinessObjectDao(GenericDao businessObjectDao) { 157 this.businessObjectDao = businessObjectDao; 158 } 159 160 /** 161 * Handles the display of the form for sending an event notification message 162 * @param request : a servlet request 163 * @param response : a servlet response 164 * @throws ServletException : an exception 165 * @throws IOException : an exception 166 * @return a ModelAndView object 167 */ 168 public ModelAndView sendEventNotificationMessage( 169 HttpServletRequest request, HttpServletResponse response) 170 throws ServletException, IOException { 171 String view = "SendEventNotificationMessage"; 172 LOG.debug("remoteUser: " + request.getRemoteUser()); 173 174 Map<String, Object> model = setupModelForSendEventNotification(request); 175 model.put("errors", new ErrorList()); // need an empty one so we don't have an NPE 176 177 return new ModelAndView(view, model); 178 } 179 180 /** 181 * This method prepares the model used for the send event notification message form. 182 * @param request 183 * @return Map<String, Object> 184 */ 185 private Map<String, Object> setupModelForSendEventNotification( 186 HttpServletRequest request) { 187 Map<String, Object> model = new HashMap<String, Object>(); 188 model.put("defaultSender", request.getRemoteUser()); 189 model.put("channels", notificationChannelService 190 .getAllNotificationChannels()); 191 model.put("priorities", businessObjectDao 192 .findAll(NotificationPriorityBo.class)); 193 // set sendDateTime to current datetime if not provided 194 String sendDateTime = request.getParameter("sendDateTime"); 195 String currentDateTime = Util.getCurrentDateTime(); 196 if (StringUtils.isEmpty(sendDateTime)) { 197 sendDateTime = currentDateTime; 198 } 199 model.put("sendDateTime", sendDateTime); 200 201 // retain the original date time or set to current if 202 // it was not in the request 203 if (request.getParameter("originalDateTime") == null) { 204 model.put("originalDateTime", currentDateTime); 205 } else { 206 model.put("originalDateTime", request.getParameter("originalDateTime")); 207 } 208 model.put("summary", request.getParameter("summary")); 209 model.put("description", request.getParameter("description")); 210 model.put("location", request.getParameter("location")); 211 model.put("startDateTime", request.getParameter("startDateTime")); 212 model.put("stopDateTime", request.getParameter("stopDateTime")); 213 214 model.put("userRecipients", request.getParameter("userRecipients")); 215 model.put("workgroupRecipients", request.getParameter("workgroupRecipients")); 216 model.put("workgroupNamespaceCodes", request.getParameter("workgroupNamespaceCodes")); 217 218 return model; 219 } 220 221 /** 222 * This method handles submitting the actual event notification message. 223 * @param request 224 * @param response 225 * @return ModelAndView 226 * @throws ServletException 227 * @throws IOException 228 */ 229 public ModelAndView submitEventNotificationMessage( 230 HttpServletRequest request, HttpServletResponse response) 231 throws ServletException, IOException { 232 LOG.debug("remoteUser: " + request.getRemoteUser()); 233 234 // obtain a workflow user object first 235 //WorkflowIdDTO initiator = new WorkflowIdDTO(request.getRemoteUser()); 236 String initiatorId = getPrincipalIdFromIdOrName( request.getRemoteUser()); 237 LOG.debug("initiatorId="+initiatorId); 238 239 // now construct the workflow document, which will interact with workflow 240 WorkflowDocument document; 241 Map<String, Object> model = new HashMap<String, Object>(); 242 String view; 243 try { 244 document = NotificationWorkflowDocument.createNotificationDocument( 245 initiatorId, 246 NotificationConstants.KEW_CONSTANTS.SEND_NOTIFICATION_REQ_DOC_TYPE); 247 248 //parse out the application content into a Notification BO 249 NotificationBo notification = populateNotificationInstance(request, model); 250 251 // now get that content in an understandable XML format and pass into document 252 String notificationAsXml = messageContentService 253 .generateNotificationMessage(notification); 254 255 Map<String, String> attrFields = new HashMap<String,String>(); 256 List<NotificationChannelReviewerBo> reviewers = notification.getChannel().getReviewers(); 257 int ui = 0; 258 int gi = 0; 259 for (NotificationChannelReviewerBo reviewer: reviewers) { 260 String prefix; 261 int index; 262 if (KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.equals(reviewer.getReviewerType())) { 263 prefix = "user"; 264 index = ui; 265 ui++; 266 } else if (KimGroupMemberTypes.GROUP_MEMBER_TYPE.equals(reviewer.getReviewerType())) { 267 prefix = "group"; 268 index = gi; 269 gi++; 270 } else { 271 LOG.error("Invalid type for reviewer " + reviewer.getReviewerId() + ": " + reviewer.getReviewerType()); 272 continue; 273 } 274 attrFields.put(prefix + index, reviewer.getReviewerId()); 275 } 276 GenericAttributeContent gac = new GenericAttributeContent("channelReviewers"); 277 document.setApplicationContent(notificationAsXml); 278 document.setAttributeContent("<attributeContent>" + gac.generateContent(attrFields) + "</attributeContent>"); 279 280 document.setTitle(notification.getTitle()); 281 282 document.route("This message was submitted via the event notification message submission form by user " 283 + initiatorId); 284 285 view = "SendEventNotificationMessage"; 286 287 // This ain't pretty, but it gets the job done for now. 288 ErrorList el = new ErrorList(); 289 el.addError("Notification(s) sent."); 290 model.put("errors", el); 291 292 } catch (ErrorList el) { 293 // route back to the send form again 294 Map<String, Object> model2 = setupModelForSendEventNotification(request); 295 model.putAll(model2); 296 model.put("errors", el); 297 298 view = "SendEventNotificationMessage"; 299 } catch (Exception e) { 300 throw new RuntimeException(e); 301 } 302 303 return new ModelAndView(view, model); 304 } 305 306 /** 307 * This method creates a new Notification instance from the event form values. 308 * @param request 309 * @param model 310 * @return Notification 311 * @throws IllegalArgumentException 312 */ 313 private NotificationBo populateNotificationInstance( 314 HttpServletRequest request, Map<String, Object> model) 315 throws IllegalArgumentException, ErrorList { 316 ErrorList errors = new ErrorList(); 317 318 NotificationBo notification = new NotificationBo(); 319 320 // grab data from form 321 // channel name 322 String channelName = request.getParameter("channelName"); 323 if (StringUtils.isEmpty(channelName) || StringUtils.equals(channelName, NONE_CHANNEL)) { 324 errors.addError("You must choose a channel."); 325 } else { 326 model.put("channelName", channelName); 327 } 328 329 // priority name 330 String priorityName = request.getParameter("priorityName"); 331 if (StringUtils.isEmpty(priorityName)) { 332 errors.addError("You must choose a priority."); 333 } else { 334 model.put("priorityName", priorityName); 335 } 336 337 // sender names 338 String senderNames = request.getParameter("senderNames"); 339 String[] senders = null; 340 if (StringUtils.isEmpty(senderNames)) { 341 errors.addError("You must enter at least one sender."); 342 } else { 343 senders = StringUtils.split(senderNames, ","); 344 345 model.put("senderNames", senderNames); 346 } 347 348 // delivery type 349 String deliveryType = request.getParameter("deliveryType"); 350 if (StringUtils.isEmpty(deliveryType)) { 351 errors.addError("You must choose a type."); 352 } else { 353 if (deliveryType 354 .equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) { 355 deliveryType = NotificationConstants.DELIVERY_TYPES.FYI; 356 } else { 357 deliveryType = NotificationConstants.DELIVERY_TYPES.ACK; 358 } 359 model.put("deliveryType", deliveryType); 360 } 361 362 //get datetime when form was initially rendered 363 String originalDateTime = request.getParameter("originalDateTime"); 364 Date origdate = null; 365 Date senddate = null; 366 Date removedate = null; 367 try { 368 origdate = Util.parseUIDateTime(originalDateTime); 369 } catch (ParseException pe) { 370 errors.addError("Original date is invalid."); 371 } 372 // send date time 373 String sendDateTime = request.getParameter("sendDateTime"); 374 if (StringUtils.isBlank(sendDateTime)) { 375 sendDateTime = Util.getCurrentDateTime(); 376 } 377 378 try { 379 senddate = Util.parseUIDateTime(sendDateTime); 380 } catch (ParseException pe) { 381 errors.addError("You specified an invalid Send Date/Time. Please use the calendar picker."); 382 } 383 384 if(senddate != null && senddate.before(origdate)) { 385 errors.addError("Send Date/Time cannot be in the past."); 386 } 387 388 model.put("sendDateTime", sendDateTime); 389 390 // auto remove date time 391 String autoRemoveDateTime = request.getParameter("autoRemoveDateTime"); 392 if (StringUtils.isNotBlank(autoRemoveDateTime)) { 393 try { 394 removedate = Util.parseUIDateTime(autoRemoveDateTime); 395 } catch (ParseException pe) { 396 errors.addError("You specified an invalid Auto-Remove Date/Time. Please use the calendar picker."); 397 } 398 399 if(removedate != null) { 400 if(removedate.before(origdate)) { 401 errors.addError("Auto-Remove Date/Time cannot be in the past."); 402 } else if (senddate != null && removedate.before(senddate)){ 403 errors.addError("Auto-Remove Date/Time cannot be before the Send Date/Time."); 404 } 405 } 406 } 407 408 model.put("autoRemoveDateTime", autoRemoveDateTime); 409 410 // user recipient names 411 String[] userRecipients = parseUserRecipients(request); 412 413 // workgroup recipient names 414 String[] workgroupRecipients = parseWorkgroupRecipients(request); 415 416 // workgroup namespace names 417 String[] workgroupNamespaceCodes = parseWorkgroupNamespaceCodes(request); 418 419 // title 420 String title = request.getParameter("title"); 421 if (!StringUtils.isEmpty(title)) { 422 model.put("title", title); 423 } else { 424 errors.addError("You must fill in a title"); 425 } 426 427 // message 428 String message = request.getParameter("message"); 429 if (StringUtils.isEmpty(message)) { 430 errors.addError("You must fill in a message."); 431 } else { 432 model.put("message", message); 433 } 434 435 // all event fields are mandatory for event type 436 437 // start date time 438 String startDateTime = request.getParameter("startDateTime"); 439 if (StringUtils.isEmpty(startDateTime)) { 440 errors.addError("You must fill in a start date/time."); 441 } else { 442 model.put("startDateTime", startDateTime); 443 } 444 445 // stop date time 446 String stopDateTime = request.getParameter("stopDateTime"); 447 if (StringUtils.isEmpty(stopDateTime)) { 448 errors.addError("You must fill in a stop date/time."); 449 } else { 450 model.put("stopDateTime", stopDateTime); 451 } 452 453 // summary 454 String summary = request.getParameter("summary"); 455 if (StringUtils.isEmpty(summary)) { 456 errors.addError("You must fill in a summary."); 457 } else { 458 model.put("summary", summary); 459 } 460 461 // description 462 String description = request.getParameter("description"); 463 if (StringUtils.isEmpty(description)) { 464 errors.addError("You must fill in a description."); 465 } else { 466 model.put("description", description); 467 } 468 469 // location 470 String location = request.getParameter("location"); 471 if (StringUtils.isEmpty(location)) { 472 errors.addError("You must fill in a location."); 473 } else { 474 model.put("location", location); 475 } 476 477 // stop processing if there are errors 478 if (errors.getErrors().size() > 0) { 479 throw errors; 480 } 481 482 // now populate the notification BO instance 483 NotificationChannelBo channel = Util.retrieveFieldReference("channel", 484 "name", channelName, NotificationChannelBo.class, 485 businessObjectDao); 486 notification.setChannel(channel); 487 488 NotificationPriorityBo priority = Util.retrieveFieldReference("priority", 489 "name", priorityName, NotificationPriorityBo.class, 490 businessObjectDao); 491 notification.setPriority(priority); 492 493 NotificationContentTypeBo contentType = Util.retrieveFieldReference( 494 "contentType", "name", 495 NotificationConstants.CONTENT_TYPES.EVENT_CONTENT_TYPE, 496 NotificationContentTypeBo.class, businessObjectDao); 497 notification.setContentType(contentType); 498 499 NotificationProducerBo producer = Util 500 .retrieveFieldReference( 501 "producer", 502 "name", 503 NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME, 504 NotificationProducerBo.class, businessObjectDao); 505 notification.setProducer(producer); 506 507 for (String senderName : senders) { 508 if (StringUtils.isEmpty(senderName)) { 509 errors.addError("A sender's name cannot be blank."); 510 } else { 511 NotificationSenderBo ns = new NotificationSenderBo(); 512 ns.setSenderName(senderName.trim()); 513 notification.addSender(ns); 514 } 515 } 516 517 boolean recipientsExist = false; 518 519 if (userRecipients != null && userRecipients.length > 0) { 520 recipientsExist = true; 521 for (String userRecipientId : userRecipients) { 522 if (isUserRecipientValid(userRecipientId, errors)) { 523 NotificationRecipientBo recipient = new NotificationRecipientBo(); 524 recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode()); 525 recipient.setRecipientId(userRecipientId); 526 notification.addRecipient(recipient); 527 } 528 } 529 } 530 531 if (workgroupRecipients != null && workgroupRecipients.length > 0) { 532 recipientsExist = true; 533 if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) { 534 if (workgroupNamespaceCodes.length == workgroupRecipients.length) { 535 for (int i = 0; i < workgroupRecipients.length; i++) { 536 if (isWorkgroupRecipientValid(workgroupRecipients[i], workgroupNamespaceCodes[i], errors)) { 537 NotificationRecipientBo recipient = new NotificationRecipientBo(); 538 recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode()); 539 recipient.setRecipientId( 540 getGroupService().getGroupByNamespaceCodeAndName(workgroupNamespaceCodes[i], 541 workgroupRecipients[i]).getId()); 542 notification.addRecipient(recipient); 543 } 544 } 545 } else { 546 errors.addError("The number of groups must match the number of namespace codes"); 547 } 548 } else { 549 errors.addError("You must specify a namespace code for every group name"); 550 } 551 } else if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) { 552 errors.addError("You must specify a group name for every namespace code"); 553 } 554 555 // check to see if there were any errors 556 if (errors.getErrors().size() > 0) { 557 throw errors; 558 } 559 560 notification.setTitle(title); 561 562 notification.setDeliveryType(deliveryType); 563 564 Date startDate = null; 565 Date stopDate = null; 566 // simpledateformat is not threadsafe, have to sync and validate 567 Date d = null; 568 if (StringUtils.isNotBlank(sendDateTime)) { 569 try { 570 d = Util.parseUIDateTime(sendDateTime); 571 } catch (ParseException pe) { 572 errors.addError("You specified an invalid send date and time. Please use the calendar picker."); 573 } 574 notification.setSendDateTimeValue(new Timestamp(d.getTime())); 575 } 576 577 Date d2 = null; 578 if (StringUtils.isNotBlank(autoRemoveDateTime)) { 579 try { 580 d2 = Util.parseUIDateTime(autoRemoveDateTime); 581 if (d2.before(d)) { 582 errors.addError("Auto Remove Date/Time cannot be before Send Date/Time."); 583 } 584 } catch (ParseException pe) { 585 errors.addError("You specified an invalid auto-remove date and time. Please use the calendar picker."); 586 } 587 notification.setAutoRemoveDateTimeValue(new Timestamp(d2.getTime())); 588 } 589 590 if (StringUtils.isNotBlank(startDateTime)) { 591 try { 592 startDate = Util.parseUIDateTime(startDateTime); 593 } catch (ParseException pe) { 594 errors.addError("You specified an invalid start date and time. Please use the calendar picker."); 595 } 596 } 597 598 if (StringUtils.isNotBlank(stopDateTime)) { 599 try { 600 stopDate = Util.parseUIDateTime(stopDateTime); 601 } catch (ParseException pe) { 602 errors.addError("You specified an invalid stop date and time. Please use the calendar picker."); 603 } 604 } 605 606 if(stopDate != null && startDate != null) { 607 if (stopDate.before(startDate)) { 608 errors.addError("Event Stop Date/Time cannot be before Event Start Date/Time."); 609 } 610 } 611 612 if (!recipientsExist && !hasPotentialRecipients(notification)) { 613 errors.addError("You must specify at least one user or group recipient."); 614 } 615 616 // check to see if there were any errors 617 if (errors.getErrors().size() > 0) { 618 throw errors; 619 } 620 621 notification 622 .setContent(NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_EVENT_OPEN 623 + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_OPEN 624 + message 625 + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_CLOSE 626 + "<event>\n" 627 + " <summary>" + summary + "</summary>\n" 628 + " <description>" + description + "</description>\n" 629 + " <location>" + location + "</location>\n" 630 + " <startDateTime>" + Util.toUIDateTimeString(startDate) + "</startDateTime>\n" 631 + " <stopDateTime>" + Util.toUIDateTimeString(stopDate) + "</stopDateTime>\n" 632 + "</event>" 633 + NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_CLOSE); 634 635 return notification; 636 } 637 }