001/* 002 * (C) Copyright 2012 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.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 * Olivier Grisel <ogrisel@nuxeo.com> 016 * Antoine Taillefer <ataillefer@nuxeo.com> 017 */ 018package org.nuxeo.drive.service.impl; 019 020import static org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY; 021 022import java.io.Serializable; 023import java.security.Principal; 024import java.util.ArrayList; 025import java.util.Calendar; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.LinkedHashSet; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033import java.util.TimeZone; 034import java.util.TreeSet; 035 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.nuxeo.common.utils.Path; 039import org.nuxeo.drive.service.FileSystemChangeFinder; 040import org.nuxeo.drive.service.FileSystemChangeSummary; 041import org.nuxeo.drive.service.FileSystemItemChange; 042import org.nuxeo.drive.service.NuxeoDriveEvents; 043import org.nuxeo.drive.service.NuxeoDriveManager; 044import org.nuxeo.drive.service.SynchronizationRoots; 045import org.nuxeo.drive.service.TooManyChangesException; 046import org.nuxeo.ecm.collections.api.CollectionConstants; 047import org.nuxeo.ecm.collections.api.CollectionManager; 048import org.nuxeo.ecm.core.api.CoreInstance; 049import org.nuxeo.ecm.core.api.CoreSession; 050import org.nuxeo.ecm.core.api.DocumentModel; 051import org.nuxeo.ecm.core.api.DocumentNotFoundException; 052import org.nuxeo.ecm.core.api.DocumentRef; 053import org.nuxeo.ecm.core.api.DocumentSecurityException; 054import org.nuxeo.ecm.core.api.IdRef; 055import org.nuxeo.ecm.core.api.IterableQueryResult; 056import org.nuxeo.ecm.core.api.NuxeoException; 057import org.nuxeo.ecm.core.api.PathRef; 058import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 059import org.nuxeo.ecm.core.api.event.CoreEventConstants; 060import org.nuxeo.ecm.core.api.repository.RepositoryManager; 061import org.nuxeo.ecm.core.api.security.SecurityConstants; 062import org.nuxeo.ecm.core.cache.Cache; 063import org.nuxeo.ecm.core.cache.CacheService; 064import org.nuxeo.ecm.core.event.Event; 065import org.nuxeo.ecm.core.event.EventService; 066import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 067import org.nuxeo.ecm.core.query.sql.NXQL; 068import org.nuxeo.ecm.platform.audit.service.NXAuditEventsService; 069import org.nuxeo.ecm.platform.ec.notification.NotificationConstants; 070import org.nuxeo.ecm.platform.query.api.PageProvider; 071import org.nuxeo.ecm.platform.query.api.PageProviderService; 072import org.nuxeo.ecm.platform.query.nxql.NXQLQueryBuilder; 073import org.nuxeo.runtime.api.Framework; 074import org.nuxeo.runtime.model.ComponentContext; 075import org.nuxeo.runtime.model.ComponentInstance; 076import org.nuxeo.runtime.model.DefaultComponent; 077 078/** 079 * Manage list of NuxeoDrive synchronization roots and devices for a given nuxeo user. 080 */ 081public class NuxeoDriveManagerImpl extends DefaultComponent implements NuxeoDriveManager { 082 083 private static final Log log = LogFactory.getLog(NuxeoDriveManagerImpl.class); 084 085 public static final String CHANGE_FINDER_EP = "changeFinder"; 086 087 public static final String NUXEO_DRIVE_FACET = "DriveSynchronized"; 088 089 public static final String DRIVE_SUBSCRIPTIONS_PROPERTY = "drv:subscriptions"; 090 091 public static final String DOCUMENT_CHANGE_LIMIT_PROPERTY = "org.nuxeo.drive.document.change.limit"; 092 093 public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); 094 095 public static final String DRIVE_SYNC_ROOT_CACHE = "driveSyncRoot"; 096 097 public static final String DRIVE_COLLECTION_SYNC_ROOT__MEMBER_CACHE = "driveCollectionSyncRootMember"; 098 099 protected static final long COLLECTION_CONTENT_PAGE_SIZE = 1000L; 100 101 /** 102 * Cache holding the synchronization roots for a given user (first map key) and repository (second map key). 103 */ 104 protected Cache syncRootCache; 105 106 /** 107 * Cache holding the collection sync root member ids for a given user (first map key) and repository (second map 108 * key). 109 */ 110 protected Cache collectionSyncRootMemberCache; 111 112 protected static ChangeFinderRegistry changeFinderRegistry; 113 114 protected FileSystemChangeFinder changeFinder; 115 116 protected Cache getSyncRootCache() { 117 if (syncRootCache == null) { 118 syncRootCache = Framework.getService(CacheService.class).getCache(DRIVE_SYNC_ROOT_CACHE); 119 } 120 return syncRootCache; 121 } 122 123 protected Cache getCollectionSyncRootMemberCache() { 124 if (collectionSyncRootMemberCache == null) { 125 collectionSyncRootMemberCache = Framework.getService(CacheService.class).getCache( 126 DRIVE_COLLECTION_SYNC_ROOT__MEMBER_CACHE); 127 } 128 return collectionSyncRootMemberCache; 129 } 130 131 protected void clearCache() { 132 log.debug("Invalidating synchronization root cache and collection sync root member cache for all users"); 133 if (getSyncRootCache() != null) { 134 syncRootCache.invalidateAll(); 135 } 136 if (getCollectionSyncRootMemberCache() != null) { 137 collectionSyncRootMemberCache.invalidateAll(); 138 } 139 } 140 141 @Override 142 public void invalidateSynchronizationRootsCache(String userName) { 143 if (log.isDebugEnabled()) { 144 log.debug("Invalidating synchronization root cache for user: " + userName); 145 } 146 getSyncRootCache().invalidate(userName); 147 } 148 149 @Override 150 public void invalidateCollectionSyncRootMemberCache(String userName) { 151 if (log.isDebugEnabled()) { 152 log.debug("Invalidating collection sync root member cache for user: " + userName); 153 } 154 getCollectionSyncRootMemberCache().invalidate(userName); 155 } 156 157 @Override 158 public void invalidateCollectionSyncRootMemberCache() { 159 log.debug("Invalidating collection sync root member cache for all users"); 160 getCollectionSyncRootMemberCache().invalidateAll(); 161 } 162 163 @Override 164 public void registerSynchronizationRoot(Principal principal, final DocumentModel newRootContainer, 165 CoreSession session) { 166 final String userName = principal.getName(); 167 // If new root is child of a sync root, ignore registration, except for 168 // the 'Locally Edited' collection: it is under the personal workspace 169 // and we want to allow both the personal workspace and the 'Locally 170 // Edited' collection to be registered as sync roots 171 Map<String, SynchronizationRoots> syncRoots = getSynchronizationRoots(principal); 172 SynchronizationRoots synchronizationRoots = syncRoots.get(session.getRepositoryName()); 173 if (!NuxeoDriveManager.LOCALLY_EDITED_COLLECTION_NAME.equals(newRootContainer.getName())) { 174 for (String syncRootPath : synchronizationRoots.getPaths()) { 175 String syncRootPrefixedPath = syncRootPath + "/"; 176 177 if (newRootContainer.getPathAsString().startsWith(syncRootPrefixedPath)) { 178 // the only exception is when the right inheritance is 179 // blocked 180 // in the hierarchy 181 boolean rightInheritanceBlockedInTheHierarchy = false; 182 // should get only parents up to the sync root 183 184 Path parentPath = newRootContainer.getPath().removeLastSegments(1); 185 while (!"/".equals(parentPath.toString())) { 186 String parentPathAsString = parentPath.toString() + "/"; 187 if (!parentPathAsString.startsWith(syncRootPrefixedPath)) { 188 break; 189 } 190 PathRef parentRef = new PathRef(parentPathAsString); 191 if (!session.hasPermission(principal, parentRef, SecurityConstants.READ)) { 192 rightInheritanceBlockedInTheHierarchy = true; 193 break; 194 } 195 parentPath = parentPath.removeLastSegments(1); 196 } 197 if (!rightInheritanceBlockedInTheHierarchy) { 198 return; 199 } 200 } 201 } 202 } 203 204 checkCanUpdateSynchronizationRoot(newRootContainer, session); 205 206 // Unregister any sub-folder of the new root, except for the 'Locally 207 // Edited' collection 208 String newRootPrefixedPath = newRootContainer.getPathAsString() + "/"; 209 for (String existingRootPath : synchronizationRoots.getPaths()) { 210 if (!existingRootPath.endsWith(NuxeoDriveManager.LOCALLY_EDITED_COLLECTION_NAME)) { 211 if (existingRootPath.startsWith(newRootPrefixedPath)) { 212 // Unregister the nested root sub-folder first 213 PathRef ref = new PathRef(existingRootPath); 214 if (session.exists(ref)) { 215 DocumentModel subFolder = session.getDocument(ref); 216 unregisterSynchronizationRoot(principal, subFolder, session); 217 } 218 } 219 } 220 } 221 222 UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(session) { 223 @Override 224 public void run() { 225 if (!newRootContainer.hasFacet(NUXEO_DRIVE_FACET)) { 226 newRootContainer.addFacet(NUXEO_DRIVE_FACET); 227 } 228 229 fireEvent(newRootContainer, session, NuxeoDriveEvents.ABOUT_TO_REGISTER_ROOT, userName); 230 231 @SuppressWarnings("unchecked") 232 List<Map<String, Object>> subscriptions = (List<Map<String, Object>>) newRootContainer.getPropertyValue(DRIVE_SUBSCRIPTIONS_PROPERTY); 233 boolean updated = false; 234 for (Map<String, Object> subscription : subscriptions) { 235 if (userName.equals(subscription.get("username"))) { 236 subscription.put("enabled", Boolean.TRUE); 237 subscription.put("lastChangeDate", Calendar.getInstance(UTC)); 238 updated = true; 239 break; 240 } 241 } 242 if (!updated) { 243 Map<String, Object> subscription = new HashMap<String, Object>(); 244 subscription.put("username", userName); 245 subscription.put("enabled", Boolean.TRUE); 246 subscription.put("lastChangeDate", Calendar.getInstance(UTC)); 247 subscriptions.add(subscription); 248 } 249 newRootContainer.setPropertyValue(DRIVE_SUBSCRIPTIONS_PROPERTY, (Serializable) subscriptions); 250 newRootContainer.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true); 251 newRootContainer.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true); 252 DocumentModel savedNewRootContainer = session.saveDocument(newRootContainer); 253 newRootContainer.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, false); 254 newRootContainer.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, false); 255 fireEvent(savedNewRootContainer, session, NuxeoDriveEvents.ROOT_REGISTERED, userName); 256 session.save(); 257 } 258 }; 259 runner.runUnrestricted(); 260 261 invalidateSynchronizationRootsCache(userName); 262 invalidateCollectionSyncRootMemberCache(userName); 263 } 264 265 @Override 266 public void unregisterSynchronizationRoot(Principal principal, final DocumentModel rootContainer, 267 CoreSession session) { 268 final String userName = principal.getName(); 269 checkCanUpdateSynchronizationRoot(rootContainer, session); 270 UnrestrictedSessionRunner runner = new UnrestrictedSessionRunner(session) { 271 @Override 272 public void run() { 273 if (!rootContainer.hasFacet(NUXEO_DRIVE_FACET)) { 274 rootContainer.addFacet(NUXEO_DRIVE_FACET); 275 } 276 fireEvent(rootContainer, session, NuxeoDriveEvents.ABOUT_TO_UNREGISTER_ROOT, userName); 277 @SuppressWarnings("unchecked") 278 List<Map<String, Object>> subscriptions = (List<Map<String, Object>>) rootContainer.getPropertyValue(DRIVE_SUBSCRIPTIONS_PROPERTY); 279 for (Map<String, Object> subscription : subscriptions) { 280 if (userName.equals(subscription.get("username"))) { 281 subscription.put("enabled", Boolean.FALSE); 282 subscription.put("lastChangeDate", Calendar.getInstance(UTC)); 283 break; 284 } 285 } 286 rootContainer.setPropertyValue(DRIVE_SUBSCRIPTIONS_PROPERTY, (Serializable) subscriptions); 287 rootContainer.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, true); 288 rootContainer.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, true); 289 session.saveDocument(rootContainer); 290 rootContainer.putContextData(NXAuditEventsService.DISABLE_AUDIT_LOGGER, false); 291 rootContainer.putContextData(NotificationConstants.DISABLE_NOTIFICATION_SERVICE, false); 292 fireEvent(rootContainer, session, NuxeoDriveEvents.ROOT_UNREGISTERED, userName); 293 session.save(); 294 } 295 }; 296 runner.runUnrestricted(); 297 invalidateSynchronizationRootsCache(userName); 298 invalidateCollectionSyncRootMemberCache(userName); 299 } 300 301 @Override 302 public Set<IdRef> getSynchronizationRootReferences(CoreSession session) { 303 Map<String, SynchronizationRoots> syncRoots = getSynchronizationRoots(session.getPrincipal()); 304 return syncRoots.get(session.getRepositoryName()).getRefs(); 305 } 306 307 @Override 308 public void handleFolderDeletion(IdRef deleted) { 309 clearCache(); 310 } 311 312 protected void fireEvent(DocumentModel sourceDocument, CoreSession session, String eventName, 313 String impactedUserName) { 314 EventService eventService = Framework.getLocalService(EventService.class); 315 DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), sourceDocument); 316 ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, session.getRepositoryName()); 317 ctx.setProperty(CoreEventConstants.SESSION_ID, session.getSessionId()); 318 ctx.setProperty("category", NuxeoDriveEvents.EVENT_CATEGORY); 319 ctx.setProperty(NuxeoDriveEvents.IMPACTED_USERNAME_PROPERTY, impactedUserName); 320 Event event = ctx.newEvent(eventName); 321 eventService.fireEvent(event); 322 } 323 324 /** 325 * Uses the {@link AuditChangeFinder} to get the summary of document changes for the given user and last successful 326 * synchronization date. 327 * <p> 328 * The {@link #DOCUMENT_CHANGE_LIMIT_PROPERTY} Framework property is used as a limit of document changes to fetch 329 * from the audit logs. Default value is 1000. If {@code lastSuccessfulSync} is missing (i.e. set to a negative 330 * value), the filesystem change summary is empty but the returned sync date is set to the actual server timestamp 331 * so that the client can reuse it as a starting timestamp for a future incremental diff request. 332 */ 333 @Override 334 public FileSystemChangeSummary getChangeSummary(Principal principal, Map<String, Set<IdRef>> lastSyncRootRefs, 335 long lastSuccessfulSync) { 336 Map<String, SynchronizationRoots> roots = getSynchronizationRoots(principal); 337 return getChangeSummary(principal, lastSyncRootRefs, roots, new HashMap<String, Set<String>>(), 338 lastSuccessfulSync, false); 339 } 340 341 /** 342 * Uses the {@link AuditChangeFinder} to get the summary of document changes for the given user and lower bound. 343 * <p> 344 * The {@link #DOCUMENT_CHANGE_LIMIT_PROPERTY} Framework property is used as a limit of document changes to fetch 345 * from the audit logs. Default value is 1000. If {@code lowerBound} is missing (i.e. set to a negative value), the 346 * filesystem change summary is empty but the returned upper bound is set to the greater event log id so that the 347 * client can reuse it as a starting id for a future incremental diff request. 348 */ 349 @Override 350 public FileSystemChangeSummary getChangeSummaryIntegerBounds(Principal principal, 351 Map<String, Set<IdRef>> lastSyncRootRefs, long lowerBound) { 352 Map<String, SynchronizationRoots> roots = getSynchronizationRoots(principal); 353 Map<String, Set<String>> collectionSyncRootMemberIds = getCollectionSyncRootMemberIds(principal); 354 return getChangeSummary(principal, lastSyncRootRefs, roots, collectionSyncRootMemberIds, lowerBound, true); 355 } 356 357 protected FileSystemChangeSummary getChangeSummary(Principal principal, Map<String, Set<IdRef>> lastActiveRootRefs, 358 Map<String, SynchronizationRoots> roots, Map<String, Set<String>> collectionSyncRootMemberIds, 359 long lowerBound, boolean integerBounds) { 360 List<FileSystemItemChange> allChanges = new ArrayList<FileSystemItemChange>(); 361 long syncDate; 362 long upperBound; 363 if (integerBounds) { 364 upperBound = changeFinder.getUpperBound(); 365 // Truncate sync date to 0 milliseconds 366 syncDate = System.currentTimeMillis(); 367 syncDate = syncDate - (syncDate % 1000); 368 } else { 369 upperBound = changeFinder.getCurrentDate(); 370 syncDate = upperBound; 371 } 372 Boolean hasTooManyChanges = Boolean.FALSE; 373 int limit = Integer.parseInt(Framework.getProperty(DOCUMENT_CHANGE_LIMIT_PROPERTY, "1000")); 374 375 // Compute the list of all repositories to consider for the aggregate 376 // summary 377 Set<String> allRepositories = new TreeSet<String>(); 378 allRepositories.addAll(roots.keySet()); 379 allRepositories.addAll(lastActiveRootRefs.keySet()); 380 allRepositories.addAll(collectionSyncRootMemberIds.keySet()); 381 382 if (!allRepositories.isEmpty() && lowerBound >= 0 && upperBound > lowerBound) { 383 for (String repositoryName : allRepositories) { 384 try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) { 385 // Get document changes 386 Set<IdRef> lastRefs = lastActiveRootRefs.get(repositoryName); 387 if (lastRefs == null) { 388 lastRefs = Collections.emptySet(); 389 } 390 SynchronizationRoots activeRoots = roots.get(repositoryName); 391 if (activeRoots == null) { 392 activeRoots = SynchronizationRoots.getEmptyRoots(repositoryName); 393 } 394 Set<String> repoCollectionSyncRootMemberIds = collectionSyncRootMemberIds.get(repositoryName); 395 if (repoCollectionSyncRootMemberIds == null) { 396 repoCollectionSyncRootMemberIds = Collections.emptySet(); 397 } 398 List<FileSystemItemChange> changes; 399 if (integerBounds) { 400 changes = changeFinder.getFileSystemChangesIntegerBounds(session, lastRefs, activeRoots, 401 repoCollectionSyncRootMemberIds, lowerBound, upperBound, limit); 402 } else { 403 changes = changeFinder.getFileSystemChanges(session, lastRefs, activeRoots, lowerBound, 404 upperBound, limit); 405 } 406 allChanges.addAll(changes); 407 } catch (TooManyChangesException e) { 408 hasTooManyChanges = Boolean.TRUE; 409 allChanges.clear(); 410 break; 411 } 412 } 413 } 414 415 // Send back to the client the list of currently active roots to be able 416 // to efficiently detect root unregistration events for the next 417 // incremental change summary 418 Map<String, Set<IdRef>> activeRootRefs = new HashMap<String, Set<IdRef>>(); 419 for (Map.Entry<String, SynchronizationRoots> rootsEntry : roots.entrySet()) { 420 activeRootRefs.put(rootsEntry.getKey(), rootsEntry.getValue().getRefs()); 421 } 422 return new FileSystemChangeSummaryImpl(allChanges, activeRootRefs, syncDate, upperBound, hasTooManyChanges); 423 } 424 425 @Override 426 @SuppressWarnings("unchecked") 427 public Map<String, SynchronizationRoots> getSynchronizationRoots(Principal principal) { 428 String userName = principal.getName(); 429 Map<String, SynchronizationRoots> syncRoots = (Map<String, SynchronizationRoots>) getSyncRootCache().get( 430 userName); 431 if (syncRoots == null) { 432 syncRoots = computeSynchronizationRoots(computeSyncRootsQuery(userName), principal); 433 getSyncRootCache().put(userName, (Serializable) syncRoots); 434 } 435 return syncRoots; 436 } 437 438 @Override 439 @SuppressWarnings("unchecked") 440 public Map<String, Set<String>> getCollectionSyncRootMemberIds(Principal principal) { 441 String userName = principal.getName(); 442 Map<String, Set<String>> collSyncRootMemberIds = (Map<String, Set<String>>) getCollectionSyncRootMemberCache().get( 443 userName); 444 if (collSyncRootMemberIds == null) { 445 collSyncRootMemberIds = computeCollectionSyncRootMemberIds(principal); 446 getCollectionSyncRootMemberCache().put(userName, (Serializable) collSyncRootMemberIds); 447 } 448 return collSyncRootMemberIds; 449 } 450 451 @Override 452 public boolean isSynchronizationRoot(Principal principal, DocumentModel doc) { 453 String repoName = doc.getRepositoryName(); 454 SynchronizationRoots syncRoots = getSynchronizationRoots(principal).get(repoName); 455 return syncRoots.getRefs().contains(doc.getRef()); 456 } 457 458 protected Map<String, SynchronizationRoots> computeSynchronizationRoots(String query, Principal principal) { 459 Map<String, SynchronizationRoots> syncRoots = new HashMap<String, SynchronizationRoots>(); 460 RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class); 461 for (String repositoryName : repositoryManager.getRepositoryNames()) { 462 try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) { 463 syncRoots.putAll(queryAndFetchSynchronizationRoots(session, query)); 464 } 465 } 466 return syncRoots; 467 } 468 469 protected Map<String, SynchronizationRoots> queryAndFetchSynchronizationRoots(CoreSession session, String query) { 470 Map<String, SynchronizationRoots> syncRoots = new HashMap<String, SynchronizationRoots>(); 471 Set<IdRef> references = new LinkedHashSet<IdRef>(); 472 Set<String> paths = new LinkedHashSet<String>(); 473 IterableQueryResult results = session.queryAndFetch(query, NXQL.NXQL); 474 try { 475 for (Map<String, Serializable> result : results) { 476 IdRef docRef = new IdRef(result.get("ecm:uuid").toString()); 477 try { 478 DocumentModel doc = session.getDocument(docRef); 479 references.add(docRef); 480 paths.add(doc.getPathAsString()); 481 } catch (DocumentNotFoundException e) { 482 log.warn(String.format( 483 "Document %s not found, not adding it to the list of synchronization roots for user %s.", 484 docRef, session.getPrincipal().getName())); 485 } catch (DocumentSecurityException e) { 486 log.warn(String.format( 487 "User %s cannot access document %s, not adding it to the list of synchronization roots.", 488 session.getPrincipal().getName(), docRef)); 489 } 490 } 491 } finally { 492 results.close(); 493 } 494 SynchronizationRoots repoSyncRoots = new SynchronizationRoots(session.getRepositoryName(), paths, references); 495 syncRoots.put(session.getRepositoryName(), repoSyncRoots); 496 return syncRoots; 497 } 498 499 @SuppressWarnings("unchecked") 500 protected Map<String, Set<String>> computeCollectionSyncRootMemberIds(Principal principal) { 501 Map<String, Set<String>> collectionSyncRootMemberIds = new HashMap<String, Set<String>>(); 502 PageProviderService pageProviderService = Framework.getLocalService(PageProviderService.class); 503 RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class); 504 for (String repositoryName : repositoryManager.getRepositoryNames()) { 505 Set<String> collectionMemberIds = new HashSet<String>(); 506 try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) { 507 Map<String, Serializable> props = new HashMap<String, Serializable>(); 508 props.put(CORE_SESSION_PROPERTY, (Serializable) session); 509 PageProvider<DocumentModel> collectionPageProvider = (PageProvider<DocumentModel>) pageProviderService.getPageProvider( 510 CollectionConstants.ALL_COLLECTIONS_PAGE_PROVIDER, null, null, 0L, props); 511 List<DocumentModel> collections = collectionPageProvider.getCurrentPage(); 512 for (DocumentModel collection : collections) { 513 if (isSynchronizationRoot(principal, collection)) { 514 PageProvider<DocumentModel> collectionMemberPageProvider = (PageProvider<DocumentModel>) pageProviderService.getPageProvider( 515 CollectionConstants.COLLECTION_CONTENT_PAGE_PROVIDER, null, 516 COLLECTION_CONTENT_PAGE_SIZE, 0L, props, collection.getId()); 517 List<DocumentModel> collectionMembers = collectionMemberPageProvider.getCurrentPage(); 518 for (DocumentModel collectionMember : collectionMembers) { 519 collectionMemberIds.add(collectionMember.getId()); 520 } 521 } 522 } 523 collectionSyncRootMemberIds.put(repositoryName, collectionMemberIds); 524 } 525 } 526 return collectionSyncRootMemberIds; 527 } 528 529 protected void checkCanUpdateSynchronizationRoot(DocumentModel newRootContainer, CoreSession session) { 530 // Cannot update a proxy or a version 531 if (newRootContainer.isProxy() || newRootContainer.isVersion()) { 532 throw new NuxeoException(String.format("Document '%s' (%s) is not a suitable synchronization root" 533 + " as it is either a readonly proxy or an archived version.", newRootContainer.getTitle(), 534 newRootContainer.getRef())); 535 } 536 } 537 538 @Override 539 public FileSystemChangeFinder getChangeFinder() { 540 return changeFinder; 541 } 542 543 @Override 544 @Deprecated 545 public void setChangeFinder(FileSystemChangeFinder changeFinder) { 546 this.changeFinder = changeFinder; 547 } 548 549 /** 550 * @since 5.9.5 551 */ 552 protected String computeSyncRootsQuery(String username) { 553 return String.format("SELECT ecm:uuid FROM Document WHERE %s/*1/username = %s" + " AND %s/*1/enabled = 1" 554 + " AND ecm:currentLifeCycleState <> 'deleted'" + " ORDER BY dc:title, dc:created DESC", 555 DRIVE_SUBSCRIPTIONS_PROPERTY, NXQLQueryBuilder.prepareStringLiteral(username, true, true), 556 DRIVE_SUBSCRIPTIONS_PROPERTY); 557 } 558 559 @Override 560 public void addToLocallyEditedCollection(CoreSession session, DocumentModel doc) { 561 562 // Add document to "Locally Edited" collection, creating if if not 563 // exists 564 CollectionManager cm = Framework.getService(CollectionManager.class); 565 DocumentModel userCollections = cm.getUserDefaultCollections(doc, session); 566 DocumentRef locallyEditedCollectionRef = new PathRef(userCollections.getPath().toString(), 567 LOCALLY_EDITED_COLLECTION_NAME); 568 DocumentModel locallyEditedCollection = null; 569 if (session.exists(locallyEditedCollectionRef)) { 570 locallyEditedCollection = session.getDocument(locallyEditedCollectionRef); 571 cm.addToCollection(locallyEditedCollection, doc, session); 572 } else { 573 cm.addToNewCollection(LOCALLY_EDITED_COLLECTION_NAME, "Documents locally edited with Nuxeo Drive", doc, 574 session); 575 locallyEditedCollection = session.getDocument(locallyEditedCollectionRef); 576 } 577 578 // Register "Locally Edited" collection as a synchronization root if not 579 // already the case 580 Set<IdRef> syncRootRefs = getSynchronizationRootReferences(session); 581 if (!syncRootRefs.contains(new IdRef(locallyEditedCollection.getId()))) { 582 registerSynchronizationRoot(session.getPrincipal(), locallyEditedCollection, session); 583 } 584 } 585 586 /*------------------------ DefaultComponent -----------------------------*/ 587 @Override 588 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 589 if (CHANGE_FINDER_EP.equals(extensionPoint)) { 590 changeFinderRegistry.addContribution((ChangeFinderDescriptor) contribution); 591 } else { 592 log.error("Unknown extension point " + extensionPoint); 593 } 594 } 595 596 @Override 597 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 598 if (CHANGE_FINDER_EP.equals(extensionPoint)) { 599 changeFinderRegistry.removeContribution((ChangeFinderDescriptor) contribution); 600 } else { 601 log.error("Unknown extension point " + extensionPoint); 602 } 603 } 604 605 @Override 606 public void activate(ComponentContext context) { 607 super.activate(context); 608 if (changeFinderRegistry == null) { 609 changeFinderRegistry = new ChangeFinderRegistry(); 610 } 611 } 612 613 @Override 614 public void deactivate(ComponentContext context) { 615 super.deactivate(context); 616 changeFinderRegistry = null; 617 } 618 619 /** 620 * Sorts the contributed factories according to their order. 621 */ 622 @Override 623 public void applicationStarted(ComponentContext context) { 624 initChangeFinder(); 625 } 626 627 protected void initChangeFinder() { 628 changeFinder = changeFinderRegistry.changeFinder; 629 } 630 631}