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}