001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.ken.web.spring;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
021import org.kuali.rice.core.framework.persistence.dao.GenericDao;
022import org.kuali.rice.ken.bo.NotificationBo;
023import org.kuali.rice.ken.bo.NotificationChannelBo;
024import org.kuali.rice.ken.bo.NotificationChannelReviewerBo;
025import org.kuali.rice.ken.bo.NotificationContentTypeBo;
026import org.kuali.rice.ken.bo.NotificationPriorityBo;
027import org.kuali.rice.ken.bo.NotificationProducerBo;
028import org.kuali.rice.ken.bo.NotificationRecipientBo;
029import org.kuali.rice.ken.bo.NotificationSenderBo;
030import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument;
031import org.kuali.rice.ken.exception.ErrorList;
032import org.kuali.rice.ken.service.NotificationChannelService;
033import org.kuali.rice.ken.service.NotificationMessageContentService;
034import org.kuali.rice.ken.service.NotificationRecipientService;
035import org.kuali.rice.ken.service.NotificationService;
036import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
037import org.kuali.rice.ken.util.NotificationConstants;
038import org.kuali.rice.ken.util.Util;
039import org.kuali.rice.kew.api.WorkflowDocument;
040import org.kuali.rice.kew.rule.GenericAttributeContent;
041import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
042import org.kuali.rice.kim.api.identity.principal.Principal;
043import org.kuali.rice.kim.api.services.KimApiServiceLocator;
044import org.springframework.web.servlet.ModelAndView;
045
046import javax.servlet.ServletException;
047import javax.servlet.http.HttpServletRequest;
048import javax.servlet.http.HttpServletResponse;
049import java.io.IOException;
050import java.sql.Timestamp;
051import java.text.ParseException;
052import java.util.Date;
053import java.util.HashMap;
054import java.util.List;
055import 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 */
062public 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}