/*
* Copyright (c) 2010-2011 Lockheed Martin Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.eurekastreams.server.action.execution.notification;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eurekastreams.commons.actions.TaskHandlerExecutionStrategy;
import org.eurekastreams.commons.actions.context.ActionContext;
import org.eurekastreams.commons.actions.context.TaskHandlerActionContext;
import org.eurekastreams.commons.exceptions.ExecutionException;
import org.eurekastreams.commons.logging.LogFactory;
import org.eurekastreams.commons.server.UserActionRequest;
import org.eurekastreams.server.action.execution.notification.filter.RecipientFilter;
import org.eurekastreams.server.action.execution.notification.notifier.Notifier;
import org.eurekastreams.server.action.execution.notification.translator.NotificationTranslator;
import org.eurekastreams.server.action.request.notification.CreateNotificationsRequest;
import org.eurekastreams.server.action.request.notification.CreateNotificationsRequest.RequestType;
import org.eurekastreams.server.domain.NotificationFilterPreferenceDTO;
import org.eurekastreams.server.domain.NotificationType;
import org.eurekastreams.server.domain.Property;
import org.eurekastreams.server.domain.PropertyHashMap;
import org.eurekastreams.server.domain.PropertyMap;
import org.eurekastreams.server.persistence.LazyLoadPropertiesMap;
import org.eurekastreams.server.persistence.mappers.DomainMapper;
import org.eurekastreams.server.persistence.mappers.requests.notification.GetNotificationFilterPreferenceRequest;
import org.eurekastreams.server.search.modelview.PersonModelView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Async action to generate notifications.
*/
public class CreateNotificationsExecution implements TaskHandlerExecutionStrategy<ActionContext>
{
/** Local logger instance. */
private final Logger log = LoggerFactory.getLogger(LogFactory.getClassName());
/** Map of valid translators. */
private final Map<RequestType, NotificationTranslator> translators;
/** List of notifiers that should be executed. */
private final Map<String, Notifier> notifiers;
/** Mapper to filter out unwanted notifications per recipient. */
private final DomainMapper<GetNotificationFilterPreferenceRequest, List<NotificationFilterPreferenceDTO>> // \n
preferencesMapper;
/** Mapper to get people for filtering (determining locked users, etc.). */
private final DomainMapper<List<Long>, List<PersonModelView>> personsMapper;
/** Provides the category for each notification type. */
private final Map<NotificationType, String> notificationTypeToCategory;
/** Recipient-based filter strategies per notifier type. */
private final Map<String, Collection<RecipientFilter>> recipientFilters;
/** Recipient-independent filter strategies per notifier type. */
private final Map<String, Collection<RecipientFilter>> bulkFilters;
/** Mappers for loading notification properties. */
private final Map<Class, DomainMapper<Serializable, Object>> propertyLoadMappers;
/** Properties provided to all notifications. */
private final Map<String, Property<Object>> defaultProperties;
/**
* Constructor.
*
* @param inTranslators
* map of translators to set.
* @param inNotifiers
* list of notifiers to set.
* @param inPreferencesMapper
* preferences mapper to set.
* @param inPersonsMapper
* Mapper to get people for filtering.
* @param inNotificationTypeCategories
* Map providing the category for each notification type.
* @param inBulkFilters
* Bulk filter strategies per notifier type.
* @param inRecipientFilters
* Recipient filter strategies per notifier type.
* @param inDefaultProperties
* Properties provided to all notifications.
* @param inPropertyLoadMappers
* Mappers for loading notification properties.
*/
public CreateNotificationsExecution(
final Map<RequestType, NotificationTranslator> inTranslators,
final Map<String, Notifier> inNotifiers,
final DomainMapper<GetNotificationFilterPreferenceRequest, List<NotificationFilterPreferenceDTO>> // \n
inPreferencesMapper, final DomainMapper<List<Long>, List<PersonModelView>> inPersonsMapper,
final Map<NotificationType, String> inNotificationTypeCategories,
final Map<String, Collection<RecipientFilter>> inBulkFilters,
final Map<String, Collection<RecipientFilter>> inRecipientFilters,
final Map<String, Property<Object>> inDefaultProperties,
final Map<Class, DomainMapper<Serializable, Object>> inPropertyLoadMappers)
{
translators = inTranslators;
notifiers = inNotifiers;
preferencesMapper = inPreferencesMapper;
personsMapper = inPersonsMapper;
notificationTypeToCategory = inNotificationTypeCategories;
bulkFilters = inBulkFilters;
recipientFilters = inRecipientFilters;
defaultProperties = inDefaultProperties;
propertyLoadMappers = inPropertyLoadMappers;
}
@Override
public Serializable execute(final TaskHandlerActionContext<ActionContext> inActionContext)
throws ExecutionException
{
CreateNotificationsRequest currentRequest = (CreateNotificationsRequest) inActionContext.getActionContext()
.getParams();
log.info("Generating notifications for {}", currentRequest.getType());
// ---- translate event to notifications ----
NotificationTranslator translator = translators.get(currentRequest.getType());
if (translator == null)
{
// exit if notification request type is disabled
return Boolean.FALSE;
}
NotificationBatch batch = translator.translate(currentRequest);
if (batch == null || batch.getRecipients().isEmpty())
{
return Boolean.TRUE;
}
// ---- prepare for filtering ----
List<NotificationFilterPreferenceDTO> recipientFilterPreferences = null;
// build a list of all recipients
Map<Long, PersonModelView> recipientIndex = buildRecipientIndex(batch);
// build a list of categories from the notifications. only preference-filterable notifications have a category
Set<String> categories = new HashSet<String>();
for (NotificationType type : batch.getRecipients().keySet())
{
String category = notificationTypeToCategory.get(type);
if (category != null)
{
categories.add(category);
}
}
// if the list is not empty, fetch the preferences
if (!categories.isEmpty())
{
recipientFilterPreferences = preferencesMapper.execute(new GetNotificationFilterPreferenceRequest(
recipientIndex.keySet(), categories));
}
// build the map containing the properties of the notification batch
PropertyMap<Object> propertyList = new PropertyHashMap<Object>();
propertyList.putAll(defaultProperties);
propertyList.putAll(batch.getProperties());
Map<String, Object> properties = new LazyLoadPropertiesMap<Object>(propertyList, propertyLoadMappers);
List<UserActionRequest> asyncRequests = inActionContext.getUserActionRequests();
for (Entry<NotificationType, Collection<Long>> notification : batch.getRecipients().entrySet())
{
NotificationType type = notification.getKey();
Collection<Long> recipientIds = notification.getValue();
for (String notifierKey : notifiers.keySet())
{
log.debug("Filtering {} recipients for notifier {} from this list: {}", new Object[] { type,
notifierKey, recipientIds });
// filter
Collection<Long> filteredRecipients = filterRecipients(recipientIds, type, properties, notifierKey,
recipientFilterPreferences, recipientIndex);
if (!filteredRecipients.isEmpty())
{
try
{
log.info("Sending notification {} via {} to {}", new Object[] { type, notifierKey,
filteredRecipients });
// send
Collection<UserActionRequest> actionRequests = notifiers.get(notifierKey).notify(type,
filteredRecipients, properties, recipientIndex);
if (actionRequests != null && !actionRequests.isEmpty())
{
asyncRequests.addAll(actionRequests);
}
}
catch (Exception ex)
{
log.error("Failed to send notifications from " + notifierKey + " for " + type, ex);
}
}
}
}
return Boolean.TRUE;
}
/**
* Creates a map of all recipient persons for the entire notification batch. This could return a lazy-loading map
* without the outer code realizing the difference. My current thinking is that most persons will be referenced
* somewhere along the way, either in the filtering or in the notifying, plus it is more efficient to ask for them
* in bulk than one at a time, so get them all up front. The truly massive case is someone posting to a stream that
* many people have subscribed to; this involves sending email, and the email notifier references the
* PersonModelView, so the lookup will not go to waste.
*
* @param batch
* Notification batch.
* @return Map of person ID to PersonModelView of all recipients.
*/
private Map<Long, PersonModelView> buildRecipientIndex(final NotificationBatch batch)
{
List<Long> allRecipientIds = new ArrayList<Long>();
for (Collection<Long> recipientIds : batch.getRecipients().values())
{
allRecipientIds.addAll(recipientIds);
}
Map<Long, PersonModelView> recipientIndex = new HashMap<Long, PersonModelView>();
for (PersonModelView person : personsMapper.execute(allRecipientIds))
{
recipientIndex.put(person.getId(), person);
}
return recipientIndex;
}
/**
* Filters out notification recipients based on per-recipient settings.
*
* @param unfilteredRecipients
* the list of all recipient ids for the notification, unfiltered.
* @param type
* Type of notification.
* @param properties
* Notification details.
* @param notifierType
* the key string for the notifier itself.
* @param preferences
* the list of all notification preferences for users in the the allRecipient list.
* @param recipientIndex
* Index of all recipients for looking up PersonModelViews.
*
* @return the filtered list of recipient ids.
*/
private Collection<Long> filterRecipients(final Collection<Long> unfilteredRecipients,
final NotificationType type, final Map<String, Object> properties, final String notifierType,
final List<NotificationFilterPreferenceDTO> preferences, final Map<Long, PersonModelView> recipientIndex)
{
// apply bulk filters first
Collection<RecipientFilter> filters = bulkFilters.get(notifierType);
if (filters != null)
{
for (RecipientFilter filter : filters)
{
if (filter.shouldFilter(type, null, properties, notifierType))
{
// rejection by a bulk filter means the notification should not be sent to any recipients
return Collections.EMPTY_LIST;
}
}
}
// optimization check (avoid copying collections)
filters = recipientFilters.get(notifierType);
String category = notificationTypeToCategory.get(type);
if ((filters == null || filters.isEmpty())
&& (category == null || preferences == null || preferences.isEmpty()))
{
return unfilteredRecipients;
}
List<Long> filteredRecipients = new ArrayList<Long>(unfilteredRecipients);
// preference filtering: remove any users who opted out of the notification (for the given transport)
if (category != null && preferences != null)
{
for (NotificationFilterPreferenceDTO preference : preferences)
{
if (preference.getNotifierType().equals(notifierType)
&& preference.getNotificationCategory().equals(category))
{
filteredRecipients.remove(preference.getPersonId());
}
}
}
// strategy filtering: apply each strategy to each recipient, remove rejected recipients
if (filters != null && !filters.isEmpty() && !filteredRecipients.isEmpty())
{
Iterator<Long> iter = filteredRecipients.iterator();
eachRecipient: while (iter.hasNext())
{
Long recipientId = iter.next();
PersonModelView recipient = recipientIndex.get(recipientId);
for (RecipientFilter filter : filters)
{
if (filter.shouldFilter(type, recipient, properties, notifierType))
{
iter.remove();
continue eachRecipient;
}
}
}
}
return filteredRecipients;
}
}