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}