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