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