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 *     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.lang.StringUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
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 Log log = LogFactory.getLog(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 String 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<FileSystemItemFactoryWrapper>();
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 = Integer.parseInt(Framework.getService(ConfigurationService.class).getProperty(
147                CONCURRENT_SCROLL_BATCH_LIMIT, CONCURRENT_SCROLL_BATCH_LIMIT_DEFAULT));
148        scrollBatchSemaphore = new Semaphore(concurrentScrollBatchLimit, false);
149    }
150
151    @Override
152    public void stop(ComponentContext context) throws InterruptedException {
153        topLevelFolderItemFactory = null;
154        fileSystemItemFactories = null;
155        scrollBatchSemaphore = null;
156    }
157
158    /*------------------------ FileSystemItemAdapterService -----------------------*/
159    @Override
160    public FileSystemItem getFileSystemItem(DocumentModel doc) {
161        return getFileSystemItem(doc, false, null, false, false, true);
162    }
163
164    @Override
165    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted) {
166        return getFileSystemItem(doc, false, null, includeDeleted, false, true);
167    }
168
169    @Override
170    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted,
171            boolean relaxSyncRootConstraint) {
172        return getFileSystemItem(doc, false, null, includeDeleted, relaxSyncRootConstraint, true);
173    }
174
175    @Override
176    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted, boolean relaxSyncRootConstraint,
177            boolean getLockInfo) {
178        return getFileSystemItem(doc, false, null, includeDeleted, relaxSyncRootConstraint, getLockInfo);
179    }
180
181    @Override
182    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem) {
183        return getFileSystemItem(doc, true, parentItem, false, false, true);
184    }
185
186    @Override
187    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted) {
188        return getFileSystemItem(doc, true, parentItem, includeDeleted, false, true);
189    }
190
191    @Override
192    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted,
193            boolean relaxSyncRootConstraint) {
194        return getFileSystemItem(doc, true, parentItem, includeDeleted, relaxSyncRootConstraint, true);
195    }
196
197    @Override
198    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted,
199            boolean relaxSyncRootConstraint, boolean getLockInfo) {
200        return getFileSystemItem(doc, true, parentItem, includeDeleted, relaxSyncRootConstraint, getLockInfo);
201    }
202
203    /**
204     * Iterates on the ordered contributed file system item factories until if finds one that can handle the given
205     * {@link FileSystemItem} id.
206     */
207    @Override
208    public FileSystemItemFactory getFileSystemItemFactoryForId(String id) {
209        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
210        while (factoriesIt.hasNext()) {
211            FileSystemItemFactoryWrapper factoryWrapper = factoriesIt.next();
212            FileSystemItemFactory factory = factoryWrapper.getFactory();
213            if (factory.canHandleFileSystemItemId(id)) {
214                return factory;
215            }
216        }
217        // No fileSystemItemFactory found, try the topLevelFolderItemFactory
218        if (getTopLevelFolderItemFactory().canHandleFileSystemItemId(id)) {
219            return getTopLevelFolderItemFactory();
220        }
221        throw new NuxeoDriveContribException(String.format(
222                "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.",
223                id));
224    }
225
226    @Override
227    public TopLevelFolderItemFactory getTopLevelFolderItemFactory() {
228        if (topLevelFolderItemFactory == null) {
229            throw new NuxeoDriveContribException(
230                    "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\">.");
231        }
232        return topLevelFolderItemFactory;
233    }
234
235    @Override
236    public VirtualFolderItemFactory getVirtualFolderItemFactory(String factoryName) {
237        FileSystemItemFactory factory = getFileSystemItemFactory(factoryName);
238        if (factory == null) {
239            throw new NuxeoDriveContribException(String.format(
240                    "No factory named %s. Please check the contributions to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"fileSystemItemFactory\">.",
241                    factoryName));
242        }
243        if (!(factory instanceof VirtualFolderItemFactory)) {
244            throw new NuxeoDriveContribException(
245                    String.format("Factory class %s for factory %s is not a VirtualFolderItemFactory.",
246                            factory.getClass().getName(), factory.getName()));
247        }
248        return (VirtualFolderItemFactory) factory;
249    }
250
251    @Override
252    public Set<String> getActiveFileSystemItemFactories() {
253        if (activeFileSystemItemFactoryRegistry.activeFactories.isEmpty()) {
254            throw new NuxeoDriveContribException(
255                    "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.");
256        }
257        return activeFileSystemItemFactoryRegistry.activeFactories;
258    }
259
260    @Override
261    public Semaphore getScrollBatchSemaphore() {
262        return scrollBatchSemaphore;
263    }
264
265    /*------------------------- For test purpose ----------------------------------*/
266    public Map<String, FileSystemItemFactoryDescriptor> getFileSystemItemFactoryDescriptors() {
267        return fileSystemItemFactoryRegistry.factoryDescriptors;
268    }
269
270    public List<FileSystemItemFactoryWrapper> getFileSystemItemFactories() {
271        return fileSystemItemFactories;
272    }
273
274    public FileSystemItemFactory getFileSystemItemFactory(String name) {
275        for (FileSystemItemFactoryWrapper factoryWrapper : fileSystemItemFactories) {
276            FileSystemItemFactory factory = factoryWrapper.getFactory();
277            if (name.equals(factory.getName())) {
278                return factory;
279            }
280        }
281        if (log.isDebugEnabled()) {
282            log.debug(String.format("No fileSystemItemFactory named %s, returning null.", name));
283        }
284        return null;
285    }
286
287    /**
288     * @deprecated since 9.3 this is method is not needed anymore with hot reload and standby strategy, but kept due to
289     *             some issues in operation NuxeoDriveSetActiveFactories which freeze Jetty in unit tests when wanting
290     *             to use standby strategy
291     */
292    @Deprecated
293    public void setActiveFactories() {
294        topLevelFolderItemFactory = topLevelFolderItemFactoryRegistry.getActiveFactory(
295                activeTopLevelFolderItemFactoryRegistry.activeFactory);
296        fileSystemItemFactories = fileSystemItemFactoryRegistry.getOrderedActiveFactories(
297                activeFileSystemItemFactoryRegistry.activeFactories);
298    }
299
300    /*--------------------------- Protected ---------------------------------------*/
301    /**
302     * Tries to adapt the given document as the top level {@link FolderItem}. If it doesn't match, iterates on the
303     * ordered contributed file system item factories until it finds one that matches and retrieves a non null
304     * {@link FileSystemItem} for the given document. A file system item factory matches if:
305     * <ul>
306     * <li>It is not bound to any docType nor facet (this is the case for the default factory contribution
307     * {@code defaultFileSystemItemFactory} bound to {@link DefaultFileSystemItemFactory})</li>
308     * <li>It is bound to a docType that matches the given doc's type</li>
309     * <li>It is bound to a facet that matches one of the given doc's facets</li>
310     * </ul>
311     */
312    protected FileSystemItem getFileSystemItem(DocumentModel doc, boolean forceParentItem, FolderItem parentItem,
313            boolean includeDeleted, boolean relaxSyncRootConstraint, boolean getLockInfo) {
314
315        FileSystemItem fileSystemItem = null;
316
317        // Try the topLevelFolderItemFactory
318        if (forceParentItem) {
319            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, parentItem, includeDeleted,
320                    relaxSyncRootConstraint, getLockInfo);
321        } else {
322            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, includeDeleted,
323                    relaxSyncRootConstraint, getLockInfo);
324        }
325        if (fileSystemItem != null) {
326            return fileSystemItem;
327        } else {
328            if (log.isDebugEnabled()) {
329                log.debug(String.format(
330                        "The topLevelFolderItemFactory is not able to adapt document %s as a FileSystemItem => trying fileSystemItemFactories.",
331                        doc.getId()));
332            }
333        }
334
335        // Try the fileSystemItemFactories
336        FileSystemItemFactoryWrapper matchingFactory = null;
337        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
338        while (factoriesIt.hasNext()) {
339            FileSystemItemFactoryWrapper factory = factoriesIt.next();
340            if (log.isDebugEnabled()) {
341                log.debug(String.format("Trying to adapt document %s (path: %s) as a FileSystemItem with factory %s",
342                        doc.getId(), doc.getPathAsString(), factory.getFactory().getName()));
343            }
344            if (generalFactoryMatches(factory) || docTypeFactoryMatches(factory, doc)
345                    || facetFactoryMatches(factory, doc, relaxSyncRootConstraint)) {
346                matchingFactory = factory;
347                try {
348                    if (forceParentItem) {
349                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, parentItem, includeDeleted,
350                                relaxSyncRootConstraint, getLockInfo);
351                    } else {
352                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, includeDeleted,
353                                relaxSyncRootConstraint, getLockInfo);
354                    }
355                } catch (RootlessItemException e) {
356                    // Give more information in the exception message on the
357                    // document whose adaption failed to recursively find the
358                    // top level item.
359                    throw new RootlessItemException(String.format(
360                            "Cannot find path to registered top" + " level when adapting document "
361                                    + " '%s' (path: %s) with factory %s",
362                            doc.getTitle(), doc.getPathAsString(), factory.getFactory().getName()), e);
363                }
364                if (fileSystemItem != null) {
365                    if (log.isDebugEnabled()) {
366                        log.debug(String.format("Adapted document '%s' (path: %s) to item with path %s with factory %s",
367                                doc.getTitle(), doc.getPathAsString(), fileSystemItem.getPath(),
368                                factory.getFactory().getName()));
369                    }
370                    return fileSystemItem;
371                }
372            }
373        }
374
375        if (matchingFactory == null) {
376            if (log.isDebugEnabled()) {
377                log.debug(String.format(
378                        "None of the fileSystemItemFactories matches document %s => returning null. Please check the contributions to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"fileSystemItemFactory\">.",
379                        doc.getId()));
380            }
381        } else {
382            if (log.isDebugEnabled()) {
383                log.debug(String.format(
384                        "None of the fileSystemItemFactories matching document %s were able to adapt this document as a FileSystemItem => returning null.",
385                        doc.getId()));
386            }
387        }
388        return fileSystemItem;
389    }
390
391    protected boolean generalFactoryMatches(FileSystemItemFactoryWrapper factory) {
392        boolean matches = StringUtils.isEmpty(factory.getDocType()) && StringUtils.isEmpty(factory.getFacet());
393        if (log.isTraceEnabled() && matches) {
394            log.trace(String.format("General factory %s matches", factory));
395        }
396        return matches;
397    }
398
399    protected boolean docTypeFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc) {
400        boolean matches = !StringUtils.isEmpty(factory.getDocType()) && factory.getDocType().equals(doc.getType());
401        if (log.isTraceEnabled() && matches) {
402            log.trace(String.format("DocType factory %s matches for doc %s (path: %s)", factory, doc.getId(),
403                    doc.getPathAsString()));
404        }
405        return matches;
406    }
407
408    protected boolean facetFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc,
409            boolean relaxSyncRootConstraint) {
410        if (!StringUtils.isEmpty(factory.getFacet())) {
411            for (String docFacet : doc.getFacets()) {
412                if (factory.getFacet().equals(docFacet)) {
413                    // Handle synchronization root case
414                    if (NuxeoDriveManagerImpl.NUXEO_DRIVE_FACET.equals(docFacet)) {
415                        boolean matches = syncRootFactoryMatches(doc, relaxSyncRootConstraint);
416                        if (log.isTraceEnabled() && matches) {
417                            log.trace(String.format("Facet factory %s matches for doc %s (path: %s)", factory,
418                                    doc.getId(), doc.getPathAsString()));
419                        }
420                        return matches;
421                    } else {
422                        if (log.isTraceEnabled()) {
423                            log.trace(String.format("Facet factory %s matches for doc %s (path: %s)", factory,
424                                    doc.getId(), doc.getPathAsString()));
425                        }
426                        return true;
427                    }
428                }
429            }
430        }
431        return false;
432    }
433
434    @SuppressWarnings("unchecked")
435    protected boolean syncRootFactoryMatches(DocumentModel doc, boolean relaxSyncRootConstraint) {
436        String userName = doc.getCoreSession().getPrincipal().getName();
437        List<Map<String, Object>> subscriptions = (List<Map<String, Object>>) doc.getPropertyValue(
438                NuxeoDriveManagerImpl.DRIVE_SUBSCRIPTIONS_PROPERTY);
439        for (Map<String, Object> subscription : subscriptions) {
440            if (Boolean.TRUE.equals(subscription.get("enabled"))) {
441                if (userName.equals(subscription.get("username"))) {
442                    if (log.isTraceEnabled()) {
443                        log.trace(String.format("Doc %s (path: %s) registered as a sync root for user %s", doc.getId(),
444                                doc.getPathAsString(), userName));
445                    }
446                    return true;
447                }
448                if (relaxSyncRootConstraint) {
449                    if (log.isTraceEnabled()) {
450                        log.trace(String.format(
451                                "Doc %s (path: %s) registered as a sync root for at least one user (relaxSyncRootConstraint is true)",
452                                doc.getId(), doc.getPathAsString()));
453                    }
454                    return true;
455                }
456            }
457        }
458        return false;
459    }
460
461}