/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.watchlist.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.watchlist.internal.api.WatchListEvent;
import org.xwiki.watchlist.internal.api.WatchListStore;
import org.xwiki.watchlist.internal.api.WatchedElementType;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.plugin.activitystream.api.ActivityEvent;
import com.xpn.xwiki.plugin.activitystream.api.ActivityEventType;
import com.xpn.xwiki.plugin.activitystream.api.ActivityStream;
import com.xpn.xwiki.plugin.activitystream.plugin.ActivityStreamPlugin;
/**
* Default implementation for {@link WatchListEventMatcher}.
*
* @version $Id: a8c95bfc4bfd21c7cbd86cbf6a4d0f60a100a833 $
*/
@Component
@Singleton
public class DefaultWatchListEventMatcher implements WatchListEventMatcher
{
/**
* Events to match.
*/
private static final List<String> MATCHING_EVENT_TYPES = new ArrayList<String>()
{
{
add(ActivityEventType.CREATE);
add(ActivityEventType.UPDATE);
add(ActivityEventType.DELETE);
}
};
/**
* Logging framework.
*/
@Inject
private Logger logger;
/**
* Used to access the user's watched elements.
*/
@Inject
private WatchListStore store;
/**
* Context provider.
*/
@Inject
private Provider<XWikiContext> contextProvider;
/**
* Used to check view rights on the matched events.
*/
@Inject
private AuthorizationManager authorizationManager;
/**
* Used to resolve string references.
*/
@Inject
private DocumentReferenceResolver<String> resolver;
/**
* Used to convert {@link ActivityEvent}s to {@link WatchListEvent}s.
*/
@Inject
private WatchListEventConverter<ActivityEvent> eventConverter;
@Override
public List<WatchListEvent> getEventsSince(Date start)
{
List<WatchListEvent> events = new ArrayList<>();
XWikiContext context = getXWikiContext();
ActivityStream actStream =
((ActivityStreamPlugin) context.getWiki().getPlugin(ActivityStreamPlugin.PLUGIN_NAME, context))
.getActivityStream();
List<Object> parameters = new ArrayList<Object>();
parameters.add(start);
try {
// FIXME: Watch out for memory usage here, since the list of events could be huge in some cases.
List<ActivityEvent> rawEvents =
actStream.searchEvents(
"act.date > ? and act.type in ('" + StringUtils.join(MATCHING_EVENT_TYPES, "','") + "')", false,
true, 0, 0, parameters, context);
// If the page has been modified several times we want to display only one diff, if the page has been
// delete after update events we want to discard the update events since we won't be able to display
// diff from a deleted document. See WatchListEvent#addEvent(WatchListEvent) and
// WatchListEvent#equals(WatchListEvent).
for (ActivityEvent rawEvent : rawEvents) {
WatchListEvent event = this.eventConverter.convert(rawEvent);
int existingIndex = events.indexOf(event);
if (existingIndex == -1) {
// An event on a new document, add the new event.
events.add(event);
} else {
// An event on an existing document, add to the events of that document.
WatchListEvent existingCompositeEvent = events.get(existingIndex);
existingCompositeEvent.addEvent(event);
}
}
} catch (Exception e) {
logger.error("Failed to retrieve updated documents from activity stream since [{}]", start, e);
}
return events;
}
@Override
public List<WatchListEvent> getMatchingVisibleEvents(List<WatchListEvent> events, String subscriber)
{
List<WatchListEvent> result = new ArrayList<WatchListEvent>();
for (WatchListEvent event : events) {
if (isEventSkipped(event)) {
// Skip events that are on a blacklist for various reasons (performance, security, etc.)
continue;
}
if (!isEventViewable(event, subscriber)) {
// Skip events on documents that are not visible to the subscriber.
continue;
}
if (!isEventMatching(event, subscriber)) {
// Skip events that are not interesting to the subscriber.
continue;
}
result.add(event);
}
// Sort the matching events by document.
Collections.sort(result);
return result;
}
@Override
public boolean isEventMatching(WatchListEvent event, String subscriber)
{
boolean isWatched = false;
try {
// The subscriber's watched users, since each event can be a composite event.
Collection<String> watchedUsers = store.getWatchedElements(subscriber, WatchedElementType.USER);
isWatched |= store.isWatched(event.getWiki(), subscriber, WatchedElementType.WIKI);
isWatched |= store.isWatched(event.getPrefixedSpace(), subscriber, WatchedElementType.SPACE);
isWatched |= store.isWatched(event.getPrefixedFullName(), subscriber, WatchedElementType.DOCUMENT);
isWatched |= CollectionUtils.intersection(watchedUsers, event.getAuthors()).size() > 0;
} catch (Exception e) {
logger.error("Failed to determine if an event for the document [{}] is interesting to [{}]",
event.getDocumentReference(), subscriber, e);
}
return isWatched;
}
@Override
public boolean isEventSkipped(WatchListEvent event)
{
// We exclude watchlist jobs from notifications since they are modified each time they are fired,
// producing useless noise.
Collection<String> possibleIntervals = store.getIntervals();
return possibleIntervals.contains(event.getFullName());
}
@Override
public boolean isEventViewable(WatchListEvent event, String subscriber)
{
DocumentReference userReference = resolver.resolve(subscriber);
return authorizationManager.hasAccess(Right.VIEW, userReference, event.getDocumentReference());
}
private XWikiContext getXWikiContext()
{
return contextProvider.get();
}
}