View Javadoc
1   /**
2    * Copyright 2005-2015 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.criteria.QueryByCriteria;
19  import org.kuali.rice.core.api.util.xml.XmlException;
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.NotificationSenderBo;
24  import org.kuali.rice.ken.dao.NotificationDao;
25  import org.kuali.rice.ken.bo.NotificationResponseBo;
26  import org.kuali.rice.ken.deliverer.impl.KEWActionListMessageDeliverer;
27  import org.kuali.rice.ken.service.NotificationAuthorizationService;
28  import org.kuali.rice.ken.service.NotificationMessageContentService;
29  import org.kuali.rice.ken.service.NotificationMessageDeliveryService;
30  import org.kuali.rice.ken.service.NotificationRecipientService;
31  import org.kuali.rice.ken.service.NotificationService;
32  import org.kuali.rice.ken.util.NotificationConstants;
33  import org.kuali.rice.krad.data.DataObjectService;
34  
35  import java.io.IOException;
36  import java.sql.Timestamp;
37  import java.util.ArrayList;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.List;
41  
42  import static org.kuali.rice.core.api.criteria.PredicateFactory.equal;
43  
44  /**
45   * NotificationService implementation - this is the default out-of-the-box implementation of the service.
46   * @author Kuali Rice Team (rice.collab@kuali.org)
47   */
48  public class NotificationServiceImpl implements NotificationService {
49  	private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
50  	.getLogger(NotificationServiceImpl.class);
51  
52      private DataObjectService dataObjectService;
53      private NotificationDao notDao;
54  	private NotificationMessageContentService messageContentService;
55  	private NotificationAuthorizationService notificationAuthorizationService;
56  	private NotificationRecipientService notificationRecipientService;
57  	private NotificationMessageDeliveryService notificationMessageDeliveryService;
58  
59  	/**
60  	 * Constructs a NotificationServiceImpl class instance.
61  	 * @param dataObjectService persistence layer to datasource.
62  	 * @param messageContentService
63  	 * @param notificationAuthorizationService
64  	 * @param notificationRecipientService
65  	 * @param notificationMessageDeliveryService
66  	 */
67  	public NotificationServiceImpl(DataObjectService dataObjectService, NotificationMessageContentService messageContentService,
68  			NotificationAuthorizationService notificationAuthorizationService, NotificationRecipientService notificationRecipientService,
69  			NotificationMessageDeliveryService notificationMessageDeliveryService,
70              NotificationDao notDao) {
71  		this.dataObjectService = dataObjectService;
72  		this.messageContentService = messageContentService;
73  		this.notificationAuthorizationService = notificationAuthorizationService;
74  		this.notificationRecipientService = notificationRecipientService;
75  		this.notificationMessageDeliveryService = notificationMessageDeliveryService;
76          this.notDao = notDao;
77  	}
78  
79  	/**
80  	 * This is the default implementation that uses the businessObjectDao.
81  	 * @see org.kuali.rice.ken.service.NotificationService#getNotification(java.lang.Long)
82  	 */
83  	@Override
84      public NotificationBo getNotification(Long id) {
85  
86  		return dataObjectService.find(NotificationBo.class, id);
87  
88  	}
89  
90  	/**
91  	 * This method is responsible for parsing out the notification message which is sent in as a String 
92  	 * of XML.  It calls the appropriate services to validate the message content, converts it to a BO, 
93  	 * and then passes it to another service where its content and meta-data is validated and if successful, it 
94  	 * is saved.
95  	 * @see org.kuali.rice.ken.service.NotificationService#sendNotification(java.lang.String)
96  	 */
97  	@Override
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 	@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 }