001/*
002 * (C) Copyright 2011 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thomas Roger <troger@nuxeo.com>
016 */
017
018package org.nuxeo.ecm.activity;
019
020import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED;
021import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_REMOVED;
022import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_UPDATED;
023import static org.nuxeo.ecm.core.schema.FacetNames.HIDDEN_IN_NAVIGATION;
024import static org.nuxeo.ecm.core.schema.FacetNames.SUPER_SPACE;
025import static org.nuxeo.ecm.core.schema.FacetNames.SYSTEM_DOCUMENT;
026
027import java.security.Principal;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Iterator;
031import java.util.List;
032
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.DocumentNotFoundException;
036import org.nuxeo.ecm.core.api.DocumentRef;
037import org.nuxeo.ecm.core.api.SystemPrincipal;
038import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
039import org.nuxeo.ecm.core.event.Event;
040import org.nuxeo.ecm.core.event.EventBundle;
041import org.nuxeo.ecm.core.event.EventContext;
042import org.nuxeo.ecm.core.event.PostCommitEventListener;
043import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
044import org.nuxeo.ecm.core.event.impl.ShallowDocumentModel;
045import org.nuxeo.runtime.api.Framework;
046
047/**
048 * Listener called asynchronously to save events as activities through the {@link ActivityStreamService}.
049 *
050 * @author <a href="mailto:troger@nuxeo.com">Thomas Roger</a>
051 * @since 5.5
052 */
053public class ActivityStreamListener implements PostCommitEventListener {
054
055    @Override
056    public void handleEvent(EventBundle events) {
057        if (isEventBundleHandled(events)) {
058            List<Event> filteredEvents = filterDuplicateEvents(events);
059            for (Event event : filteredEvents) {
060                handleEvent(event);
061            }
062        }
063    }
064
065    protected List<Event> filterDuplicateEvents(EventBundle events) {
066        List<Event> filteredEvents = new ArrayList<Event>();
067
068        for (Event event : events) {
069            filteredEvents = removeEventIfExist(filteredEvents, event);
070            filteredEvents.add(event);
071        }
072
073        return filteredEvents;
074    }
075
076    protected List<Event> removeEventIfExist(List<Event> events, Event event) {
077        EventContext eventContext = event.getContext();
078        if (eventContext instanceof DocumentEventContext) {
079            DocumentModel doc = ((DocumentEventContext) eventContext).getSourceDocument();
080            for (Iterator<Event> it = events.iterator(); it.hasNext();) {
081                Event filteredEvent = it.next();
082                EventContext filteredEventContext = filteredEvent.getContext();
083                if (filteredEventContext instanceof DocumentEventContext) {
084                    DocumentModel filteredEventDoc = ((DocumentEventContext) filteredEventContext).getSourceDocument();
085                    if (event.getName().equals(filteredEvent.getName())
086                            && doc.getRef().equals(filteredEventDoc.getRef())) {
087                        it.remove();
088                        break;
089                    }
090                }
091            }
092        }
093        return events;
094    }
095
096    protected void handleEvent(Event event) {
097        EventContext eventContext = event.getContext();
098        if (eventContext instanceof DocumentEventContext) {
099            if (isEventHandled(event)) {
100                DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
101                DocumentModel doc = docEventContext.getSourceDocument();
102                if (isSkippedDocument(doc)) {
103                    return;
104                }
105
106                if (docEventContext.getPrincipal() instanceof SystemPrincipal) {
107                    // do not log activity for system principal
108                    return;
109                }
110
111                // add activity without context
112                ActivityStreamService activityStreamService = Framework.getLocalService(ActivityStreamService.class);
113                Activity activity = toActivity(docEventContext, event);
114                activityStreamService.addActivity(activity);
115
116                CoreSession session = docEventContext.getCoreSession();
117                for (DocumentRef ref : getParentSuperSpaceRefs(session, doc)) {
118                    String context = ActivityHelper.createDocumentActivityObject(session.getRepositoryName(),
119                            ref.toString());
120                    activity = toActivity(docEventContext, event, context);
121                    activityStreamService.addActivity(activity);
122                }
123            }
124        }
125    }
126
127    protected boolean isSkippedDocument(DocumentModel doc) {
128        // Not really interested in non live document or if document
129        // cannot be reconnected
130        // or if not visible
131        return doc instanceof ShallowDocumentModel || doc.hasFacet(HIDDEN_IN_NAVIGATION) //
132                || doc.hasFacet(SYSTEM_DOCUMENT) //
133                || doc.isProxy() //
134                || doc.isVersion();
135    }
136
137    protected boolean isEventHandled(Event event) {
138        for (String eventName : getHandledEventsName()) {
139            if (eventName.equals(event.getName())) {
140                return true;
141            }
142        }
143        return false;
144    }
145
146    protected boolean isEventBundleHandled(EventBundle events) {
147        for (String eventName : getHandledEventsName()) {
148            if (events.containsEventName(eventName)) {
149                return true;
150            }
151        }
152        return false;
153    }
154
155    protected List<String> getHandledEventsName() {
156        return Arrays.asList(DOCUMENT_CREATED, DOCUMENT_UPDATED, DOCUMENT_REMOVED);
157    }
158
159    protected Activity toActivity(DocumentEventContext docEventContext, Event event) {
160        return toActivity(docEventContext, event, null);
161    }
162
163    protected Activity toActivity(DocumentEventContext docEventContext, Event event, String context) {
164        Principal principal = docEventContext.getPrincipal();
165        DocumentModel doc = docEventContext.getSourceDocument();
166        return new ActivityBuilder().actor(ActivityHelper.createUserActivityObject(principal)).displayActor(
167                ActivityHelper.generateDisplayName(principal)).verb(event.getName()).object(
168                ActivityHelper.createDocumentActivityObject(doc)).displayObject(ActivityHelper.getDocumentTitle(doc)).target(
169                ActivityHelper.createDocumentActivityObject(doc.getRepositoryName(), doc.getParentRef().toString())).displayTarget(
170                getDocumentTitle(docEventContext.getCoreSession(), doc.getParentRef())).context(context).build();
171    }
172
173    protected String getDocumentTitle(CoreSession session, DocumentRef docRef) {
174        try {
175            DocumentModel doc = session.getDocument(docRef);
176            return ActivityHelper.getDocumentTitle(doc);
177        } catch (DocumentNotFoundException e) {
178            return docRef.toString();
179        }
180    }
181
182    protected List<DocumentRef> getParentSuperSpaceRefs(CoreSession session, final DocumentModel doc)
183            {
184        final List<DocumentRef> parents = new ArrayList<DocumentRef>();
185        new UnrestrictedSessionRunner(session) {
186            @Override
187            public void run() {
188                List<DocumentModel> parentDocuments = session.getParentDocuments(doc.getRef());
189                for (DocumentModel parent : parentDocuments) {
190                    if (parent.hasFacet(SUPER_SPACE)) {
191                        parents.add(parent.getRef());
192                    }
193                }
194            }
195        }.runUnrestricted();
196        return parents;
197    }
198
199}