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