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