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