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 }