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