001/**
002 * Copyright 2005-2015 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.service.impl;
017
018import org.kuali.rice.core.api.criteria.QueryByCriteria;
019import org.kuali.rice.core.api.util.xml.XmlException;
020import org.kuali.rice.ken.bo.NotificationBo;
021import org.kuali.rice.ken.bo.NotificationMessageDelivery;
022import org.kuali.rice.ken.bo.NotificationRecipientBo;
023import org.kuali.rice.ken.bo.NotificationSenderBo;
024import org.kuali.rice.ken.dao.NotificationDao;
025import org.kuali.rice.ken.bo.NotificationResponseBo;
026import org.kuali.rice.ken.deliverer.impl.KEWActionListMessageDeliverer;
027import org.kuali.rice.ken.service.NotificationAuthorizationService;
028import org.kuali.rice.ken.service.NotificationMessageContentService;
029import org.kuali.rice.ken.service.NotificationMessageDeliveryService;
030import org.kuali.rice.ken.service.NotificationRecipientService;
031import org.kuali.rice.ken.service.NotificationService;
032import org.kuali.rice.ken.util.NotificationConstants;
033import org.kuali.rice.krad.data.DataObjectService;
034
035import java.io.IOException;
036import java.sql.Timestamp;
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.Collections;
040import java.util.List;
041
042import static org.kuali.rice.core.api.criteria.PredicateFactory.equal;
043
044/**
045 * NotificationService implementation - this is the default out-of-the-box implementation of the service.
046 * @author Kuali Rice Team (rice.collab@kuali.org)
047 */
048public class NotificationServiceImpl implements NotificationService {
049        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
050        .getLogger(NotificationServiceImpl.class);
051
052    private DataObjectService dataObjectService;
053    private NotificationDao notDao;
054        private NotificationMessageContentService messageContentService;
055        private NotificationAuthorizationService notificationAuthorizationService;
056        private NotificationRecipientService notificationRecipientService;
057        private NotificationMessageDeliveryService notificationMessageDeliveryService;
058
059        /**
060         * Constructs a NotificationServiceImpl class instance.
061         * @param dataObjectService persistence layer to datasource.
062         * @param messageContentService
063         * @param notificationAuthorizationService
064         * @param notificationRecipientService
065         * @param notificationMessageDeliveryService
066         */
067        public NotificationServiceImpl(DataObjectService dataObjectService, NotificationMessageContentService messageContentService,
068                        NotificationAuthorizationService notificationAuthorizationService, NotificationRecipientService notificationRecipientService,
069                        NotificationMessageDeliveryService notificationMessageDeliveryService,
070            NotificationDao notDao) {
071                this.dataObjectService = dataObjectService;
072                this.messageContentService = messageContentService;
073                this.notificationAuthorizationService = notificationAuthorizationService;
074                this.notificationRecipientService = notificationRecipientService;
075                this.notificationMessageDeliveryService = notificationMessageDeliveryService;
076        this.notDao = notDao;
077        }
078
079        /**
080         * This is the default implementation that uses the businessObjectDao.
081         * @see org.kuali.rice.ken.service.NotificationService#getNotification(java.lang.Long)
082         */
083        @Override
084    public NotificationBo getNotification(Long id) {
085
086                return dataObjectService.find(NotificationBo.class, id);
087
088        }
089
090        /**
091         * This method is responsible for parsing out the notification message which is sent in as a String 
092         * of XML.  It calls the appropriate services to validate the message content, converts it to a BO, 
093         * and then passes it to another service where its content and meta-data is validated and if successful, it 
094         * is saved.
095         * @see org.kuali.rice.ken.service.NotificationService#sendNotification(java.lang.String)
096         */
097        @Override
098    public NotificationResponseBo sendNotification(String notificationMessageAsXml) throws IOException, XmlException {
099                // try to parse out the XML with the message content service
100                NotificationBo notification = messageContentService.parseNotificationRequestMessage(notificationMessageAsXml);
101
102                // now call out to the meat of the notification sending - this will validate users, groups, producers, and save
103                return sendNotification(notification);
104        }
105
106        /**
107         * @see org.kuali.rice.ken.service.NotificationService#sendNotification(org.kuali.rice.ken.bo.NotificationBo)
108         */
109        @Override
110    public NotificationResponseBo sendNotification(NotificationBo notification) {
111                NotificationResponseBo response = new NotificationResponseBo();
112
113                // make sure that the producer is able to send notifications on behalf of the channel
114                boolean producerAuthorizedForChannel = notificationAuthorizationService.isProducerAuthorizedToSendNotificationForChannel(notification.getProducer(), notification.getChannel());
115                if(!producerAuthorizedForChannel) {
116                        LOG.error("Producer " + notification.getProducer() + " is not authorized to send messages to channel " + notification.getChannel());
117                        response.setStatus(NotificationConstants.RESPONSE_STATUSES.FAILURE);
118                        response.setMessage(NotificationConstants.RESPONSE_MESSAGES.PRODUCER_NOT_AUTHORIZED_FOR_CHANNEL);
119                        return response;
120                }
121
122                // make sure that the recipients are valid
123                for(int i = 0; i < notification.getRecipients().size(); i++) {
124                        NotificationRecipientBo recipient = notification.getRecipient(i);
125            if (recipient.getNotification() == null) {
126                recipient.setNotification(notification);
127            }
128                        boolean validRecipient = notificationRecipientService.isRecipientValid(recipient.getRecipientId(), recipient.getRecipientType());
129                        if(!validRecipient) {
130                                response.setStatus(NotificationConstants.RESPONSE_STATUSES.FAILURE);
131                                response.setMessage(NotificationConstants.RESPONSE_MESSAGES.INVALID_RECIPIENT + " - recipientId=" + 
132                                                recipient.getRecipientId() + ", recipientType=" + recipient.getRecipientType());
133                                return response;
134                        }
135                }
136
137        // ensure the notification is set for all the senders
138        for (NotificationSenderBo sender : notification.getSenders()) {
139            sender.setNotification(notification);
140        }
141
142                // set the creationDateTime attribute to the current timestamp if it's currently null
143                if (notification.getCreationDateTime() == null) {
144                        notification.setCreationDateTimeValue(new Timestamp(System.currentTimeMillis()));
145                }
146
147                // set the sendDateTime attribute to the current timestamp if it's currently null
148                if(notification.getSendDateTime() == null) {
149                        notification.setSendDateTimeValue(new Timestamp(System.currentTimeMillis()));
150                }
151
152                // if the autoremove time is before the send date time, reject the notification
153                if (notification.getAutoRemoveDateTime() != null) {
154                        if (notification.getAutoRemoveDateTimeValue().before(notification.getSendDateTimeValue()))  {
155                                response.setStatus(NotificationConstants.RESPONSE_STATUSES.FAILURE);
156                                response.setMessage(NotificationConstants.RESPONSE_MESSAGES.INVALID_REMOVE_DATE);
157                                return response;
158                        }
159                }
160
161                // make sure the delivery types are valid
162                if(!notification.getDeliveryType().equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.ACK) && 
163                                !notification.getDeliveryType().equalsIgnoreCase(NotificationConstants.DELIVERY_TYPES.FYI)) {
164                        response.setStatus(NotificationConstants.RESPONSE_STATUSES.FAILURE);
165                        response.setMessage(NotificationConstants.RESPONSE_MESSAGES.INVALID_DELIVERY_TYPE + " - deliveryType=" + 
166                                        notification.getDeliveryType());
167                        return response;
168                }
169
170                // now try to persist the object
171                try {
172                        notification = dataObjectService.save(notification);
173                } catch(Exception e) {
174                        response.setStatus(NotificationConstants.RESPONSE_STATUSES.FAILURE);
175                        response.setMessage(NotificationConstants.RESPONSE_MESSAGES.ERROR_SAVING_NOTIFICATION);
176                        return response;
177                }
178
179                // everything looks good!
180                response.setMessage(NotificationConstants.RESPONSE_MESSAGES.SUCCESSFULLY_RECEIVED);
181                response.setNotificationId(notification.getId());
182                return response;
183        }
184
185        /**
186         * This is the default implementation that uses the businessObjectDao and its findMatching method.
187         * @see org.kuali.rice.ken.service.NotificationService#getNotificationsForRecipientByType(java.lang.String, java.lang.String)
188         */
189        @Override
190    public Collection getNotificationsForRecipientByType(String contentTypeName, String recipientId) {
191        QueryByCriteria.Builder criteria = QueryByCriteria.Builder.create();
192        criteria.setPredicates(equal(NotificationConstants.BO_PROPERTY_NAMES.CONTENT_TYPE_NAME, contentTypeName),
193                equal(NotificationConstants.BO_PROPERTY_NAMES.RECIPIENTS_RECIPIENT_ID, recipientId));
194
195                return Collections.unmodifiableCollection(dataObjectService.findMatching(NotificationBo.class, criteria.build()).getResults());
196        }
197
198        /**
199         * @see org.kuali.rice.ken.service.NotificationService#dismissNotificationMessageDelivery(java.lang.Long, java.lang.String)
200         */
201        @Override
202    public void dismissNotificationMessageDelivery(Long id, String user, String cause) {
203                // TODO: implement pessimistic locking on the message delivery
204                NotificationMessageDelivery nmd = notificationMessageDeliveryService.getNotificationMessageDelivery(id);
205                dismissNotificationMessageDelivery(nmd, user, cause);
206        }
207
208        /**
209         * @see org.kuali.rice.ken.service.NotificationService#dismissNotificationMessageDelivery(org.kuali.rice.ken.bo.NotificationMessageDelivery, java.lang.String, java.lang.String)
210         */   
211        public void dismissNotificationMessageDelivery(NotificationMessageDelivery nmd, String user, String cause) {
212                // get the notification that generated this particular message delivery
213                NotificationBo notification = nmd.getNotification();
214
215                // get all of the other deliveries of this notification for the user
216                Collection<NotificationMessageDelivery> userDeliveries = notificationMessageDeliveryService.getNotificationMessageDeliveries(notification, nmd.getUserRecipientId());
217
218                final String targetStatus;
219                // if the cause was our internal "autoremove" cause, then we need to indicate
220                // the message was autoremoved instead of normally dismissed
221                if (NotificationConstants.AUTO_REMOVE_CAUSE.equals(cause)) {
222                        targetStatus = NotificationConstants.MESSAGE_DELIVERY_STATUS.AUTO_REMOVED;
223                } else {
224                        targetStatus = NotificationConstants.MESSAGE_DELIVERY_STATUS.REMOVED;
225                }
226
227                KEWActionListMessageDeliverer deliverer = new KEWActionListMessageDeliverer();
228                // TODO: implement pessimistic locking on all these message deliveries
229                // now, do dispatch in reverse...dismiss each message delivery via the appropriate deliverer
230                for (NotificationMessageDelivery messageDelivery: userDeliveries) {
231
232                        // don't attempt to dismiss undelivered message deliveries
233                        if (!NotificationConstants.MESSAGE_DELIVERY_STATUS.DELIVERED.equals(messageDelivery.getMessageDeliveryStatus())) {
234                                LOG.info("Skipping dismissal of non-delivered message delivery #" + messageDelivery.getId());
235                        } else if (targetStatus.equals(messageDelivery.getMessageDeliveryStatus())) {
236                                LOG.info("Skipping dismissal of already removed message delivery #" + messageDelivery.getId());
237                        } else {
238                                LOG.debug("Dismissing message delivery #" + messageDelivery.getId() + " " + messageDelivery.getVersionNumber());//.getLockVerNbr());
239
240                                // we have our message deliverer, so tell it to dismiss the message
241                                //try {
242                                deliverer.dismissMessageDelivery(messageDelivery, user, cause);
243                                //} catch (NotificationMessageDismissalException nmde) {
244                                //LOG.error("Error dismissing message " + messageDelivery, nmde);
245                                //throw new RuntimeException(nmde);
246                                //}
247                        }
248
249                        // by definition we have succeeded at this point if no exception was thrown by the messageDeliverer
250                        // so update the status of the delivery message instance to indicate its dismissal
251                        // if the message delivery was not actually delivered in the first place, we still need to mark it as
252                        // removed here so delivery is not attempted again
253                        messageDelivery.setMessageDeliveryStatus(targetStatus);
254                        // TODO: locking
255                        // mark as unlocked
256                        //messageDelivery.setLockedDate(null);
257                        LOG.debug("Saving message delivery #" + messageDelivery.getId() + " " + messageDelivery.getVersionNumber());
258                        messageDelivery = dataObjectService.save(messageDelivery);
259
260                        LOG.debug("Message delivery '" + messageDelivery.getId() + "' for notification '" + messageDelivery.getNotification().getId() + "' was successfully dismissed.");
261                }
262        }
263
264        /**
265         * This method is responsible for atomically finding all untaken, unresolved notifications that are ready to be sent,
266         * marking them as taken and returning them to the caller for processing.
267         * NOTE: it is important that this method execute in a SEPARATE dedicated transaction; either the caller should
268         * NOT be wrapped by Spring declarative transaction and this service should be wrapped (which is the case), or
269         * the caller should arrange to invoke this from within a newly created transaction).
270         * @return a list of available notifications that have been marked as taken by the caller
271         */
272        //switch to JPA criteria
273        @Override
274    public Collection<NotificationBo> takeNotificationsForResolution() {
275                // get all unprocessed notifications with sendDateTime <= current
276                Collection<NotificationBo> available_notifications = notDao.findMatchedNotificationsForResolution(new Timestamp(System.currentTimeMillis()), dataObjectService);
277        List<NotificationBo> savedNotifications = new ArrayList<NotificationBo>();
278                //LOG.debug("Available notifications: " + available_notifications.size());
279
280                // mark as "taken"
281                if (available_notifications != null) {
282                        for (NotificationBo notification: available_notifications) {
283                                LOG.info("notification: " + notification);
284                                notification.setLockedDateValue(new Timestamp(System.currentTimeMillis()));
285                                savedNotifications.add(dataObjectService.save(notification));
286                        }
287                }
288
289                return savedNotifications;
290        }
291
292        /**
293         * Unlocks specified notification
294         * @param notification the notification object to unlock
295         */
296        //switch to JPA criteria
297        @Override
298    public void unlockNotification(NotificationBo notification) {
299                Collection<NotificationBo> notifications = notDao.findMatchedNotificationsForUnlock(notification, dataObjectService);
300                
301                if (notifications == null || notifications.size() == 0) {
302                        throw new RuntimeException("Notification #" + notification.getId() + " not found to unlock");
303                }
304
305                NotificationBo n = notifications.iterator().next();
306                n.setLockedDateValue(null);
307
308                dataObjectService.save(n);
309        }
310}