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