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}