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:[email protected]">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}