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