View Javadoc

1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.ken.service.impl;
17  
18  import org.kuali.rice.core.api.util.xml.XmlException;
19  import org.kuali.rice.core.framework.persistence.dao.GenericDao;
20  import org.kuali.rice.ken.bo.NotificationBo;
21  import org.kuali.rice.ken.bo.NotificationMessageDelivery;
22  import org.kuali.rice.ken.bo.NotificationRecipientBo;
23  import org.kuali.rice.ken.bo.NotificationResponseBo;
24  import org.kuali.rice.ken.dao.NotificationDao;
25  import org.kuali.rice.ken.deliverer.impl.KEWActionListMessageDeliverer;
26  import org.kuali.rice.ken.service.NotificationAuthorizationService;
27  import org.kuali.rice.ken.service.NotificationMessageContentService;
28  import org.kuali.rice.ken.service.NotificationMessageDeliveryService;
29  import org.kuali.rice.ken.service.NotificationRecipientService;
30  import org.kuali.rice.ken.service.NotificationService;
31  import org.kuali.rice.ken.service.NotificationWorkflowDocumentService;
32  import org.kuali.rice.ken.util.NotificationConstants;
33  
34  import java.io.IOException;
35  import java.sql.Timestamp;
36  import java.util.Collection;
37  import java.util.HashMap;
38  
39  //import org.kuali.rice.core.jpa.criteria.Criteria;
40  
41  /**
42   * NotificationService implementation - this is the default out-of-the-box implementation of the service.
43   * @author Kuali Rice Team (rice.collab@kuali.org)
44   */
45  public class NotificationServiceImpl implements NotificationService {
46  	private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
47  	.getLogger(NotificationServiceImpl.class);
48  
49  	private GenericDao businessObjectDao;
50  	private NotificationDao notDao;
51  	private NotificationMessageContentService messageContentService;
52  	private NotificationAuthorizationService notificationAuthorizationService;
53  	private NotificationRecipientService notificationRecipientService;
54  	private NotificationWorkflowDocumentService notificationWorkflowDocumentService;
55  	private NotificationMessageDeliveryService notificationMessageDeliveryService;
56  
57  	/**
58  	 * Constructs a NotificationServiceImpl class instance.
59  	 * @param businessObjectDao
60  	 * @param messageContentService
61  	 * @param notificationAuthorizationService
62  	 * @param notificationRecipientService
63  	 * @param notificationWorkflowDocumentService
64  	 * @param notificationMessageDeliveryService
65  	 */
66  	public NotificationServiceImpl(GenericDao businessObjectDao, NotificationMessageContentService messageContentService,
67  			NotificationAuthorizationService notificationAuthorizationService, NotificationRecipientService notificationRecipientService, 
68  			NotificationWorkflowDocumentService notificationWorkflowDocumentService, 
69  			NotificationMessageDeliveryService notificationMessageDeliveryService,
70  			NotificationDao notDao) {
71  		this.businessObjectDao = businessObjectDao;
72  		this.messageContentService = messageContentService;
73  		this.notificationAuthorizationService = notificationAuthorizationService;
74  		this.notificationRecipientService = notificationRecipientService;
75  		this.notificationWorkflowDocumentService = notificationWorkflowDocumentService;
76  		this.notificationMessageDeliveryService = notificationMessageDeliveryService;
77  		this.notDao = notDao;
78  	}
79  
80  	/**
81  	 * This is the default implementation that uses the businessObjectDao.
82  	 * @see org.kuali.rice.ken.service.NotificationService#getNotification(java.lang.Long)
83  	 */
84  	public NotificationBo getNotification(Long id) {
85  		HashMap<String, Long> primaryKeys = new HashMap<String, Long>();
86  		primaryKeys.put(NotificationConstants.BO_PROPERTY_NAMES.ID, id);
87  
88  		return (NotificationBo) businessObjectDao.findByPrimaryKey(NotificationBo.class, primaryKeys);
89  	}
90  
91  	/**
92  	 * This method is responsible for parsing out the notification message which is sent in as a String 
93  	 * of XML.  It calls the appropriate services to validate the message content, converts it to a BO, 
94  	 * and then passes it to another service where its content and meta-data is validated and if successful, it 
95  	 * is saved.
96  	 * @see org.kuali.rice.ken.service.NotificationService#sendNotification(java.lang.String)
97  	 */
98  	public NotificationResponseBo sendNotification(String notificationMessageAsXml) throws IOException, XmlException {
99  		// 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 }