001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.ken.web.spring;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.log4j.Logger;
020    import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
021    import org.kuali.rice.core.framework.persistence.dao.GenericDao;
022    import org.kuali.rice.ken.bo.NotificationBo;
023    import org.kuali.rice.ken.bo.NotificationChannelBo;
024    import org.kuali.rice.ken.bo.NotificationChannelReviewerBo;
025    import org.kuali.rice.ken.bo.NotificationContentTypeBo;
026    import org.kuali.rice.ken.bo.NotificationPriorityBo;
027    import org.kuali.rice.ken.bo.NotificationProducerBo;
028    import org.kuali.rice.ken.bo.NotificationRecipientBo;
029    import org.kuali.rice.ken.bo.NotificationSenderBo;
030    import org.kuali.rice.ken.document.kew.NotificationWorkflowDocument;
031    import org.kuali.rice.ken.exception.ErrorList;
032    import org.kuali.rice.ken.service.NotificationChannelService;
033    import org.kuali.rice.ken.service.NotificationMessageContentService;
034    import org.kuali.rice.ken.service.NotificationRecipientService;
035    import org.kuali.rice.ken.service.NotificationService;
036    import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
037    import org.kuali.rice.ken.util.NotificationConstants;
038    import org.kuali.rice.ken.util.Util;
039    import org.kuali.rice.kew.api.WorkflowDocument;
040    import org.kuali.rice.kew.rule.GenericAttributeContent;
041    import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
042    import org.kuali.rice.kim.api.identity.principal.Principal;
043    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
044    import org.springframework.web.servlet.ModelAndView;
045    
046    import javax.servlet.ServletException;
047    import javax.servlet.http.HttpServletRequest;
048    import javax.servlet.http.HttpServletResponse;
049    import java.io.IOException;
050    import java.sql.Timestamp;
051    import java.text.ParseException;
052    import java.util.Date;
053    import java.util.HashMap;
054    import java.util.List;
055    import java.util.Map;
056    
057    /**
058     * This class is the controller for sending Simple notification messages via an end user interface.
059     * @author Kuali Rice Team (rice.collab@kuali.org)
060     */
061    public class SendNotificationMessageController extends BaseSendNotificationController {
062        /** Logger for this class and subclasses */
063        private static final Logger LOG = Logger
064                .getLogger(SendNotificationMessageController.class);
065    
066        private static final String NONE_CHANNEL = "___NONE___";
067        private static final long REASONABLE_IMMEDIATE_TIME_THRESHOLD = 1000 * 60 * 5; // <= 5 minutes is "immediate"
068    
069        /**
070         * Returns whether the specified time is considered "in the future", based on some reasonable
071         * threshold
072         * @param time the time to test
073         * @return whether the specified time is considered "in the future", based on some reasonable
074         *         threshold
075         */
076        private boolean timeIsInTheFuture(long time) {
077            boolean future = (time - System.currentTimeMillis()) > REASONABLE_IMMEDIATE_TIME_THRESHOLD;
078            LOG.info("Time: " + new Date(time) + " is in the future? " + future);
079            return future;
080        }
081    
082        /**
083         * Returns whether the specified Notification can be reasonably expected to have recipients.
084         * This is determined on whether the channel has default recipients, is subscribably, and
085         * whether the send date time is far enough in the future to expect that if there are no
086         * subscribers, there may actually be some by the time the notification is sent.
087         * @param notification the notification to test
088         * @return whether the specified Notification can be reasonably expected to have recipients
089         */
090        private boolean hasPotentialRecipients(NotificationBo notification) {
091            LOG.info("notification channel " + notification.getChannel() + " is subscribable: "
092                    + notification.getChannel().isSubscribable());
093            return notification.getChannel().getRecipientLists().size() > 0
094                    ||
095                    notification.getChannel().getSubscriptions().size() > 0
096                    ||
097                    (notification.getChannel().isSubscribable() && timeIsInTheFuture(notification.getSendDateTimeValue()
098                            .getTime()));
099        }
100    
101        protected NotificationService notificationService;
102    
103        protected NotificationWorkflowDocumentService notificationWorkflowDocService;
104    
105        protected NotificationChannelService notificationChannelService;
106    
107        protected NotificationRecipientService notificationRecipientService;
108    
109        protected NotificationMessageContentService messageContentService;
110    
111        protected GenericDao businessObjectDao;
112    
113        /**
114         * Set the NotificationService
115         * @param notificationService
116         */
117        public void setNotificationService(NotificationService notificationService) {
118            this.notificationService = notificationService;
119        }
120    
121        /**
122         * This method sets the NotificationWorkflowDocumentService
123         * @param s
124         */
125        public void setNotificationWorkflowDocumentService(
126                NotificationWorkflowDocumentService s) {
127            this.notificationWorkflowDocService = s;
128        }
129    
130        /**
131         * Sets the notificationChannelService attribute value.
132         * @param notificationChannelService The notificationChannelService to set.
133         */
134        public void setNotificationChannelService(
135                NotificationChannelService notificationChannelService) {
136            this.notificationChannelService = notificationChannelService;
137        }
138    
139        /**
140         * Sets the notificationRecipientService attribute value.
141         * @param notificationRecipientService
142         */
143        public void setNotificationRecipientService(
144                NotificationRecipientService notificationRecipientService) {
145            this.notificationRecipientService = notificationRecipientService;
146        }
147    
148        /**
149         * Sets the messageContentService attribute value.
150         * @param messageContentService
151         */
152        public void setMessageContentService(
153                NotificationMessageContentService notificationMessageContentService) {
154            this.messageContentService = notificationMessageContentService;
155        }
156    
157        /**
158         * Sets the businessObjectDao attribute value.
159         * @param businessObjectDao The businessObjectDao to set.
160         */
161        public void setBusinessObjectDao(GenericDao businessObjectDao) {
162            this.businessObjectDao = businessObjectDao;
163        }
164    
165        /**
166         * Handles the display of the form for sending a simple notification message
167         * @param request : a servlet request
168         * @param response : a servlet response
169         * @throws ServletException : an exception
170         * @throws IOException : an exception
171         * @return a ModelAndView object
172         */
173        public ModelAndView sendSimpleNotificationMessage(
174                HttpServletRequest request, HttpServletResponse response)
175                throws ServletException, IOException {
176            String view = "SendSimpleNotificationMessage";
177    
178            LOG.debug("remoteUser: " + request.getRemoteUser());
179    
180            Map<String, Object> model = setupModelForSendSimpleNotification(request);
181            model.put("errors", new ErrorList()); // need an empty one so we don't have an NPE
182    
183            return new ModelAndView(view, model);
184        }
185    
186        /**
187         * This method prepares the model used for the send simple notification message form.
188         * @param request
189         * @return Map<String, Object>
190         */
191        private Map<String, Object> setupModelForSendSimpleNotification(
192                HttpServletRequest request) {
193            Map<String, Object> model = new HashMap<String, Object>();
194            model.put("defaultSender", request.getRemoteUser());
195            model.put("channels", notificationChannelService
196                    .getAllNotificationChannels());
197            model.put("priorities", businessObjectDao
198                    .findAll(NotificationPriorityBo.class));
199            // set sendDateTime to current datetime if not provided
200            String sendDateTime = request.getParameter("sendDateTime");
201            String currentDateTime = Util.getCurrentDateTime();
202            if (StringUtils.isEmpty(sendDateTime)) {
203                sendDateTime = currentDateTime;
204            }
205            model.put("sendDateTime", sendDateTime);
206    
207            // retain the original date time or set to current if
208            // it was not in the request
209            if (request.getParameter("originalDateTime") == null) {
210                model.put("originalDateTime", currentDateTime);
211            } else {
212                model.put("originalDateTime", request.getParameter("originalDateTime"));
213            }
214    
215            model.put("userRecipients", request.getParameter("userRecipients"));
216            model.put("workgroupRecipients", request.getParameter("workgroupRecipients"));
217            model.put("workgroupNamespaceCodes", request.getParameter("workgroupNamespaceCodes"));
218    
219            return model;
220        }
221    
222        /**
223         * This method handles submitting the actual simple notification message.
224         * @param request
225         * @param response
226         * @return ModelAndView
227         * @throws ServletException
228         * @throws IOException
229         */
230        public ModelAndView submitSimpleNotificationMessage(
231                HttpServletRequest request, HttpServletResponse response)
232                throws ServletException, IOException {
233            LOG.debug("remoteUser: " + request.getRemoteUser());
234    
235            // obtain a workflow user object first
236            //WorkflowIdDTO initiator = new WorkflowIdDTO(request.getRemoteUser());
237            String initiatorId = getPrincipalIdFromIdOrName( request.getRemoteUser());
238            LOG.debug("initiatorId="+initiatorId);
239    
240            // now construct the workflow document, which will interact with workflow
241            WorkflowDocument document;
242            Map<String, Object> model = new HashMap<String, Object>();
243            String view;
244            try {
245                document = NotificationWorkflowDocument.createNotificationDocument(
246                        initiatorId,
247                        NotificationConstants.KEW_CONSTANTS.SEND_NOTIFICATION_REQ_DOC_TYPE);
248    
249                //parse out the application content into a Notification BO
250                NotificationBo notification = populateNotificationInstance(request,
251                        model);
252    
253                // now get that content in an understandable XML format and pass into document
254                String notificationAsXml = messageContentService
255                        .generateNotificationMessage(notification);
256    
257                Map<String, String> attrFields = new HashMap<String, String>();
258                List<NotificationChannelReviewerBo> reviewers = notification.getChannel().getReviewers();
259                int ui = 0;
260                int gi = 0;
261                for (NotificationChannelReviewerBo reviewer : reviewers) {
262                    String prefix;
263                    int index;
264                    if (KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
265                        prefix = "user";
266                        index = ui;
267                        ui++;
268                    } else if (KimGroupMemberTypes.GROUP_MEMBER_TYPE.equals(reviewer.getReviewerType())) {
269                        prefix = "group";
270                        index = gi;
271                        gi++;
272                    } else {
273                        LOG.error("Invalid type for reviewer " + reviewer.getReviewerId() + ": "
274                                + reviewer.getReviewerType());
275                        continue;
276                    }
277                    attrFields.put(prefix + index, reviewer.getReviewerId());
278                }
279                GenericAttributeContent gac = new GenericAttributeContent("channelReviewers");
280                document.setApplicationContent(notificationAsXml);
281                document.setAttributeContent("<attributeContent>" + gac.generateContent(attrFields) + "</attributeContent>");
282    
283                document.setTitle(notification.getTitle());
284    
285                document.route("This message was submitted via the simple notification message submission form by user "
286                        + initiatorId);
287    
288                view = "SendSimpleNotificationMessage";
289    
290                // This ain't pretty, but it gets the job done for now.
291                ErrorList el = new ErrorList();
292                el.addError("Notification(s) sent.");
293                model.put("errors", el);
294    
295            } catch (ErrorList el) {
296                // route back to the send form again
297                Map<String, Object> model2 = setupModelForSendSimpleNotification(request);
298                model.putAll(model2);
299                model.put("errors", el);
300    
301                view = "SendSimpleNotificationMessage";
302            } catch (Exception e) {
303                throw new RuntimeException(e);
304            }
305    
306            return new ModelAndView(view, model);
307        }
308    
309        /**
310         * This method creates a new Notification instance from the form values.
311         * @param request
312         * @param model
313         * @return Notification
314         * @throws IllegalArgumentException
315         */
316        private NotificationBo populateNotificationInstance(
317                HttpServletRequest request, Map<String, Object> model)
318                throws IllegalArgumentException, ErrorList {
319            ErrorList errors = new ErrorList();
320    
321            NotificationBo notification = new NotificationBo();
322    
323            // grab data from form
324            // channel name
325            String channelName = request.getParameter("channelName");
326            if (StringUtils.isEmpty(channelName) || StringUtils.equals(channelName, NONE_CHANNEL)) {
327                errors.addError("You must choose a channel.");
328            } else {
329                model.put("channelName", channelName);
330            }
331    
332            // priority name
333            String priorityName = request.getParameter("priorityName");
334            if (StringUtils.isEmpty(priorityName)) {
335                errors.addError("You must choose a priority.");
336            } else {
337                model.put("priorityName", priorityName);
338            }
339    
340            // sender names
341            String senderNames = request.getParameter("senderNames");
342            String[] senders = null;
343            if (StringUtils.isEmpty(senderNames)) {
344                errors.addError("You must enter at least one sender.");
345            } else {
346                senders = StringUtils.split(senderNames, ",");
347    
348                model.put("senderNames", senderNames);
349            }
350    
351            // delivery type
352            String deliveryType = request.getParameter("deliveryType");
353            if (StringUtils.isEmpty(deliveryType)) {
354                errors.addError("You must choose a type.");
355            } else {
356                if (deliveryType
357                        .equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
358                    deliveryType = NotificationConstants.DELIVERY_TYPES.FYI;
359                } else {
360                    deliveryType = NotificationConstants.DELIVERY_TYPES.ACK;
361                }
362                model.put("deliveryType", deliveryType);
363            }
364    
365            // get datetime when form was initially rendered
366            String originalDateTime = request.getParameter("originalDateTime");
367            Date origdate = null;
368            Date senddate = null;
369            Date removedate = null;
370            try {
371                origdate = Util.parseUIDateTime(originalDateTime);
372            } catch (ParseException pe) {
373                errors.addError("Original date is invalid.");
374            }
375            // send date time
376            String sendDateTime = request.getParameter("sendDateTime");
377            if (StringUtils.isBlank(sendDateTime)) {
378                sendDateTime = Util.getCurrentDateTime();
379            }
380    
381            try {
382                senddate = Util.parseUIDateTime(sendDateTime);
383            } catch (ParseException pe) {
384                errors.addError("You specified an invalid Send Date/Time.  Please use the calendar picker.");
385            }
386    
387            if (senddate != null && senddate.before(origdate)) {
388                errors.addError("Send Date/Time cannot be in the past.");
389            }
390    
391            model.put("sendDateTime", sendDateTime);
392    
393            // auto remove date time
394            String autoRemoveDateTime = request.getParameter("autoRemoveDateTime");
395            if (StringUtils.isNotBlank(autoRemoveDateTime)) {
396                try {
397                    removedate = Util.parseUIDateTime(autoRemoveDateTime);
398                } catch (ParseException pe) {
399                    errors.addError("You specified an invalid Auto-Remove Date/Time.  Please use the calendar picker.");
400                }
401    
402                if (removedate != null) {
403                    if (removedate.before(origdate)) {
404                        errors.addError("Auto-Remove Date/Time cannot be in the past.");
405                    } else if (senddate != null && removedate.before(senddate)) {
406                        errors.addError("Auto-Remove Date/Time cannot be before the Send Date/Time.");
407                    }
408                }
409            }
410    
411            model.put("autoRemoveDateTime", autoRemoveDateTime);
412    
413            // user recipient names
414            String[] userRecipients = parseUserRecipients(request);
415    
416            // workgroup recipient names
417            String[] workgroupRecipients = parseWorkgroupRecipients(request);
418    
419            // workgroup namespace codes
420            String[] workgroupNamespaceCodes = parseWorkgroupNamespaceCodes(request);
421    
422            // title
423            String title = request.getParameter("title");
424            if (!StringUtils.isEmpty(title)) {
425                model.put("title", title);
426            } else {
427                errors.addError("You must fill in a title");
428            }
429    
430            // message
431            String message = request.getParameter("message");
432            if (StringUtils.isEmpty(message)) {
433                errors.addError("You must fill in a message.");
434            } else {
435                model.put("message", message);
436            }
437    
438            // stop processing if there are errors
439            if (errors.getErrors().size() > 0) {
440                throw errors;
441            }
442    
443            // now populate the notification BO instance
444            NotificationChannelBo channel = Util.retrieveFieldReference("channel",
445                    "name", channelName, NotificationChannelBo.class,
446                    businessObjectDao);
447            notification.setChannel(channel);
448    
449            NotificationPriorityBo priority = Util.retrieveFieldReference("priority",
450                    "name", priorityName, NotificationPriorityBo.class,
451                    businessObjectDao);
452            notification.setPriority(priority);
453    
454            NotificationContentTypeBo contentType = Util.retrieveFieldReference(
455                    "contentType", "name",
456                    NotificationConstants.CONTENT_TYPES.SIMPLE_CONTENT_TYPE,
457                    NotificationContentTypeBo.class, businessObjectDao);
458            notification.setContentType(contentType);
459    
460            NotificationProducerBo producer = Util
461                    .retrieveFieldReference(
462                            "producer",
463                            "name",
464                            NotificationConstants.KEW_CONSTANTS.NOTIFICATION_SYSTEM_USER_NAME,
465                            NotificationProducerBo.class, businessObjectDao);
466            notification.setProducer(producer);
467    
468            for (String senderName : senders) {
469                if (StringUtils.isEmpty(senderName)) {
470                    errors.addError("A sender's name cannot be blank.");
471                } else {
472                    NotificationSenderBo ns = new NotificationSenderBo();
473                    ns.setSenderName(senderName.trim());
474                    notification.addSender(ns);
475                }
476            }
477    
478            boolean recipientsExist = false;
479    
480            if (userRecipients != null && userRecipients.length > 0) {
481                recipientsExist = true;
482                for (String userRecipientId : userRecipients) {
483                    if (isUserRecipientValid(userRecipientId, errors)) {
484                        NotificationRecipientBo recipient = new NotificationRecipientBo();
485                        recipient.setRecipientType(KimGroupMemberTypes.PRINCIPAL_MEMBER_TYPE.getCode());
486                        recipient.setRecipientId(userRecipientId);
487                        notification.addRecipient(recipient);
488                    }
489                }
490            }
491    
492            if (workgroupRecipients != null && workgroupRecipients.length > 0) {
493                recipientsExist = true;
494                if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
495                    if (workgroupNamespaceCodes.length == workgroupRecipients.length) {
496                        for (int i = 0; i < workgroupRecipients.length; i++) {
497                            if (isWorkgroupRecipientValid(workgroupRecipients[i], workgroupNamespaceCodes[i], errors)) {
498                                NotificationRecipientBo recipient = new NotificationRecipientBo();
499                                recipient.setRecipientType(KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode());
500                                recipient.setRecipientId(
501                                        getGroupService().getGroupByNamespaceCodeAndName(workgroupNamespaceCodes[i],
502                                                workgroupRecipients[i]).getId());
503                                notification.addRecipient(recipient);
504                            }
505                        }
506                    } else {
507                        errors.addError("The number of groups must match the number of namespace codes");
508                    }
509                } else {
510                    errors.addError("You must specify a namespace code for every group name");
511                }
512            } else if (workgroupNamespaceCodes != null && workgroupNamespaceCodes.length > 0) {
513                errors.addError("You must specify a group name for every namespace code");
514            }
515    
516            // check to see if there were any errors
517            if (errors.getErrors().size() > 0) {
518                throw errors;
519            }
520    
521            notification.setTitle(title);
522    
523            notification.setDeliveryType(deliveryType);
524    
525            // simpledateformat is not threadsafe, have to sync and validate
526            Date d = null;
527            if (StringUtils.isNotBlank(sendDateTime)) {
528                try {
529                    d = Util.parseUIDateTime(sendDateTime);
530                } catch (ParseException pe) {
531                    errors.addError("You specified an invalid send date and time.  Please use the calendar picker.");
532                }
533                notification.setSendDateTimeValue(new Timestamp(d.getTime()));
534            }
535    
536            Date d2 = null;
537            if (StringUtils.isNotBlank(autoRemoveDateTime)) {
538                try {
539                    d2 = Util.parseUIDateTime(autoRemoveDateTime);
540                    if (d2.before(d)) {
541                        errors.addError("Auto Remove Date/Time cannot be before Send Date/Time.");
542                    }
543                } catch (ParseException pe) {
544                    errors.addError("You specified an invalid auto remove date and time.  Please use the calendar picker.");
545                }
546                notification.setAutoRemoveDateTimeValue(new Timestamp(d2.getTime()));
547            }
548    
549            if (!recipientsExist && !hasPotentialRecipients(notification)) {
550                errors.addError("You must specify at least one user or group recipient.");
551            }
552    
553            // check to see if there were any errors
554            if (errors.getErrors().size() > 0) {
555                throw errors;
556            }
557    
558            notification
559                    .setContent(NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_SIMPLE_OPEN
560                            + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_OPEN
561                            + message
562                            + NotificationConstants.XML_MESSAGE_CONSTANTS.MESSAGE_CLOSE
563                            + NotificationConstants.XML_MESSAGE_CONSTANTS.CONTENT_CLOSE);
564    
565            return notification;
566        }
567    }