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    }