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