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