/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * <a href="mailto:grenard@nuxeo.com">Guillaume</a> */ package org.nuxeo.ecm.collections.core; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; import java.util.TreeSet; import org.apache.commons.lang.StringUtils; import org.nuxeo.common.utils.i18n.I18NUtils; import org.nuxeo.ecm.collections.api.CollectionConstants; import org.nuxeo.ecm.collections.api.CollectionManager; import org.nuxeo.ecm.collections.core.adapter.Collection; import org.nuxeo.ecm.collections.core.adapter.CollectionMember; import org.nuxeo.ecm.collections.core.listener.CollectionAsynchrnonousQuery; import org.nuxeo.ecm.collections.core.worker.DuplicateCollectionMemberWork; import org.nuxeo.ecm.collections.core.worker.RemoveFromCollectionWork; import org.nuxeo.ecm.collections.core.worker.RemovedAbstractWork; import org.nuxeo.ecm.collections.core.worker.RemovedCollectionMemberWork; import org.nuxeo.ecm.collections.core.worker.RemovedCollectionWork; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.DocumentSecurityException; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.LifeCycleConstants; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; import org.nuxeo.ecm.core.api.event.CoreEventConstants; import org.nuxeo.ecm.core.api.event.DocumentEventCategories; import org.nuxeo.ecm.core.api.security.ACE; import org.nuxeo.ecm.core.api.security.ACL; import org.nuxeo.ecm.core.api.security.ACP; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.api.security.impl.ACLImpl; import org.nuxeo.ecm.core.api.security.impl.ACPImpl; import org.nuxeo.ecm.core.event.Event; import org.nuxeo.ecm.core.event.EventService; import org.nuxeo.ecm.core.event.impl.DocumentEventContext; import org.nuxeo.ecm.core.versioning.VersioningService; import org.nuxeo.ecm.core.work.api.WorkManager; import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService; import org.nuxeo.ecm.platform.dublincore.listener.DublinCoreListener; import org.nuxeo.ecm.platform.ec.notification.NotificationConstants; import org.nuxeo.ecm.platform.userworkspace.api.UserWorkspaceService; import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.DefaultComponent; import org.nuxeo.runtime.transaction.TransactionHelper; /** * @since 5.9.3 */ public class CollectionManagerImpl extends DefaultComponent implements CollectionManager { private static final String PERMISSION_ERROR_MESSAGE = "Privilege '%s' is not granted to '%s'"; public static void disableEvents(final DocumentModel doc) { doc.putContextData(DublinCoreListener.DISABLE_DUBLINCORE_LISTENER, true); doc.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true); doc.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true); doc.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, true); } @Override public void addToCollection(final DocumentModel collection, final DocumentModel documentToBeAdded, final CoreSession session) throws DocumentSecurityException { checkCanAddToCollection(collection, documentToBeAdded, session); final Map<String, Serializable> props = new HashMap<>(); props.put(CollectionConstants.COLLECTION_REF_EVENT_CTX_PROP, collection.getRef()); fireEvent(documentToBeAdded, session, CollectionConstants.BEFORE_ADDED_TO_COLLECTION, props); Collection colAdapter = collection.getAdapter(Collection.class); colAdapter.addDocument(documentToBeAdded.getId()); collection.getCoreSession().saveDocument(colAdapter.getDocument()); new UnrestrictedSessionRunner(session) { @Override public void run() { documentToBeAdded.addFacet(CollectionConstants.COLLECTABLE_FACET); // We want to disable the following listener on a // collection member when it is added to a collection disableEvents(documentToBeAdded); CollectionMember docAdapter = documentToBeAdded.getAdapter(CollectionMember.class); docAdapter.addToCollection(collection.getId()); DocumentModel addedDoc = session.saveDocument(docAdapter.getDocument()); fireEvent(addedDoc, session, CollectionConstants.ADDED_TO_COLLECTION, props); } }.runUnrestricted(); } @Override public void addToCollection(final DocumentModel collection, final List<DocumentModel> documentListToBeAdded, final CoreSession session) { for (DocumentModel documentToBeAdded : documentListToBeAdded) { addToCollection(collection, documentToBeAdded, session); } } @Override public void addToNewCollection(final String newTitle, final String newDescription, final DocumentModel documentToBeAdded, final CoreSession session) { addToCollection(createCollection(newTitle, newDescription, documentToBeAdded, session), documentToBeAdded, session); } @Override public void addToNewCollection(final String newTitle, final String newDescription, final List<DocumentModel> documentListToBeAdded, CoreSession session) { DocumentModel newCollection = createCollection(newTitle, newDescription, documentListToBeAdded.get(0), session); for (DocumentModel documentToBeAdded : documentListToBeAdded) { addToCollection(newCollection, documentToBeAdded, session); } } @Override public boolean canAddToCollection(final DocumentModel collection, final CoreSession session) { return isCollection(collection) && session.hasPermission(collection.getRef(), SecurityConstants.WRITE_PROPERTIES); } @Override public boolean canManage(final DocumentModel collection, final CoreSession session) { return isCollection(collection) && session.hasPermission(collection.getRef(), SecurityConstants.EVERYTHING); } public void checkCanAddToCollection(final DocumentModel collection, final DocumentModel documentToBeAdded, final CoreSession session) { if (!isCollectable(documentToBeAdded)) { throw new IllegalArgumentException( String.format("Document %s is not collectable", documentToBeAdded.getTitle())); } checkCanCollectInCollection(collection, session); } /** * @since 8.4 */ protected void checkCanCollectInCollection(final DocumentModel collection, final CoreSession session) { if (!isCollection(collection)) { throw new IllegalArgumentException(String.format("Document %s is not a collection", collection.getTitle())); } if (!session.hasPermission(collection.getRef(), SecurityConstants.WRITE_PROPERTIES)) { throw new DocumentSecurityException(String.format(PERMISSION_ERROR_MESSAGE, CollectionConstants.CAN_COLLECT_PERMISSION, session.getPrincipal().getName())); } } protected DocumentModel createCollection(final String newTitle, final String newDescription, final DocumentModel context, final CoreSession session) { DocumentModel defaultCollections = getUserDefaultCollections(context, session); DocumentModel newCollection = session.createDocumentModel(defaultCollections.getPath().toString(), newTitle, CollectionConstants.COLLECTION_TYPE); newCollection.setPropertyValue("dc:title", newTitle); newCollection.setPropertyValue("dc:description", newDescription); return session.createDocument(newCollection); } protected DocumentModel createDefaultCollections(final CoreSession session, DocumentModel userWorkspace) { DocumentModel doc = session.createDocumentModel(userWorkspace.getPath().toString(), CollectionConstants.DEFAULT_COLLECTIONS_NAME, CollectionConstants.COLLECTIONS_TYPE); String title = null; try { title = I18NUtils.getMessageString("messages", CollectionConstants.DEFAULT_COLLECTIONS_TITLE, new Object[0], getLocale(session)); } catch (MissingResourceException e) { title = CollectionConstants.DEFAULT_COLLECTIONS_TITLE; } doc.setPropertyValue("dc:title", title); doc.setPropertyValue("dc:description", ""); doc = session.createDocument(doc); ACP acp = new ACPImpl(); ACE denyEverything = new ACE(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false); ACE allowEverything = new ACE(session.getPrincipal().getName(), SecurityConstants.EVERYTHING, true); ACL acl = new ACLImpl(); acl.setACEs(new ACE[] { allowEverything, denyEverything }); acp.addACL(acl); doc.setACP(acp, true); return doc; } @Override public DocumentModel getUserDefaultCollections(final DocumentModel context, final CoreSession session) { final UserWorkspaceService userWorkspaceService = Framework.getLocalService(UserWorkspaceService.class); final DocumentModel userWorkspace = userWorkspaceService.getCurrentUserPersonalWorkspace(session, context); final DocumentRef lookupRef = new PathRef(userWorkspace.getPath().toString(), CollectionConstants.DEFAULT_COLLECTIONS_NAME); if (session.exists(lookupRef)) { return session.getChild(userWorkspace.getRef(), CollectionConstants.DEFAULT_COLLECTIONS_NAME); } else { // does not exist yet, let's create it synchronized (this) { TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); if (!session.exists(lookupRef)) { boolean succeed = false; try { createDefaultCollections(session, userWorkspace); succeed = true; } finally { if (succeed) { TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); } } } return session.getDocument(lookupRef); } } } @Override public List<DocumentModel> getVisibleCollection(final DocumentModel collectionMember, final CoreSession session) { return getVisibleCollection(collectionMember, CollectionConstants.MAX_COLLECTION_RETURNED, session); } @Override public List<DocumentModel> getVisibleCollection(final DocumentModel collectionMember, int maxResult, CoreSession session) { List<DocumentModel> result = new ArrayList<DocumentModel>(); if (isCollected(collectionMember)) { CollectionMember collectionMemberAdapter = collectionMember.getAdapter(CollectionMember.class); List<String> collectionIds = collectionMemberAdapter.getCollectionIds(); for (int i = 0; i < collectionIds.size() && result.size() < maxResult; i++) { final String collectionId = collectionIds.get(i); DocumentRef documentRef = new IdRef(collectionId); if (session.exists(documentRef) && session.hasPermission(documentRef, SecurityConstants.READ) && !LifeCycleConstants.DELETED_STATE.equals(session.getCurrentLifeCycleState(documentRef))) { DocumentModel collection = session.getDocument(documentRef); if (!collection.isVersion()) { result.add(collection); } } } } return result; } @Override public boolean hasVisibleCollection(final DocumentModel collectionMember, CoreSession session) { CollectionMember collectionMemberAdapter = collectionMember.getAdapter(CollectionMember.class); List<String> collectionIds = collectionMemberAdapter.getCollectionIds(); for (final String collectionId : collectionIds) { DocumentRef documentRef = new IdRef(collectionId); if (session.exists(documentRef) && session.hasPermission(documentRef, SecurityConstants.READ)) { return true; } } return false; } @Override public boolean isCollectable(final DocumentModel doc) { return !doc.hasFacet(CollectionConstants.NOT_COLLECTABLE_FACET); } @Override public boolean isCollected(final DocumentModel doc) { return doc.hasFacet(CollectionConstants.COLLECTABLE_FACET); } @Override public boolean isCollection(final DocumentModel doc) { return doc.hasFacet(CollectionConstants.COLLECTION_FACET); } @Override public boolean isInCollection(DocumentModel collection, DocumentModel document, CoreSession session) { if (isCollected(document)) { final CollectionMember collectionMemberAdapter = document.getAdapter(CollectionMember.class); return collectionMemberAdapter.getCollectionIds().contains(collection.getId()); } return false; } @Override public void processCopiedCollection(final DocumentModel collection) { Collection collectionAdapter = collection.getAdapter(Collection.class); List<String> documentIds = collectionAdapter.getCollectedDocumentIds(); int i = 0; while (i < documentIds.size()) { int limit = (int) (((i + CollectionAsynchrnonousQuery.MAX_RESULT) > documentIds.size()) ? documentIds.size() : (i + CollectionAsynchrnonousQuery.MAX_RESULT)); DuplicateCollectionMemberWork work = new DuplicateCollectionMemberWork(collection.getRepositoryName(), collection.getId(), documentIds.subList(i, limit), i); WorkManager workManager = Framework.getLocalService(WorkManager.class); workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true); i = limit; } } @Override public void processRemovedCollection(final DocumentModel collection) { final WorkManager workManager = Framework.getLocalService(WorkManager.class); final RemovedAbstractWork work = new RemovedCollectionWork(); work.setDocument(collection.getRepositoryName(), collection.getId()); workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true); } @Override public void processRemovedCollectionMember(final DocumentModel collectionMember) { final WorkManager workManager = Framework.getLocalService(WorkManager.class); final RemovedAbstractWork work = new RemovedCollectionMemberWork(); work.setDocument(collectionMember.getRepositoryName(), collectionMember.getId()); workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true); } @Override public void processRestoredCollection(DocumentModel collection, DocumentModel version) { final Set<String> collectionMemberIdsToBeRemoved = new TreeSet<String>( collection.getAdapter(Collection.class).getCollectedDocumentIds()); collectionMemberIdsToBeRemoved.removeAll(version.getAdapter(Collection.class).getCollectedDocumentIds()); final Set<String> collectionMemberIdsToBeAdded = new TreeSet<String>( version.getAdapter(Collection.class).getCollectedDocumentIds()); collectionMemberIdsToBeAdded.removeAll(collection.getAdapter(Collection.class).getCollectedDocumentIds()); int i = 0; while (i < collectionMemberIdsToBeRemoved.size()) { int limit = (int) (((i + CollectionAsynchrnonousQuery.MAX_RESULT) > collectionMemberIdsToBeRemoved.size()) ? collectionMemberIdsToBeRemoved.size() : (i + CollectionAsynchrnonousQuery.MAX_RESULT)); RemoveFromCollectionWork work = new RemoveFromCollectionWork(collection.getRepositoryName(), collection.getId(), new ArrayList<String>(collectionMemberIdsToBeRemoved).subList(i, limit), i); WorkManager workManager = Framework.getLocalService(WorkManager.class); workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true); i = limit; } i = 0; while (i < collectionMemberIdsToBeAdded.size()) { int limit = (int) (((i + CollectionAsynchrnonousQuery.MAX_RESULT) > collectionMemberIdsToBeAdded.size()) ? collectionMemberIdsToBeAdded.size() : (i + CollectionAsynchrnonousQuery.MAX_RESULT)); DuplicateCollectionMemberWork work = new DuplicateCollectionMemberWork(collection.getRepositoryName(), collection.getId(), new ArrayList<String>(collectionMemberIdsToBeAdded).subList(i, limit), i); WorkManager workManager = Framework.getLocalService(WorkManager.class); workManager.schedule(work, WorkManager.Scheduling.IF_NOT_SCHEDULED, true); i = limit; } } @Override public void removeAllFromCollection(final DocumentModel collection, final List<DocumentModel> documentListToBeRemoved, final CoreSession session) { for (DocumentModel documentToBeRemoved : documentListToBeRemoved) { removeFromCollection(collection, documentToBeRemoved, session); } } @Override public void removeFromCollection(final DocumentModel collection, final DocumentModel documentToBeRemoved, final CoreSession session) { checkCanAddToCollection(collection, documentToBeRemoved, session); Map<String, Serializable> props = new HashMap<>(); props.put(CollectionConstants.COLLECTION_REF_EVENT_CTX_PROP, new IdRef(collection.getId())); fireEvent(documentToBeRemoved, session, CollectionConstants.BEFORE_REMOVED_FROM_COLLECTION, props); Collection colAdapter = collection.getAdapter(Collection.class); colAdapter.removeDocument(documentToBeRemoved.getId()); collection.getCoreSession().saveDocument(colAdapter.getDocument()); new UnrestrictedSessionRunner(session) { @Override public void run() { doRemoveFromCollection(documentToBeRemoved, collection.getId(), session); } }.runUnrestricted(); } @Override public void doRemoveFromCollection(DocumentModel documentToBeRemoved, String collectionId, CoreSession session) { // We want to disable the following listener on a // collection member when it is removed from a collection disableEvents(documentToBeRemoved); CollectionMember docAdapter = documentToBeRemoved.getAdapter(CollectionMember.class); docAdapter.removeFromCollection(collectionId); DocumentModel removedDoc = session.saveDocument(docAdapter.getDocument()); Map<String, Serializable> props = new HashMap<>(); props.put(CollectionConstants.COLLECTION_REF_EVENT_CTX_PROP, new IdRef(collectionId)); fireEvent(removedDoc, session, CollectionConstants.REMOVED_FROM_COLLECTION, props); } @Override public DocumentModel createCollection(final CoreSession session, String title, String description, String path) { DocumentModel newCollection = null; // Test if the path is null or empty if (StringUtils.isEmpty(path)) { // A default collection is created with the given name newCollection = createCollection(title, description, null, session); } else { // If the path does not exist, an exception is thrown if (!session.exists(new PathRef(path))) { throw new NuxeoException(String.format("Path \"%s\" specified in parameter not found", path)); } // Create a new collection in the given path DocumentModel collectionModel = session.createDocumentModel(path, title, CollectionConstants.COLLECTION_TYPE); collectionModel.setPropertyValue("dc:title", title); collectionModel.setPropertyValue("dc:description", description); newCollection = session.createDocument(collectionModel); } return newCollection; } protected Locale getLocale(final CoreSession session) { Locale locale = null; locale = Framework.getLocalService(LocaleProvider.class).getLocale(session); if (locale == null) { locale = Locale.getDefault(); } return new Locale(Locale.getDefault().getLanguage()); } protected void fireEvent(DocumentModel doc, CoreSession session, String eventName, Map<String, Serializable> props) { EventService eventService = Framework.getService(EventService.class); DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), doc); ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, session.getRepositoryName()); ctx.setProperty(CoreEventConstants.SESSION_ID, session.getSessionId()); ctx.setProperty("category", DocumentEventCategories.EVENT_DOCUMENT_CATEGORY); ctx.setProperties(props); Event event = ctx.newEvent(eventName); eventService.fireEvent(event); } @Override public boolean moveMembers(final CoreSession session, final DocumentModel collection, final DocumentModel member1, final DocumentModel member2) { checkCanCollectInCollection(collection, session); ; Collection collectionAdapter = collection.getAdapter(Collection.class); boolean result = collectionAdapter.moveMembers(member1.getId(), member2 != null ? member2.getId() : null); if (result) { session.saveDocument(collectionAdapter.getDocument()); } return result; } }