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