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