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