001/*
002 * (C) Copyright 2012-2018 Nuxeo (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 *     Antoine Taillefer <ataillefer@nuxeo.com>
018 */
019package org.nuxeo.drive.service.impl;
020
021import java.util.ArrayList;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.concurrent.Semaphore;
027
028import org.apache.commons.lang3.StringUtils;
029import org.apache.logging.log4j.LogManager;
030import org.apache.logging.log4j.Logger;
031import org.nuxeo.drive.adapter.FileSystemItem;
032import org.nuxeo.drive.adapter.FolderItem;
033import org.nuxeo.drive.adapter.NuxeoDriveContribException;
034import org.nuxeo.drive.adapter.RootlessItemException;
035import org.nuxeo.drive.service.FileSystemItemAdapterService;
036import org.nuxeo.drive.service.FileSystemItemFactory;
037import org.nuxeo.drive.service.TopLevelFolderItemFactory;
038import org.nuxeo.drive.service.VirtualFolderItemFactory;
039import org.nuxeo.ecm.core.api.DocumentModel;
040import org.nuxeo.runtime.api.Framework;
041import org.nuxeo.runtime.model.ComponentContext;
042import org.nuxeo.runtime.model.ComponentInstance;
043import org.nuxeo.runtime.model.DefaultComponent;
044import org.nuxeo.runtime.services.config.ConfigurationService;
045
046/**
047 * Default implementation of the {@link FileSystemItemAdapterService}.
048 *
049 * @author Antoine Taillefer
050 */
051public class FileSystemItemAdapterServiceImpl extends DefaultComponent implements FileSystemItemAdapterService {
052
053    private static final Logger log = LogManager.getLogger(FileSystemItemAdapterServiceImpl.class);
054
055    public static final String FILE_SYSTEM_ITEM_FACTORY_EP = "fileSystemItemFactory";
056
057    public static final String TOP_LEVEL_FOLDER_ITEM_FACTORY_EP = "topLevelFolderItemFactory";
058
059    public static final String ACTIVE_FILE_SYSTEM_ITEM_FACTORIES_EP = "activeFileSystemItemFactories";
060
061    protected static final String CONCURRENT_SCROLL_BATCH_LIMIT = "org.nuxeo.drive.concurrentScrollBatchLimit";
062
063    protected static final int CONCURRENT_SCROLL_BATCH_LIMIT_DEFAULT = 4;
064
065    protected TopLevelFolderItemFactoryRegistry topLevelFolderItemFactoryRegistry;
066
067    protected FileSystemItemFactoryRegistry fileSystemItemFactoryRegistry;
068
069    protected ActiveTopLevelFolderItemFactoryRegistry activeTopLevelFolderItemFactoryRegistry;
070
071    protected ActiveFileSystemItemFactoryRegistry activeFileSystemItemFactoryRegistry;
072
073    protected TopLevelFolderItemFactory topLevelFolderItemFactory;
074
075    protected List<FileSystemItemFactoryWrapper> fileSystemItemFactories;
076
077    protected Semaphore scrollBatchSemaphore;
078
079    /*------------------------ DefaultComponent -----------------------------*/
080    @Override
081    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
082        if (FILE_SYSTEM_ITEM_FACTORY_EP.equals(extensionPoint)) {
083            fileSystemItemFactoryRegistry.addContribution((FileSystemItemFactoryDescriptor) contribution);
084        } else if (TOP_LEVEL_FOLDER_ITEM_FACTORY_EP.equals(extensionPoint)) {
085            topLevelFolderItemFactoryRegistry.addContribution((TopLevelFolderItemFactoryDescriptor) contribution);
086        } else if (ACTIVE_FILE_SYSTEM_ITEM_FACTORIES_EP.equals(extensionPoint)) {
087            if (contribution instanceof ActiveTopLevelFolderItemFactoryDescriptor) {
088                activeTopLevelFolderItemFactoryRegistry.addContribution(
089                        (ActiveTopLevelFolderItemFactoryDescriptor) contribution);
090            } else if (contribution instanceof ActiveFileSystemItemFactoriesDescriptor) {
091                activeFileSystemItemFactoryRegistry.addContribution(
092                        (ActiveFileSystemItemFactoriesDescriptor) contribution);
093            }
094        } else {
095            log.error("Unknown extension point {}", extensionPoint);
096        }
097    }
098
099    @Override
100    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
101        if (FILE_SYSTEM_ITEM_FACTORY_EP.equals(extensionPoint)) {
102            fileSystemItemFactoryRegistry.removeContribution((FileSystemItemFactoryDescriptor) contribution);
103        } else if (TOP_LEVEL_FOLDER_ITEM_FACTORY_EP.equals(extensionPoint)) {
104            topLevelFolderItemFactoryRegistry.removeContribution((TopLevelFolderItemFactoryDescriptor) contribution);
105        } else if (ACTIVE_FILE_SYSTEM_ITEM_FACTORIES_EP.equals(extensionPoint)) {
106            if (contribution instanceof ActiveTopLevelFolderItemFactoryDescriptor) {
107                activeTopLevelFolderItemFactoryRegistry.removeContribution(
108                        (ActiveTopLevelFolderItemFactoryDescriptor) contribution);
109            } else if (contribution instanceof ActiveFileSystemItemFactoriesDescriptor) {
110                activeFileSystemItemFactoryRegistry.removeContribution(
111                        (ActiveFileSystemItemFactoriesDescriptor) contribution);
112            }
113        } else {
114            log.error("Unknown extension point {}", extensionPoint);
115        }
116    }
117
118    @Override
119    public void activate(ComponentContext context) {
120        fileSystemItemFactoryRegistry = new FileSystemItemFactoryRegistry();
121        topLevelFolderItemFactoryRegistry = new TopLevelFolderItemFactoryRegistry();
122        activeTopLevelFolderItemFactoryRegistry = new ActiveTopLevelFolderItemFactoryRegistry();
123        activeFileSystemItemFactoryRegistry = new ActiveFileSystemItemFactoryRegistry();
124        fileSystemItemFactories = new ArrayList<>();
125    }
126
127    @Override
128    public void deactivate(ComponentContext context) {
129        super.deactivate(context);
130        fileSystemItemFactoryRegistry = null;
131        topLevelFolderItemFactoryRegistry = null;
132        activeTopLevelFolderItemFactoryRegistry = null;
133        activeFileSystemItemFactoryRegistry = null;
134        fileSystemItemFactories = null;
135    }
136
137    /**
138     * Sorts the contributed factories according to their order and initializes the {@link #scrollBatchSemaphore}.
139     */
140    @Override
141    public void start(ComponentContext context) {
142        topLevelFolderItemFactory = topLevelFolderItemFactoryRegistry.getActiveFactory(
143                activeTopLevelFolderItemFactoryRegistry.activeFactory);
144        fileSystemItemFactories = fileSystemItemFactoryRegistry.getOrderedActiveFactories(
145                activeFileSystemItemFactoryRegistry.activeFactories);
146        int concurrentScrollBatchLimit = Framework.getService(ConfigurationService.class)
147                                                  .getInteger(CONCURRENT_SCROLL_BATCH_LIMIT,
148                                                          CONCURRENT_SCROLL_BATCH_LIMIT_DEFAULT);
149        scrollBatchSemaphore = new Semaphore(concurrentScrollBatchLimit, false);
150    }
151
152    @Override
153    public void stop(ComponentContext context) throws InterruptedException {
154        topLevelFolderItemFactory = null;
155        fileSystemItemFactories = null;
156        scrollBatchSemaphore = null;
157    }
158
159    /*------------------------ FileSystemItemAdapterService -----------------------*/
160    @Override
161    public FileSystemItem getFileSystemItem(DocumentModel doc) {
162        return getFileSystemItem(doc, false, null, false, false, true);
163    }
164
165    @Override
166    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted) {
167        return getFileSystemItem(doc, false, null, includeDeleted, false, true);
168    }
169
170    @Override
171    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted,
172            boolean relaxSyncRootConstraint) {
173        return getFileSystemItem(doc, false, null, includeDeleted, relaxSyncRootConstraint, true);
174    }
175
176    @Override
177    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted, boolean relaxSyncRootConstraint,
178            boolean getLockInfo) {
179        return getFileSystemItem(doc, false, null, includeDeleted, relaxSyncRootConstraint, getLockInfo);
180    }
181
182    @Override
183    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem) {
184        return getFileSystemItem(doc, true, parentItem, false, false, true);
185    }
186
187    @Override
188    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted) {
189        return getFileSystemItem(doc, true, parentItem, includeDeleted, false, true);
190    }
191
192    @Override
193    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted,
194            boolean relaxSyncRootConstraint) {
195        return getFileSystemItem(doc, true, parentItem, includeDeleted, relaxSyncRootConstraint, true);
196    }
197
198    @Override
199    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted,
200            boolean relaxSyncRootConstraint, boolean getLockInfo) {
201        return getFileSystemItem(doc, true, parentItem, includeDeleted, relaxSyncRootConstraint, getLockInfo);
202    }
203
204    /**
205     * Iterates on the ordered contributed file system item factories until if finds one that can handle the given
206     * {@link FileSystemItem} id.
207     */
208    @Override
209    public FileSystemItemFactory getFileSystemItemFactoryForId(String id) {
210        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
211        while (factoriesIt.hasNext()) {
212            FileSystemItemFactoryWrapper factoryWrapper = factoriesIt.next();
213            FileSystemItemFactory factory = factoryWrapper.getFactory();
214            if (factory.canHandleFileSystemItemId(id)) {
215                return factory;
216            }
217        }
218        // No fileSystemItemFactory found, try the topLevelFolderItemFactory
219        if (getTopLevelFolderItemFactory().canHandleFileSystemItemId(id)) {
220            return getTopLevelFolderItemFactory();
221        }
222        throw new NuxeoDriveContribException(String.format(
223                "No fileSystemItemFactory found for FileSystemItem with id %s. Please check the contributions to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"fileSystemItemFactory\"> and make sure there is at least one defining a FileSystemItemFactory class for which the #canHandleFileSystemItemId(String id) method returns true.",
224                id));
225    }
226
227    @Override
228    public TopLevelFolderItemFactory getTopLevelFolderItemFactory() {
229        if (topLevelFolderItemFactory == null) {
230            throw new NuxeoDriveContribException(
231                    "Found no active top level folder item factory. Please check there is a contribution to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"topLevelFolderItemFactory\"> and to <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"activeTopLevelFolderItemFactory\">.");
232        }
233        return topLevelFolderItemFactory;
234    }
235
236    @Override
237    public VirtualFolderItemFactory getVirtualFolderItemFactory(String factoryName) {
238        FileSystemItemFactory factory = getFileSystemItemFactory(factoryName);
239        if (factory == null) {
240            throw new NuxeoDriveContribException(String.format(
241                    "No factory named %s. Please check the contributions to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"fileSystemItemFactory\">.",
242                    factoryName));
243        }
244        if (!(factory instanceof VirtualFolderItemFactory)) {
245            throw new NuxeoDriveContribException(
246                    String.format("Factory class %s for factory %s is not a VirtualFolderItemFactory.",
247                            factory.getClass().getName(), factory.getName()));
248        }
249        return (VirtualFolderItemFactory) factory;
250    }
251
252    @Override
253    public Set<String> getActiveFileSystemItemFactories() {
254        if (activeFileSystemItemFactoryRegistry.activeFactories.isEmpty()) {
255            throw new NuxeoDriveContribException(
256                    "Found no active file system item factories. Please check there is a contribution to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"activeFileSystemItemFactories\"> declaring at least one factory.");
257        }
258        return activeFileSystemItemFactoryRegistry.activeFactories;
259    }
260
261    @Override
262    public Semaphore getScrollBatchSemaphore() {
263        return scrollBatchSemaphore;
264    }
265
266    /*------------------------- For test purpose ----------------------------------*/
267    public Map<String, FileSystemItemFactoryDescriptor> getFileSystemItemFactoryDescriptors() {
268        return fileSystemItemFactoryRegistry.factoryDescriptors;
269    }
270
271    public List<FileSystemItemFactoryWrapper> getFileSystemItemFactories() {
272        return fileSystemItemFactories;
273    }
274
275    public FileSystemItemFactory getFileSystemItemFactory(String name) {
276        for (FileSystemItemFactoryWrapper factoryWrapper : fileSystemItemFactories) {
277            FileSystemItemFactory factory = factoryWrapper.getFactory();
278            if (name.equals(factory.getName())) {
279                return factory;
280            }
281        }
282        log.debug("No fileSystemItemFactory named {}, returning null.", name);
283        return null;
284    }
285
286    /**
287     * @deprecated since 9.3 this is method is not needed anymore with hot reload and standby strategy, but kept due to
288     *             some issues in operation NuxeoDriveSetActiveFactories which freeze Jetty in unit tests when wanting
289     *             to use standby strategy
290     */
291    @Deprecated
292    public void setActiveFactories() {
293        topLevelFolderItemFactory = topLevelFolderItemFactoryRegistry.getActiveFactory(
294                activeTopLevelFolderItemFactoryRegistry.activeFactory);
295        fileSystemItemFactories = fileSystemItemFactoryRegistry.getOrderedActiveFactories(
296                activeFileSystemItemFactoryRegistry.activeFactories);
297    }
298
299    /*--------------------------- Protected ---------------------------------------*/
300    /**
301     * Tries to adapt the given document as the top level {@link FolderItem}. If it doesn't match, iterates on the
302     * ordered contributed file system item factories until it finds one that matches and retrieves a non null
303     * {@link FileSystemItem} for the given document. A file system item factory matches if:
304     * <ul>
305     * <li>It is not bound to any docType nor facet (this is the case for the default factory contribution
306     * {@code defaultFileSystemItemFactory} bound to {@link DefaultFileSystemItemFactory})</li>
307     * <li>It is bound to a docType that matches the given doc's type</li>
308     * <li>It is bound to a facet that matches one of the given doc's facets</li>
309     * </ul>
310     */
311    protected FileSystemItem getFileSystemItem(DocumentModel doc, boolean forceParentItem, FolderItem parentItem,
312            boolean includeDeleted, boolean relaxSyncRootConstraint, boolean getLockInfo) {
313
314        FileSystemItem fileSystemItem;
315
316        // Try the topLevelFolderItemFactory
317        if (forceParentItem) {
318            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, parentItem, includeDeleted,
319                    relaxSyncRootConstraint, getLockInfo);
320        } else {
321            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, includeDeleted,
322                    relaxSyncRootConstraint, getLockInfo);
323        }
324        if (fileSystemItem != null) {
325            return fileSystemItem;
326        } else {
327            log.debug(
328                    "The topLevelFolderItemFactory is not able to adapt document {} as a FileSystemItem => trying fileSystemItemFactories.",
329                    doc::getId);
330        }
331
332        // Try the fileSystemItemFactories
333        FileSystemItemFactoryWrapper matchingFactory = null;
334
335        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
336        while (factoriesIt.hasNext()) {
337            FileSystemItemFactoryWrapper factory = factoriesIt.next();
338            log.debug("Trying to adapt document {} (path: {}) as a FileSystemItem with factory {}", doc::getId,
339                    doc::getPathAsString, () -> factory.getFactory().getName());
340            if (generalFactoryMatches(factory) || docTypeFactoryMatches(factory, doc)
341                    || facetFactoryMatches(factory, doc, relaxSyncRootConstraint)) {
342                matchingFactory = factory;
343                try {
344                    if (forceParentItem) {
345                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, parentItem, includeDeleted,
346                                relaxSyncRootConstraint, getLockInfo);
347                    } else {
348                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, includeDeleted,
349                                relaxSyncRootConstraint, getLockInfo);
350                    }
351                } catch (RootlessItemException e) {
352                    // Give more information in the exception message on the
353                    // document whose adaption failed to recursively find the
354                    // top level item.
355                    throw new RootlessItemException(String.format(
356                            "Cannot find path to registered top" + " level when adapting document "
357                                    + " '%s' (path: %s) with factory %s",
358                            doc.getTitle(), doc.getPathAsString(), factory.getFactory().getName()), e);
359                }
360                if (fileSystemItem != null) {
361                    log.debug("Adapted document '{}' (path: {}) to item with path {} with factory {}", doc::getTitle,
362                            doc::getPathAsString, fileSystemItem::getPath, () -> factory.getFactory().getName());
363                    return fileSystemItem;
364                }
365            }
366        }
367
368        if (matchingFactory == null) {
369            log.debug(
370                    "None of the fileSystemItemFactories matches document {} => returning null. Please check the contributions to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"fileSystemItemFactory\">.",
371                    doc::getId);
372        } else {
373            log.debug(
374                    "None of the fileSystemItemFactories matching document {} were able to adapt this document as a FileSystemItem => returning null.",
375                    doc::getId);
376        }
377        return fileSystemItem;
378    }
379
380    protected boolean generalFactoryMatches(FileSystemItemFactoryWrapper factory) {
381        boolean matches = StringUtils.isEmpty(factory.getDocType()) && StringUtils.isEmpty(factory.getFacet());
382        if (matches) {
383            log.trace("General factory {} matches", factory);
384        }
385        return matches;
386    }
387
388    protected boolean docTypeFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc) {
389        boolean matches = !StringUtils.isEmpty(factory.getDocType()) && factory.getDocType().equals(doc.getType());
390        if (matches) {
391            log.trace("DocType factory {} matches for doc {} (path: {})", () -> factory, doc::getId,
392                    doc::getPathAsString);
393        }
394        return matches;
395    }
396
397    protected boolean facetFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc,
398            boolean relaxSyncRootConstraint) {
399        if (!StringUtils.isEmpty(factory.getFacet())) {
400            for (String docFacet : doc.getFacets()) {
401                if (factory.getFacet().equals(docFacet)) {
402                    // Handle synchronization root case
403                    if (NuxeoDriveManagerImpl.NUXEO_DRIVE_FACET.equals(docFacet)) {
404                        boolean matches = syncRootFactoryMatches(doc, relaxSyncRootConstraint);
405                        if (matches) {
406                            log.trace("Facet factory {} matches for doc {} (path: {})", () -> factory, doc::getId,
407                                    doc::getPathAsString);
408                        }
409                        return matches;
410                    } else {
411                        log.trace("Facet factory {} matches for doc {} (path: {})", () -> factory, doc::getId,
412                                doc::getPathAsString);
413                        return true;
414                    }
415                }
416            }
417        }
418        return false;
419    }
420
421    @SuppressWarnings("unchecked")
422    protected boolean syncRootFactoryMatches(DocumentModel doc, boolean relaxSyncRootConstraint) {
423        String userName = doc.getPrincipal().getName();
424        List<Map<String, Object>> subscriptions = (List<Map<String, Object>>) doc.getPropertyValue(
425                NuxeoDriveManagerImpl.DRIVE_SUBSCRIPTIONS_PROPERTY);
426        for (Map<String, Object> subscription : subscriptions) {
427            if (Boolean.TRUE.equals(subscription.get("enabled"))) {
428                if (userName.equals(subscription.get("username"))) {
429                    log.trace("Doc {} (path: {}) registered as a sync root for user {}", doc::getId,
430                            doc::getPathAsString, () -> userName);
431                    return true;
432                }
433                if (relaxSyncRootConstraint) {
434                    log.trace(
435                            "Doc {} (path: {}) registered as a sync root for at least one user (relaxSyncRootConstraint is true)",
436                            doc::getId, doc::getPathAsString);
437                    return true;
438                }
439            }
440        }
441        return false;
442    }
443
444}