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