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