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