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.ken.bo.NotificationBo;
019import org.kuali.rice.ken.bo.NotificationMessageDelivery;
020import org.kuali.rice.ken.bo.NotificationRecipientBo;
021import org.kuali.rice.ken.bo.NotificationRecipientListBo;
022import org.kuali.rice.ken.bo.UserChannelSubscriptionBo;
023import org.kuali.rice.ken.deliverer.impl.KEWActionListMessageDeliverer;
024import org.kuali.rice.ken.exception.NotificationMessageDeliveryException;
025import org.kuali.rice.ken.service.NotificationMessageDeliveryResolverService;
026import org.kuali.rice.ken.service.NotificationRecipientService;
027import org.kuali.rice.ken.service.NotificationService;
028import org.kuali.rice.ken.service.ProcessingResult;
029import org.kuali.rice.ken.util.NotificationConstants;
030import org.kuali.rice.kim.api.KimConstants.KimGroupMemberTypes;
031import org.kuali.rice.kim.api.identity.principal.Principal;
032import org.kuali.rice.kim.api.services.KimApiServiceLocator;
033import org.kuali.rice.krad.data.DataObjectService;
034import org.springframework.transaction.PlatformTransactionManager;
035
036import java.sql.Timestamp;
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.HashSet;
040import java.util.Iterator;
041import java.util.List;
042import java.util.concurrent.ExecutorService;
043
044
045/**
046 * This is the default out-of-the-box implementation that leverages the status flag on a notification (RESOLVED versus UNRESOLVED) to determine whether
047 * the notification's message deliveries need to be resolved or not.  This also looks at the start and auto remove
048 * dates and times.
049 * @author Kuali Rice Team (rice.collab@kuali.org)
050 */
051public class NotificationMessageDeliveryResolverServiceImpl extends ConcurrentJob<NotificationBo> implements NotificationMessageDeliveryResolverService {
052    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
053        .getLogger(NotificationMessageDeliveryResolverServiceImpl.class);
054
055    private NotificationRecipientService notificationRecipientService;
056    private DataObjectService dataObjectService;
057    private NotificationService notificationService;
058
059    /**
060     * Constructs a NotificationMessageDeliveryDispatchServiceImpl instance.
061     * @param notificationRecipientService
062     * @param dataObjectService
063     * @param txManager
064     * @param executor
065     */
066    public NotificationMessageDeliveryResolverServiceImpl(NotificationService notificationService, NotificationRecipientService notificationRecipientService,
067            DataObjectService dataObjectService, PlatformTransactionManager txManager, ExecutorService executor) {
068        super(txManager, executor);
069        this.notificationService = notificationService;
070        this.notificationRecipientService = notificationRecipientService;
071        this.dataObjectService = dataObjectService;
072    }
073
074    /**
075     * Obtains and marks as taken all unresolved (and untaken) notifications
076     * @return a collection of available Notifications to process
077     */
078    @Override
079    protected Collection<NotificationBo> takeAvailableWorkItems() {
080        Collection<NotificationBo> nots = notificationService.takeNotificationsForResolution();
081        //LOG.debug("Took " + nots.size() + " notifications");
082        
083        //for (Notification not: nots) {
084        //   LOG.debug("Took notification: " + not.getId() + " " + not.getTitle());
085        //}
086        return nots;
087    }
088
089
090    /**
091     * This method is responsible for building out the complete recipient list, which will resolve all members for groups, and add
092     * them to the official list only if they are not already in the list.
093     * @param notification
094     * @return HashSet<String>
095     */
096    private HashSet<String> buildCompleteRecipientList(NotificationBo notification) {
097        HashSet<String> completeRecipientList = new HashSet<String>(notification.getRecipients().size());
098
099        // process the list that came in with the notification request
100           for (int i = 0; i < notification.getRecipients().size(); i++) {
101               NotificationRecipientBo recipient = notification.getRecipient(i);
102               if (KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode().equals(recipient.getRecipientType())) {
103                   // resolve group's users
104                   String[] groupMembers = notificationRecipientService.getGroupMembers(recipient.getRecipientId());
105                   for(int j = 0; j < groupMembers.length; j++) {
106                       completeRecipientList.add(groupMembers[j]);
107                   }
108               } else {  // just a user, so add to the list
109                   Principal principal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(recipient.getRecipientId());
110                   completeRecipientList.add(principal.getPrincipalId());
111               }
112           }
113
114           // now process the default recipient lists that are associated with the channel
115           Iterator<NotificationRecipientListBo> i = notification.getChannel().getRecipientLists().iterator();
116           while (i.hasNext()) {
117               NotificationRecipientListBo listRecipient  = i.next();
118               if (KimGroupMemberTypes.GROUP_MEMBER_TYPE.getCode().equals(listRecipient.getRecipientType())) {
119                   // resolve group's users
120                   String[] groupMembers = notificationRecipientService.getGroupMembers(listRecipient.getRecipientId());
121                   for (int j = 0; j < groupMembers.length; j++) {
122                       completeRecipientList.add(groupMembers[j]);
123                   }
124               } else {  // just a user, so add to the list
125                   Principal principal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(listRecipient.getRecipientId());
126                           completeRecipientList.add(principal.getPrincipalId());
127               }
128           }
129
130           // now process the subscribers that are associated with the channel
131           List<UserChannelSubscriptionBo> subscriptions = notification.getChannel().getSubscriptions();
132           for (UserChannelSubscriptionBo subscription: subscriptions) {
133               // NOTE: at this time channel subscriptions are USER-only - GROUP is not supported
134               // this could be implemented by adding a recipientType/userType column as we do in
135               // other recipient/user-related tables/BOs
136               completeRecipientList.add(subscription.getUserId());
137           }
138
139           return completeRecipientList;
140    }
141
142    /**
143     * Generates all message deliveries for a given notification and save thems to the database.
144     * Updates each Notification record to indicate it has been resolved.
145     * Should be performed within a separate transaction
146     * @param notifications the Notification for which to generate message deliveries
147     * @return a count of the number of message deliveries generated
148     */
149    /* Perform within transaction */
150    @Override
151    protected Collection<Object> processWorkItems(Collection<NotificationBo> notifications) {
152        List<Object> successes = new ArrayList<Object>();
153
154        // because this concurrent job does not performed grouping of work items, there should only
155        // ever be one notification object per work unit anyway...
156        for (NotificationBo notification: notifications) {
157            // now figure out each unique recipient for this notification
158            HashSet<String> uniqueRecipients = buildCompleteRecipientList(notification);
159
160            // now for each unique recipient, figure out each delivery end point and create a NotificationMessageDelivery record
161            Iterator<String> j = uniqueRecipients.iterator();
162            while(j.hasNext()) {
163                String userRecipientId = j.next();
164
165                NotificationMessageDelivery defaultMessageDelivery = new NotificationMessageDelivery();
166                defaultMessageDelivery.setMessageDeliveryStatus(NotificationConstants.MESSAGE_DELIVERY_STATUS.UNDELIVERED);
167                defaultMessageDelivery.setNotification(notification);
168                defaultMessageDelivery.setUserRecipientId(userRecipientId);
169
170                //now save that delivery end point; this record will be later processed by the dispatch service which will actually deliver it
171               defaultMessageDelivery =  dataObjectService.save(defaultMessageDelivery);
172
173                try {
174                    new KEWActionListMessageDeliverer().deliverMessage(defaultMessageDelivery);
175                } catch (NotificationMessageDeliveryException e) {
176                    throw new RuntimeException(e);
177                }
178
179                // we have no delivery stage any more, anything we send to KCB needs to be considered "delivered" from
180                // the perspective of KEN
181                defaultMessageDelivery.setMessageDeliveryStatus(NotificationConstants.MESSAGE_DELIVERY_STATUS.DELIVERED);
182                defaultMessageDelivery = dataObjectService.save(defaultMessageDelivery);
183
184                successes.add(defaultMessageDelivery);
185
186                // also, update the status of the notification so that it's message deliveries are not resolved again
187                notification.setProcessingFlag(NotificationConstants.PROCESSING_FLAGS.RESOLVED);
188                // unlock the record now
189                notification.setLockedDateValue(null);
190                dataObjectService.save(notification);
191            }
192
193        }
194
195        return successes;
196    }
197
198    /**
199     * @see org.kuali.rice.ken.service.impl.ConcurrentJob#unlockWorkItem(java.lang.Object)
200     */
201    @Override
202    protected void unlockWorkItem(NotificationBo notification) {
203        LOG.debug("Unlocking notification: " + notification.getId() + " " + notification.getTitle());
204        notificationService.unlockNotification(notification);
205    }
206
207    /**
208     * This method is responsible for resolving the list of NotificationMessageDelivery records for a given notification.  This service will look
209     * at all notifications that are ready to be delivered and will "explode" out specific message delivery records for given delivery end points.
210     * @see org.kuali.rice.ken.service.NotificationMessageDeliveryResolverService#resolveNotificationMessageDeliveries()
211     */
212    public ProcessingResult resolveNotificationMessageDeliveries() {
213        LOG.debug("[" + new Timestamp(System.currentTimeMillis()).toString() + "] STARTING RESOLUTION OF NOTIFICATION MESSAGE DELIVERIES");
214
215        ProcessingResult result = run();
216
217        LOG.debug("[" + new Timestamp(System.currentTimeMillis()).toString() + "] FINISHED RESOLUTION OF NOTIFICATION MESSAGE DELIVERIES - " +
218                  "Message Delivery End Points Resolved = " + result.getSuccesses().size());
219
220        return result;
221    }
222}