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