001/*
002 * (C) Copyright 2014 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-2.1.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 *     <a href="mailto:grenard@nuxeo.com">Guillaume</a>
016 */
017package org.nuxeo.ecm.collections.core;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.MissingResourceException;
026import java.util.Set;
027import java.util.TreeSet;
028
029import org.apache.commons.lang.StringUtils;
030import org.nuxeo.common.utils.i18n.I18NUtils;
031import org.nuxeo.ecm.collections.api.CollectionConstants;
032import org.nuxeo.ecm.collections.api.CollectionManager;
033import org.nuxeo.ecm.collections.core.adapter.Collection;
034import org.nuxeo.ecm.collections.core.adapter.CollectionMember;
035import org.nuxeo.ecm.collections.core.listener.CollectionAsynchrnonousQuery;
036import org.nuxeo.ecm.collections.core.worker.DuplicateCollectionMemberWork;
037import org.nuxeo.ecm.collections.core.worker.RemoveFromCollectionWork;
038import org.nuxeo.ecm.collections.core.worker.RemovedAbstractWork;
039import org.nuxeo.ecm.collections.core.worker.RemovedCollectionMemberWork;
040import org.nuxeo.ecm.collections.core.worker.RemovedCollectionWork;
041import org.nuxeo.ecm.core.api.CoreSession;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.DocumentRef;
044import org.nuxeo.ecm.core.api.DocumentSecurityException;
045import org.nuxeo.ecm.core.api.IdRef;
046import org.nuxeo.ecm.core.api.LifeCycleConstants;
047import org.nuxeo.ecm.core.api.NuxeoException;
048import org.nuxeo.ecm.core.api.PathRef;
049import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
050import org.nuxeo.ecm.core.api.event.CoreEventConstants;
051import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
052import org.nuxeo.ecm.core.api.security.ACE;
053import org.nuxeo.ecm.core.api.security.ACL;
054import org.nuxeo.ecm.core.api.security.ACP;
055import org.nuxeo.ecm.core.api.security.SecurityConstants;
056import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
057import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
058import org.nuxeo.ecm.core.event.Event;
059import org.nuxeo.ecm.core.event.EventService;
060import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
061import org.nuxeo.ecm.core.versioning.VersioningService;
062import org.nuxeo.ecm.core.work.api.WorkManager;
063import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService;
064import org.nuxeo.ecm.platform.dublincore.listener.DublinCoreListener;
065import org.nuxeo.ecm.platform.ec.notification.NotificationConstants;
066import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService;
067import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider;
068import org.nuxeo.runtime.api.Framework;
069import org.nuxeo.runtime.model.DefaultComponent;
070import org.nuxeo.runtime.transaction.TransactionHelper;
071
072/**
073 * @since 5.9.3
074 */
075public class CollectionManagerImpl extends DefaultComponent implements CollectionManager {
076
077    private static final String PERMISSION_ERROR_MESSAGE = "Privilege '%s' is not granted to '%s'";
078
079    public static void disableEvents(final DocumentModel doc) {
080        doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true);
081        doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true);
082        doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true);
083        doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true);
084    }
085
086    @Override
087    public void addToCollection(final DocumentModel collection, final DocumentModel documentToBeAdded,
088            final CoreSession session) throws DocumentSecurityException {
089        checkCanAddToCollection(collection, documentToBeAdded, session);
090        final Map<String, Serializable> props = new HashMap<>();
091        props.put(CollectionConstants.COLLECTION_REF_EVENT_CTX_PROP, collection.getRef());
092        fireEvent(documentToBeAdded, session, CollectionConstants.BEFORE_ADDED_TO_COLLECTION, props);
093        Collection colAdapter = collection.getAdapter(Collection.class);
094        colAdapter.addDocument(documentToBeAdded.getId());
095        collection.getCoreSession().saveDocument(colAdapter.getDocument());
096
097        new UnrestrictedSessionRunner(session) {
098
099            @Override
100            public void run() {
101
102                DocumentModel temp = documentToBeAdded;
103
104                temp.addFacet(CollectionConstants.COLLECTABLE_FACET);
105
106                disableEvents(temp);
107
108                temp = session.saveDocument(temp);
109
110                // We want to disable the following listener on a
111                // collection member when it is added to a collection
112                disableEvents(temp);
113
114                CollectionMember docAdapter = temp.getAdapter(CollectionMember.class);
115                docAdapter.addToCollection(collection.getId());
116                DocumentModel addedDoc = session.saveDocument(docAdapter.getDocument());
117                fireEvent(addedDoc, session, CollectionConstants.ADDED_TO_COLLECTION, props);
118            }
119
120        }.runUnrestricted();
121    }
122
123    @Override
124    public void addToCollection(final DocumentModel collection, final List<DocumentModel> documentListToBeAdded,
125            final CoreSession session) {
126        for (DocumentModel documentToBeAdded : documentListToBeAdded) {
127            addToCollection(collection, documentToBeAdded, session);
128        }
129    }
130
131    @Override
132    public void addToNewCollection(final String newTitle, final String newDescription,
133            final DocumentModel documentToBeAdded, final CoreSession session) {
134        addToCollection(createCollection(newTitle, newDescription, documentToBeAdded, session), documentToBeAdded,
135                session);
136    }
137
138    @Override
139    public void addToNewCollection(final String newTitle, final String newDescription,
140            final List<DocumentModel> documentListToBeAdded, CoreSession session) {
141        DocumentModel newCollection = createCollection(newTitle, newDescription, documentListToBeAdded.get(0), session);
142        for (DocumentModel documentToBeAdded : documentListToBeAdded) {
143            addToCollection(newCollection, documentToBeAdded, session);
144        }
145    }
146
147    @Override
148    public boolean canAddToCollection(final DocumentModel collection, final CoreSession session) {
149        return isCollection(collection)
150                && session.hasPermission(collection.getRef(), SecurityConstants.WRITE_PROPERTIES);
151    }
152
153    @Override
154    public boolean canManage(final DocumentModel collection, final CoreSession session) {
155        return isCollection(collection) && session.hasPermission(collection.getRef(), SecurityConstants.EVERYTHING);
156    }
157
158    public void checkCanAddToCollection(final DocumentModel collection, final DocumentModel documentToBeAdded,
159            final CoreSession session) {
160        if (!isCollectable(documentToBeAdded)) {
161            throw new IllegalArgumentException(String.format("Document %s is not collectable",
162                    documentToBeAdded.getTitle()));
163        }
164        if (!isCollection(collection)) {
165            throw new IllegalArgumentException(String.format("Document %s is not a collection",
166                    documentToBeAdded.getTitle()));
167        }
168        if (!session.hasPermission(collection.getRef(), SecurityConstants.WRITE_PROPERTIES)) {
169            throw new DocumentSecurityException(String.format(PERMISSION_ERROR_MESSAGE,
170                    CollectionConstants.CAN_COLLECT_PERMISSION, session.getPrincipal().getName()));
171        }
172    }
173
174    protected DocumentModel createCollection(final String newTitle, final String newDescription,
175            final DocumentModel context, final CoreSession session) {
176        DocumentModel defaultCollections = getUserDefaultCollections(context, session);
177        DocumentModel newCollection = session.createDocumentModel(defaultCollections.getPath().toString(), newTitle,
178                CollectionConstants.COLLECTION_TYPE);
179        newCollection.setPropertyValue("dc:title", newTitle);
180        newCollection.setPropertyValue("dc:description", newDescription);
181        return session.createDocument(newCollection);
182    }
183
184    protected DocumentModel createDefaultCollections(final CoreSession session, DocumentModel userWorkspace)
185            {
186        DocumentModel doc = session.createDocumentModel(userWorkspace.getPath().toString(),
187                CollectionConstants.DEFAULT_COLLECTIONS_NAME, CollectionConstants.COLLECTIONS_TYPE);
188        String title = null;
189        try {
190            title = I18NUtils.getMessageString("messages", CollectionConstants.DEFAULT_COLLECTIONS_TITLE,
191                    new Object[0], getLocale(session));
192        } catch (MissingResourceException e) {
193            title = CollectionConstants.DEFAULT_COLLECTIONS_TITLE;
194        }
195        doc.setPropertyValue("dc:title", title);
196        doc.setPropertyValue("dc:description", "");
197        doc = session.createDocument(doc);
198
199        ACP acp = new ACPImpl();
200        ACE denyEverything = new ACE(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false);
201        ACE allowEverything = new ACE(session.getPrincipal().getName(), SecurityConstants.EVERYTHING, true);
202        ACL acl = new ACLImpl();
203        acl.setACEs(new ACE[] { allowEverything, denyEverything });
204        acp.addACL(acl);
205        doc.setACP(acp, true);
206
207        return doc;
208    }
209
210    @Override
211    public DocumentModel getUserDefaultCollections(final DocumentModel context, final CoreSession session)
212            {
213        final UserWorkspaceService userWorkspaceService = Framework.getLocalService(UserWorkspaceService.class);
214        final DocumentModel userWorkspace = userWorkspaceService.getCurrentUserPersonalWorkspace(session, context);
215        final DocumentRef lookupRef = new PathRef(userWorkspace.getPath().toString(),
216                CollectionConstants.DEFAULT_COLLECTIONS_NAME);
217        if (session.exists(lookupRef)) {
218            return session.getChild(userWorkspace.getRef(), CollectionConstants.DEFAULT_COLLECTIONS_NAME);
219        } else {
220            // does not exist yet, let's create it
221            synchronized (this) {
222                TransactionHelper.commitOrRollbackTransaction();
223                TransactionHelper.startTransaction();
224                if (!session.exists(lookupRef)) {
225                    boolean succeed = false;
226                    try {
227                        createDefaultCollections(session, userWorkspace);
228                        succeed = true;
229                    } finally {
230                        if (succeed) {
231                            TransactionHelper.commitOrRollbackTransaction();
232                            TransactionHelper.startTransaction();
233                        }
234                    }
235                }
236                return session.getDocument(lookupRef);
237            }
238        }
239    }
240
241    @Override
242    public List<DocumentModel> getVisibleCollection(final DocumentModel collectionMember, final CoreSession session)
243            {
244        return getVisibleCollection(collectionMember, CollectionConstants.MAX_COLLECTION_RETURNED, session);
245    }
246
247    @Override
248    public List<DocumentModel> getVisibleCollection(final DocumentModel collectionMember, int maxResult,
249            CoreSession session) {
250        List<DocumentModel> result = new ArrayList<DocumentModel>();
251        CollectionMember collectionMemberAdapter = collectionMember.getAdapter(CollectionMember.class);
252        List<String> collectionIds = collectionMemberAdapter.getCollectionIds();
253        for (int i = 0; i < collectionIds.size() && result.size() < maxResult; i++) {
254            final String collectionId = collectionIds.get(i);
255            DocumentRef documentRef = new IdRef(collectionId);
256            if (session.exists(documentRef) && session.hasPermission(documentRef, SecurityConstants.READ)
257                    && !LifeCycleConstants.DELETED_STATE.equals(session.getCurrentLifeCycleState(documentRef))) {
258                DocumentModel collection = session.getDocument(documentRef);
259                if (!collection.isVersion()) {
260                    result.add(collection);
261                }
262            }
263        }
264        return result;
265    }
266
267    @Override
268    public boolean hasVisibleCollection(final DocumentModel collectionMember, CoreSession session)
269            {
270        CollectionMember collectionMemberAdapter = collectionMember.getAdapter(CollectionMember.class);
271        List<String> collectionIds = collectionMemberAdapter.getCollectionIds();
272        for (final String collectionId : collectionIds) {
273            DocumentRef documentRef = new IdRef(collectionId);
274            if (session.exists(documentRef) && session.hasPermission(documentRef, SecurityConstants.READ)) {
275                return true;
276            }
277        }
278        return false;
279    }
280
281    @Override
282    public boolean isCollectable(final DocumentModel doc) {
283        return !doc.hasFacet(CollectionConstants.NOT_COLLECTABLE_FACET) && !doc.isVersion() && !doc.isProxy();
284    }
285
286    @Override
287    public boolean isCollected(final DocumentModel doc) {
288        return doc.hasFacet(CollectionConstants.COLLECTABLE_FACET);
289    }
290
291    @Override
292    public boolean isCollection(final DocumentModel doc) {
293        return doc.hasFacet(CollectionConstants.COLLECTION_FACET);
294    }
295
296    @Override
297    public boolean isInCollection(DocumentModel collection, DocumentModel document, CoreSession session)
298            {
299        if (isCollected(document)) {
300            final CollectionMember collectionMemberAdapter = document.getAdapter(CollectionMember.class);
301            return collectionMemberAdapter.getCollectionIds().contains(collection.getId());
302        }
303        return false;
304    }
305
306    @Override
307    public void processCopiedCollection(final DocumentModel collection) {
308        Collection collectionAdapter = collection.getAdapter(Collection.class);
309        List<String> documentIds = collectionAdapter.getCollectedDocumentIds();
310
311        int i = 0;
312        while (i < documentIds.size()) {
313            int limit = (int) (((i + CollectionAsynchrnonousQuery.MAX_RESULT) > documentIds.size()) ? documentIds.size()
314                    : (i + CollectionAsynchrnonousQuery.MAX_RESULT));
315            DuplicateCollectionMemberWork work = new DuplicateCollectionMemberWork(collection.getRepositoryName(),
316                    collection.getId(), documentIds.subList(i, limit), i);
317            WorkManager workManager = Framework.getLocalService(WorkManager.class);
318            workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true);
319
320            i = limit;
321        }
322    }
323
324    @Override
325    public void processRemovedCollection(final DocumentModel collection) {
326        final WorkManager workManager = Framework.getLocalService(WorkManager.class);
327        final RemovedAbstractWork work = new RemovedCollectionWork();
328        work.setDocument(collection.getRepositoryName(), collection.getId());
329        workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true);
330    }
331
332    @Override
333    public void processRemovedCollectionMember(final DocumentModel collectionMember) {
334        final WorkManager workManager = Framework.getLocalService(WorkManager.class);
335        final RemovedAbstractWork work = new RemovedCollectionMemberWork();
336        work.setDocument(collectionMember.getRepositoryName(), collectionMember.getId());
337        workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true);
338    }
339
340    @Override
341    public void processRestoredCollection(DocumentModel collection, DocumentModel version) {
342        final Set<String> collectionMemberIdsToBeRemoved = new TreeSet<String>(
343                collection.getAdapter(Collection.class).getCollectedDocumentIds());
344        collectionMemberIdsToBeRemoved.removeAll(version.getAdapter(Collection.class).getCollectedDocumentIds());
345
346        final Set<String> collectionMemberIdsToBeAdded = new TreeSet<String>(
347                version.getAdapter(Collection.class).getCollectedDocumentIds());
348        collectionMemberIdsToBeAdded.removeAll(collection.getAdapter(Collection.class).getCollectedDocumentIds());
349
350        int i = 0;
351        while (i < collectionMemberIdsToBeRemoved.size()) {
352            int limit = (int) (((i + CollectionAsynchrnonousQuery.MAX_RESULT) > collectionMemberIdsToBeRemoved.size())
353                    ? collectionMemberIdsToBeRemoved.size() : (i + CollectionAsynchrnonousQuery.MAX_RESULT));
354            RemoveFromCollectionWork work = new RemoveFromCollectionWork(collection.getRepositoryName(),
355                    collection.getId(), new ArrayList<String>(collectionMemberIdsToBeRemoved).subList(i, limit), i);
356            WorkManager workManager = Framework.getLocalService(WorkManager.class);
357            workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true);
358
359            i = limit;
360        }
361        i = 0;
362        while (i < collectionMemberIdsToBeAdded.size()) {
363            int limit = (int) (((i + CollectionAsynchrnonousQuery.MAX_RESULT) > collectionMemberIdsToBeAdded.size())
364                    ? collectionMemberIdsToBeAdded.size() : (i + CollectionAsynchrnonousQuery.MAX_RESULT));
365            DuplicateCollectionMemberWork work = new DuplicateCollectionMemberWork(collection.getRepositoryName(),
366                    collection.getId(), new ArrayList<String>(collectionMemberIdsToBeAdded).subList(i, limit), i);
367            WorkManager workManager = Framework.getLocalService(WorkManager.class);
368            workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true);
369
370            i = limit;
371        }
372    }
373
374    @Override
375    public void removeAllFromCollection(final DocumentModel collection,
376            final List<DocumentModel> documentListToBeRemoved, final CoreSession session) {
377        for (DocumentModel documentToBeRemoved : documentListToBeRemoved) {
378            removeFromCollection(collection, documentToBeRemoved, session);
379        }
380    }
381
382    @Override
383    public void removeFromCollection(final DocumentModel collection, final DocumentModel documentToBeRemoved,
384            final CoreSession session) {
385        checkCanAddToCollection(collection, documentToBeRemoved, session);
386        Map<String, Serializable> props = new HashMap<>();
387        props.put(CollectionConstants.COLLECTION_REF_EVENT_CTX_PROP, new IdRef(collection.getId()));
388        fireEvent(documentToBeRemoved, session, CollectionConstants.BEFORE_REMOVED_FROM_COLLECTION, props);
389        Collection colAdapter = collection.getAdapter(Collection.class);
390        colAdapter.removeDocument(documentToBeRemoved.getId());
391        collection.getCoreSession().saveDocument(colAdapter.getDocument());
392
393        new UnrestrictedSessionRunner(session) {
394
395            @Override
396            public void run() {
397                doRemoveFromCollection(documentToBeRemoved, collection.getId(), session);
398            }
399
400        }.runUnrestricted();
401    }
402
403    @Override
404    public void doRemoveFromCollection(DocumentModel documentToBeRemoved, String collectionId, CoreSession session) {
405        // We want to disable the following listener on a
406        // collection member when it is removed from a collection
407        disableEvents(documentToBeRemoved);
408
409        CollectionMember docAdapter = documentToBeRemoved.getAdapter(CollectionMember.class);
410        docAdapter.removeFromCollection(collectionId);
411        DocumentModel removedDoc = session.saveDocument(docAdapter.getDocument());
412        Map<String, Serializable> props = new HashMap<>();
413        props.put(CollectionConstants.COLLECTION_REF_EVENT_CTX_PROP, new IdRef(collectionId));
414        fireEvent(removedDoc, session, CollectionConstants.REMOVED_FROM_COLLECTION, props);
415    }
416
417    @Override
418    public DocumentModel createCollection(final CoreSession session, String title, String description, String path)
419            {
420        DocumentModel newCollection = null;
421        // Test if the path is null or empty
422        if (StringUtils.isEmpty(path)) {
423            // A default collection is created with the given name
424            newCollection = createCollection(title, description, null, session);
425        } else {
426            // If the path does not exist, an exception is thrown
427            if (!session.exists(new PathRef(path))) {
428                throw new NuxeoException(String.format("Path \"%s\" specified in parameter not found", path));
429            }
430            // Create a new collection in the given path
431            DocumentModel collectionModel = session.createDocumentModel(path, title,
432                    CollectionConstants.COLLECTION_TYPE);
433            collectionModel.setPropertyValue("dc:title", title);
434            collectionModel.setPropertyValue("dc:description", description);
435            newCollection = session.createDocument(collectionModel);
436        }
437        return newCollection;
438    }
439
440    protected Locale getLocale(final CoreSession session) {
441        Locale locale = null;
442        locale = Framework.getLocalService(LocaleProvider.class).getLocale(session);
443        if (locale == null) {
444            locale = Locale.getDefault();
445        }
446        return new Locale(Locale.getDefault().getLanguage());
447    }
448
449    protected void fireEvent(DocumentModel doc, CoreSession session, String eventName, Map<String, Serializable> props)
450            {
451        EventService eventService = Framework.getService(EventService.class);
452        DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), doc);
453        ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, session.getRepositoryName());
454        ctx.setProperty(CoreEventConstants.SESSION_ID, session.getSessionId());
455        ctx.setProperty("category", DocumentEventCategories.EVENT_DOCUMENT_CATEGORY);
456        ctx.setProperties(props);
457        Event event = ctx.newEvent(eventName);
458        eventService.fireEvent(event);
459    }
460
461}