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