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 Simple notification messages via an end user interface.
58   * @author Kuali Rice Team (rice.collab@kuali.org)
59   */
60  public class SendNotificationMessageController extends BaseSendNotificationController {
61  	/** Logger for this class and subclasses */
62  	private static final Logger LOG = Logger
63  	.getLogger(SendNotificationMessageController.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 a simple 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 sendSimpleNotificationMessage(
167 			HttpServletRequest request, HttpServletResponse response)
168 	throws ServletException, IOException {
169 		String view = "SendSimpleNotificationMessage";
170 
171 		LOG.debug("remoteUser: " + request.getRemoteUser());
172 
173 		Map<String, Object> model = setupModelForSendSimpleNotification(request);
174 		model.put("errors", new ErrorList()); // need an empty one so we don't have an NPE
175 
176 		return new ModelAndView(view, model);
177 	}
178 
179 	/**
180 	 * This method prepares the model used for the send simple notification message form.
181 	 * @param request
182 	 * @return Map<String, Object>
183 	 */
184 	private Map<String, Object> setupModelForSendSimpleNotification(
185 			HttpServletRequest request) {
186 		Map<String, Object> model = new HashMap<String, Object>();
187 		model.put("defaultSender", request.getRemoteUser());
188 		model.put("channels", notificationChannelService
189 				.getAllNotificationChannels());
190 		model.put("priorities", businessObjectDao
191 				.findAll(NotificationPriority.class));
192 		// set sendDateTime to current datetime if not provided
193 		String sendDateTime = request.getParameter("sendDateTime");
194 		String currentDateTime = Util.getCurrentDateTime();
195 		if (StringUtils.isEmpty(sendDateTime)) {
196 			sendDateTime = currentDateTime;
197 		}
198 		model.put("sendDateTime", sendDateTime);
199 
200 		// retain the original date time or set to current if
201 		// it was not in the request
202 		if (request.getParameter("originalDateTime") == null) {
203 			model.put("originalDateTime", currentDateTime);
204 		} else {
205 			model.put("originalDateTime", request.getParameter("originalDateTime"));
206 		}
207 
208 		model.put("userRecipients", request.getParameter("userRecipients"));
209 		model.put("workgroupRecipients", request.getParameter("workgroupRecipients"));
210 		model.put("workgroupNamespaceCodes", request.getParameter("workgroupNamespaceCodes"));
211 
212 		return model;
213 	}
214 
215 	/**
216 	 * This method handles submitting the actual simple notification message.
217 	 * @param request
218 	 * @param response
219 	 * @return ModelAndView
220 	 * @throws ServletException
221 	 * @throws IOException
222 	 */
223 	public ModelAndView submitSimpleNotificationMessage(
224 			HttpServletRequest request, HttpServletResponse response)
225 	throws ServletException, IOException {
226 		LOG.debug("remoteUser: " + request.getRemoteUser());
227 
228 		// obtain a workflow user object first
229 		WorkflowIdDTO initiator = new WorkflowIdDTO(request.getRemoteUser());
230 
231 		// now construct the workflow document, which will interact with workflow
232 		NotificationWorkflowDocument document;
233 		Map<String, Object> model = new HashMap<String, Object>();
234 		String view;
235 		try {
236 			document = new NotificationWorkflowDocument(
237 					initiator,
238 					NotificationConstants.KEW_CONSTANTS.SEND_NOTIFICATION_REQ_DOC_TYPE);
239 
240 			//parse out the application content into a Notification BO
241 			Notification notification = populateNotificationInstance(request,
242 					model);
243 
244 			// now get that content in an understandable XML format and pass into document
245 			String notificationAsXml = messageContentService
246 			.generateNotificationMessage(notification);
247 
248 			Map<String, String> attrFields = new HashMap<String,String>();
249 			List<NotificationChannelReviewer> reviewers = notification.getChannel().getReviewers();
250 			int ui = 0;
251 			int gi = 0;
252 			for (NotificationChannelReviewer reviewer: reviewers) {
253 				String prefix;
254 				int index;
255 				if (KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
256 					prefix = "user";
257 					index = ui;
258 					ui++;
259 				} else if (KimGroupMemberTypes.GROUP_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
260 					prefix = "group";
261 					index = gi;
262 					gi++;
263 				} else {
264 					LOG.error("Invalid type for reviewer " + reviewer.getReviewerId() + ": " + reviewer.getReviewerType());
265 					continue;
266 				}
267 				attrFields.put(prefix + index, reviewer.getReviewerId());
268 			}
269 			GenericAttributeContent gac = new GenericAttributeContent("channelReviewers");
270 			document.getDocumentContent().setApplicationContent(notificationAsXml);
271 			document.getDocumentContent().setAttributeContent("<attributeContent>" + gac.generateContent(attrFields) + "</attributeContent>");
272 
273 			document.setTitle(notification.getTitle());
274 
275 			document.routeDocument("This message was submitted via the simple notification message submission form by user "
276 					+ initiator.getWorkflowId());
277 
278 			view = "SendSimpleNotificationMessage";
279 
280 			// This ain't pretty, but it gets the job done for now.
281 			ErrorList el = new ErrorList();
282 			el.addError("Notification(s) sent.");
283 			model.put("errors", el);
284 
285 		} catch (ErrorList el) {
286 			// route back to the send form again
287 			Map<String, Object> model2 = setupModelForSendSimpleNotification(request);
288 			model.putAll(model2);
289 			model.put("errors", el);
290 
291 			view = "SendSimpleNotificationMessage";
292 		} catch (Exception e) {
293 			throw new RuntimeException(e);
294 		}
295 
296 		return new ModelAndView(view, model);
297 	}
298 
299 	/**
300 	 * This method creates a new Notification instance from the form values.
301 	 * @param request
302 	 * @param model
303 	 * @return Notification
304 	 * @throws IllegalArgumentException
305 	 */
306 	private Notification populateNotificationInstance(
307 			HttpServletRequest request, Map<String, Object> model)
308 	throws IllegalArgumentException, ErrorList {
309 		ErrorList errors = new ErrorList();
310 
311 		Notification notification = new Notification();
312 
313 		// grab data from form
314 		// channel name
315 		String channelName = request.getParameter("channelName");
316 		if (StringUtils.isEmpty(channelName) || StringUtils.equals(channelName, NONE_CHANNEL)) {
317 			errors.addError("You must choose a channel.");
318 		} else {
319 			model.put("channelName", channelName);
320 		}
321 
322 		// priority name
323 		String priorityName = request.getParameter("priorityName");
324 		if (StringUtils.isEmpty(priorityName)) {
325 			errors.addError("You must choose a priority.");
326 		} else {
327 			model.put("priorityName", priorityName);
328 		}
329 
330 		// sender names
331 		String senderNames = request.getParameter("senderNames");
332 		String[] senders = null;
333 		if (StringUtils.isEmpty(senderNames)) {
334 			errors.addError("You must enter at least one sender.");
335 		} else {
336 			senders = StringUtils.split(senderNames, ",");
337 
338 			model.put("senderNames", senderNames);
339 		}
340 
341 		// delivery type
342 		String deliveryType = request.getParameter("deliveryType");
343 		if (StringUtils.isEmpty(deliveryType)) {
344 			errors.addError("You must choose a type.");
345 		} else {
346 			if (deliveryType
347 					.equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
348 				deliveryType = NotificationConstants.DELIVERY_TYPES.FYI;
349 			} else {
350 				deliveryType = NotificationConstants.DELIVERY_TYPES.ACK;
351 			}
352 			model.put("deliveryType", deliveryType);
353 		}
354 
355 		// get datetime when form was initially rendered
356 		String originalDateTime = request.getParameter("originalDateTime");
357 		Date origdate = null;
358 		Date senddate = null;
359 		Date removedate = null;
360 		try {
361 			origdate = Util.parseUIDateTime(originalDateTime);
362 		} catch (ParseException pe) {
363 			errors.addError("Original date is invalid.");
364 		}
365 		// send date time
366 		String sendDateTime = request.getParameter("sendDateTime");
367 		if (StringUtils.isBlank(sendDateTime)) {
368 			sendDateTime = Util.getCurrentDateTime();
369 		}
370 
371 		try {
372 			senddate = Util.parseUIDateTime(sendDateTime);
373 		} catch (ParseException pe) {
374 			errors.addError("You specified an invalid Send Date/Time.  Please use the calendar picker.");
375 		}
376 
377 		if(senddate != null && senddate.before(origdate)) {
378 			errors.addError("Send Date/Time cannot be in the past.");
379 		}
380 
381 		model.put("sendDateTime", sendDateTime);
382 
383 		// auto remove date time
384 		String autoRemoveDateTime = request.getParameter("autoRemoveDateTime");
385 		if (StringUtils.isNotBlank(autoRemoveDateTime)) {
386 			try {
387 				removedate = Util.parseUIDateTime(autoRemoveDateTime);
388 			} catch (ParseException pe) {
389 				errors.addError("You specified an invalid Auto-Remove Date/Time.  Please use the calendar picker.");
390 			}
391 
392 			if(removedate != null) {
393 				if(removedate.before(origdate)) {
394 					errors.addError("Auto-Remove Date/Time cannot be in the past.");
395 				} else if (senddate != null && removedate.before(senddate)){
396 					errors.addError("Auto-Remove Date/Time cannot be before the Send Date/Time.");
397 				}
398 			}
399 		}
400 
401 		model.put("autoRemoveDateTime", autoRemoveDateTime);
402 
403 		// user recipient names
404 		String[] userRecipients = parseUserRecipients(request);
405 
406 		// workgroup recipient names
407 		String[] workgroupRecipients = parseWorkgroupRecipients(request);
408 
409 		// workgroup namespace codes
410 		String[] workgroupNamespaceCodes = parseWorkgroupNamespaceCodes(request);
411 		
412 		// title
413 		String title = request.getParameter("title");
414 		if (!StringUtils.isEmpty(title)) {
415 			model.put("title", title);
416 		} else {
417 			errors.addError("You must fill in a title");
418 		}
419 
420 		// message
421 		String message = request.getParameter("message");
422 		if (StringUtils.isEmpty(message)) {
423 			errors.addError("You must fill in a message.");
424 		} else {
425 			model.put("message", message);
426 		}
427 
428 		// stop processing if there are errors
429 		if (errors.getErrors().size() > 0) {
430 			throw errors;
431 		}
432 
433 		// now populate the notification BO instance
434 		NotificationChannel channel = Util.retrieveFieldReference("channel",
435 				"name", channelName, NotificationChannel.class,
436 				businessObjectDao);
437 		notification.setChannel(channel);
438 
439 		NotificationPriority priority = Util.retrieveFieldReference("priority",
440 				"name", priorityName, NotificationPriority.class,
441 				businessObjectDao);
442 		notification.setPriority(priority);
443 
444 		NotificationContentType contentType = Util.retrieveFieldReference(
445 				"contentType", "name",
446 				NotificationConstants.CONTENT_TYPES.SIMPLE_CONTENT_TYPE,
447 				NotificationContentType.class, businessObjectDao);
448 		notification.setContentType(contentType);
449 
450 		NotificationProducer producer = Util
451 		.retrieveFieldReference(
452 				"producer",
453 				"name",
454 				NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME,
455 				NotificationProducer.class, businessObjectDao);
456 		notification.setProducer(producer);
457 
458 		for (String senderName : senders) {
459 			if (StringUtils.isEmpty(senderName)) {
460 				errors.addError("A sender's name cannot be blank.");
461 			} else {
462 				NotificationSender ns = new NotificationSender();
463 				ns.setSenderName(senderName.trim());
464 				notification.addSender(ns);
465 			}
466 		}
467 
468 		boolean recipientsExist = false;
469 
470 		if (userRecipients != null && userRecipients.length > 0) {
471 			recipientsExist = true;
472 			for (String userRecipientId : userRecipients) {
473 				if (isUserRecipientValid(userRecipientId, errors)) {
474 					NotificationRecipient recipient = new NotificationRecipient();
475 					recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE);
476 					recipient.setRecipientId(userRecipientId);
477 					notification.addRecipient(recipient);
478 				}
479 			}
480 		}
481 
482 		if (workgroupRecipients != null && workgroupRecipients.length > 0) {
483 			recipientsExist = true;
484 			if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
485 				if (workgroupNamespaceCodes.length == workgroupRecipients.length) {
486 					for (int i = 0; i < workgroupRecipients.length; i++) {
487 						if (isWorkgroupRecipientValid(workgroupRecipients[i], workgroupNamespaceCodes[i], errors)) {
488 							NotificationRecipient recipient = new NotificationRecipient();
489 							recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE);
490 							recipient.setRecipientId(
491 									getIdentityManagementService().getGroupByName(workgroupNamespaceCodes[i], workgroupRecipients[i]).getGroupId());
492 							notification.addRecipient(recipient);
493 						}
494 					}
495 				} else {
496 					errors.addError("The number of groups must match the number of namespace codes");
497 				}
498 			} else {
499 				errors.addError("You must specify a namespace code for every group name");
500 			}
501 		} else if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
502 			errors.addError("You must specify a group name for every namespace code");
503 		}
504 
505 		// check to see if there were any errors
506 		if (errors.getErrors().size() > 0) {
507 			throw errors;
508 		}
509 
510 		notification.setTitle(title);
511 
512 		notification.setDeliveryType(deliveryType);
513 
514 		// simpledateformat is not threadsafe, have to sync and validate
515 		Date d = null;
516 		if (StringUtils.isNotBlank(sendDateTime)) {
517 			try {
518 				d = Util.parseUIDateTime(sendDateTime);
519 			} catch (ParseException pe) {
520 				errors.addError("You specified an invalid send date and time.  Please use the calendar picker.");
521 			}
522 			notification.setSendDateTime(new Timestamp(d.getTime()));
523 		}
524 
525 		Date d2 = null;
526 		if (StringUtils.isNotBlank(autoRemoveDateTime)) {
527 			try {
528 				d2 = Util.parseUIDateTime(autoRemoveDateTime);
529 				if (d2.before(d)) {
530 					errors.addError("Auto Remove Date/Time cannot be before Send Date/Time.");
531 				}
532 			} catch (ParseException pe) {
533 				errors.addError("You specified an invalid auto remove date and time.  Please use the calendar picker.");
534 			}
535 			notification.setAutoRemoveDateTime(new Timestamp(d2.getTime()));
536 		}
537 
538 
539 		if (!recipientsExist && !hasPotentialRecipients(notification)) {
540 			errors.addError("You must specify at least one user or group recipient.");
541 		}
542 
543 		// check to see if there were any errors
544 		if (errors.getErrors().size() > 0) {
545 			throw errors;
546 		}
547 
548 		notification
549 		.setContent(NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_SIMPLE_OPEN
550 				+ NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_OPEN
551 				+ message
552 				+ NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_CLOSE
553 				+ NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_CLOSE);
554 
555 		return notification;
556 	}
557 }