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}