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 * 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 */
061public 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}