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