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