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((ActiveTopLevelFolderItemFactoryDescriptor) contribution);
089            } else if (contribution instanceof ActiveFileSystemItemFactoriesDescriptor) {
090                activeFileSystemItemFactoryRegistry.addContribution((ActiveFileSystemItemFactoriesDescriptor) contribution);
091            }
092        } else {
093            log.error("Unknown extension point " + extensionPoint);
094        }
095    }
096
097    @Override
098    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
099        if (FILE_SYSTEM_ITEM_FACTORY_EP.equals(extensionPoint)) {
100            fileSystemItemFactoryRegistry.removeContribution((FileSystemItemFactoryDescriptor) contribution);
101        } else if (TOP_LEVEL_FOLDER_ITEM_FACTORY_EP.equals(extensionPoint)) {
102            topLevelFolderItemFactoryRegistry.removeContribution((TopLevelFolderItemFactoryDescriptor) contribution);
103        } else if (ACTIVE_FILE_SYSTEM_ITEM_FACTORIES_EP.equals(extensionPoint)) {
104            if (contribution instanceof ActiveTopLevelFolderItemFactoryDescriptor) {
105                activeTopLevelFolderItemFactoryRegistry.removeContribution((ActiveTopLevelFolderItemFactoryDescriptor) contribution);
106            } else if (contribution instanceof ActiveFileSystemItemFactoriesDescriptor) {
107                activeFileSystemItemFactoryRegistry.removeContribution((ActiveFileSystemItemFactoriesDescriptor) contribution);
108            }
109        } else {
110            log.error("Unknown extension point " + extensionPoint);
111        }
112    }
113
114    @Override
115    public void activate(ComponentContext context) {
116        fileSystemItemFactoryRegistry = new FileSystemItemFactoryRegistry();
117        topLevelFolderItemFactoryRegistry = new TopLevelFolderItemFactoryRegistry();
118        activeTopLevelFolderItemFactoryRegistry = new ActiveTopLevelFolderItemFactoryRegistry();
119        activeFileSystemItemFactoryRegistry = new ActiveFileSystemItemFactoryRegistry();
120        fileSystemItemFactories = new ArrayList<FileSystemItemFactoryWrapper>();
121    }
122
123    @Override
124    public void deactivate(ComponentContext context) {
125        super.deactivate(context);
126        fileSystemItemFactoryRegistry = null;
127        topLevelFolderItemFactoryRegistry = null;
128        activeTopLevelFolderItemFactoryRegistry = null;
129        activeFileSystemItemFactoryRegistry = null;
130        fileSystemItemFactories = null;
131    }
132
133    /**
134     * Sorts the contributed factories according to their order and initializes the {@link #scrollBatchSemaphore}.
135     */
136    @Override
137    public void applicationStarted(ComponentContext context) {
138        setActiveFactories();
139        int concurrentScrollBatchLimit = Integer.parseInt(Framework.getService(ConfigurationService.class).getProperty(
140                CONCURRENT_SCROLL_BATCH_LIMIT, CONCURRENT_SCROLL_BATCH_LIMIT_DEFAULT));
141        scrollBatchSemaphore = new Semaphore(concurrentScrollBatchLimit, false);
142    }
143
144    /*------------------------ FileSystemItemAdapterService -----------------------*/
145    @Override
146    public FileSystemItem getFileSystemItem(DocumentModel doc) {
147        return getFileSystemItem(doc, false, null, false, false, true);
148    }
149
150    @Override
151    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted) {
152        return getFileSystemItem(doc, false, null, includeDeleted, false, true);
153    }
154
155    @Override
156    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted, boolean relaxSyncRootConstraint) {
157        return getFileSystemItem(doc, false, null, includeDeleted, relaxSyncRootConstraint, true);
158    }
159
160    @Override
161    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted, boolean relaxSyncRootConstraint,
162            boolean getLockInfo) {
163        return getFileSystemItem(doc, false, null, includeDeleted, relaxSyncRootConstraint, getLockInfo);
164    }
165
166    @Override
167    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem) {
168        return getFileSystemItem(doc, true, parentItem, false, false, true);
169    }
170
171    @Override
172    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted) {
173        return getFileSystemItem(doc, true, parentItem, includeDeleted, false, true);
174    }
175
176    @Override
177    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted,
178            boolean relaxSyncRootConstraint) {
179        return getFileSystemItem(doc, true, parentItem, includeDeleted, relaxSyncRootConstraint, true);
180    }
181
182    @Override
183    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted,
184            boolean relaxSyncRootConstraint, boolean getLockInfo) {
185        return getFileSystemItem(doc, true, parentItem, includeDeleted, relaxSyncRootConstraint, getLockInfo);
186    }
187
188    /**
189     * Iterates on the ordered contributed file system item factories until if finds one that can handle the given
190     * {@link FileSystemItem} id.
191     */
192    @Override
193    public FileSystemItemFactory getFileSystemItemFactoryForId(String id) {
194        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
195        while (factoriesIt.hasNext()) {
196            FileSystemItemFactoryWrapper factoryWrapper = factoriesIt.next();
197            FileSystemItemFactory factory = factoryWrapper.getFactory();
198            if (factory.canHandleFileSystemItemId(id)) {
199                return factory;
200            }
201        }
202        // No fileSystemItemFactory found, try the topLevelFolderItemFactory
203        if (getTopLevelFolderItemFactory().canHandleFileSystemItemId(id)) {
204            return getTopLevelFolderItemFactory();
205        }
206        throw new NuxeoDriveContribException(
207                String.format(
208                        "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.",
209                        id));
210    }
211
212    @Override
213    public TopLevelFolderItemFactory getTopLevelFolderItemFactory() {
214        if (topLevelFolderItemFactory == null) {
215            throw new NuxeoDriveContribException(
216                    "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\">.");
217        }
218        return topLevelFolderItemFactory;
219    }
220
221    @Override
222    public VirtualFolderItemFactory getVirtualFolderItemFactory(String factoryName) {
223        FileSystemItemFactory factory = getFileSystemItemFactory(factoryName);
224        if (factory == null) {
225            throw new NuxeoDriveContribException(
226                    String.format(
227                            "No factory named %s. Please check the contributions to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"fileSystemItemFactory\">.",
228                            factoryName));
229        }
230        if (!(factory instanceof VirtualFolderItemFactory)) {
231            throw new NuxeoDriveContribException(String.format(
232                    "Factory class %s for factory %s is not a VirtualFolderItemFactory.", factory.getClass().getName(),
233                    factory.getName()));
234        }
235        return (VirtualFolderItemFactory) factory;
236    }
237
238    @Override
239    public Set<String> getActiveFileSystemItemFactories() {
240        if (activeFileSystemItemFactoryRegistry.activeFactories.isEmpty()) {
241            throw new NuxeoDriveContribException(
242                    "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.");
243        }
244        return activeFileSystemItemFactoryRegistry.activeFactories;
245    }
246
247    @Override
248    public Semaphore getScrollBatchSemaphore() {
249        return scrollBatchSemaphore;
250    }
251
252    /*------------------------- For test purpose ----------------------------------*/
253    public Map<String, FileSystemItemFactoryDescriptor> getFileSystemItemFactoryDescriptors() {
254        return fileSystemItemFactoryRegistry.factoryDescriptors;
255    }
256
257    public List<FileSystemItemFactoryWrapper> getFileSystemItemFactories() {
258        return fileSystemItemFactories;
259    }
260
261    public FileSystemItemFactory getFileSystemItemFactory(String name) {
262        for (FileSystemItemFactoryWrapper factoryWrapper : fileSystemItemFactories) {
263            FileSystemItemFactory factory = factoryWrapper.getFactory();
264            if (name.equals(factory.getName())) {
265                return factory;
266            }
267        }
268        if (log.isDebugEnabled()) {
269            log.debug(String.format("No fileSystemItemFactory named %s, returning null.", name));
270        }
271        return null;
272    }
273
274    public void setActiveFactories() {
275        topLevelFolderItemFactory = topLevelFolderItemFactoryRegistry.getActiveFactory(activeTopLevelFolderItemFactoryRegistry.activeFactory);
276        fileSystemItemFactories = fileSystemItemFactoryRegistry.getOrderedActiveFactories(activeFileSystemItemFactoryRegistry.activeFactories);
277    }
278
279    /*--------------------------- Protected ---------------------------------------*/
280    /**
281     * Tries to adapt the given document as the top level {@link FolderItem}. If it doesn't match, iterates on the
282     * ordered contributed file system item factories until it finds one that matches and retrieves a non null
283     * {@link FileSystemItem} for the given document. A file system item factory matches if:
284     * <ul>
285     * <li>It is not bound to any docType nor facet (this is the case for the default factory contribution
286     * {@code defaultFileSystemItemFactory} bound to {@link DefaultFileSystemItemFactory})</li>
287     * <li>It is bound to a docType that matches the given doc's type</li>
288     * <li>It is bound to a facet that matches one of the given doc's facets</li>
289     * </ul>
290     */
291    protected FileSystemItem getFileSystemItem(DocumentModel doc, boolean forceParentItem, FolderItem parentItem,
292            boolean includeDeleted, boolean relaxSyncRootConstraint, boolean getLockInfo) {
293
294        FileSystemItem fileSystemItem = null;
295
296        // Try the topLevelFolderItemFactory
297        if (forceParentItem) {
298            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, parentItem, includeDeleted,
299                    relaxSyncRootConstraint, getLockInfo);
300        } else {
301            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, includeDeleted,
302                    relaxSyncRootConstraint, getLockInfo);
303        }
304        if (fileSystemItem != null) {
305            return fileSystemItem;
306        } else {
307            if (log.isDebugEnabled()) {
308                log.debug(String.format(
309                        "The topLevelFolderItemFactory is not able to adapt document %s as a FileSystemItem => trying fileSystemItemFactories.",
310                        doc.getId()));
311            }
312        }
313
314        // Try the fileSystemItemFactories
315        FileSystemItemFactoryWrapper matchingFactory = null;
316        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
317        while (factoriesIt.hasNext()) {
318            FileSystemItemFactoryWrapper factory = factoriesIt.next();
319            if (log.isDebugEnabled()) {
320                log.debug(String.format("Trying to adapt document %s (path: %s) as a FileSystemItem with factory %s",
321                        doc.getId(), doc.getPathAsString(), factory.getFactory().getName()));
322            }
323            if (generalFactoryMatches(factory) || docTypeFactoryMatches(factory, doc)
324                    || facetFactoryMatches(factory, doc, relaxSyncRootConstraint)) {
325                matchingFactory = factory;
326                try {
327                    if (forceParentItem) {
328                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, parentItem, includeDeleted,
329                                relaxSyncRootConstraint, getLockInfo);
330                    } else {
331                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, includeDeleted,
332                                relaxSyncRootConstraint, getLockInfo);
333                    }
334                } catch (RootlessItemException e) {
335                    // Give more information in the exception message on the
336                    // document whose adaption failed to recursively find the
337                    // top level item.
338                    throw new RootlessItemException(String.format("Cannot find path to registered top"
339                            + " level when adapting document " + " '%s' (path: %s) with factory %s", doc.getTitle(),
340                            doc.getPathAsString(), factory.getFactory().getName()), e);
341                }
342                if (fileSystemItem != null) {
343                    if (log.isDebugEnabled()) {
344                        log.debug(String.format(
345                                "Adapted document '%s' (path: %s) to item with path %s with factory %s",
346                                doc.getTitle(), doc.getPathAsString(), fileSystemItem.getPath(), factory.getFactory()
347                                                                                                        .getName()));
348                    }
349                    return fileSystemItem;
350                }
351            }
352        }
353
354        if (matchingFactory == null) {
355            if (log.isDebugEnabled()) {
356                log.debug(String.format(
357                        "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\">.",
358                        doc.getId()));
359            }
360        } else {
361            if (log.isDebugEnabled()) {
362                log.debug(String.format(
363                        "None of the fileSystemItemFactories matching document %s were able to adapt this document as a FileSystemItem => returning null.",
364                        doc.getId()));
365            }
366        }
367        return fileSystemItem;
368    }
369
370    protected boolean generalFactoryMatches(FileSystemItemFactoryWrapper factory) {
371        boolean matches = StringUtils.isEmpty(factory.getDocType()) && StringUtils.isEmpty(factory.getFacet());
372        if (log.isTraceEnabled() && matches) {
373            log.trace(String.format("General factory %s matches", factory));
374        }
375        return matches;
376    }
377
378    protected boolean docTypeFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc) {
379        boolean matches = !StringUtils.isEmpty(factory.getDocType()) && factory.getDocType().equals(doc.getType());
380        if (log.isTraceEnabled() && matches) {
381            log.trace(String.format("DocType factory %s matches for doc %s (path: %s)", factory, doc.getId(),
382                    doc.getPathAsString()));
383        }
384        return matches;
385    }
386
387    protected boolean facetFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc,
388            boolean relaxSyncRootConstraint) {
389        if (!StringUtils.isEmpty(factory.getFacet())) {
390            for (String docFacet : doc.getFacets()) {
391                if (factory.getFacet().equals(docFacet)) {
392                    // Handle synchronization root case
393                    if (NuxeoDriveManagerImpl.NUXEO_DRIVE_FACET.equals(docFacet)) {
394                        boolean matches = syncRootFactoryMatches(doc, relaxSyncRootConstraint);
395                        if (log.isTraceEnabled() && matches) {
396                            log.trace(String.format("Facet factory %s matches for doc %s (path: %s)", factory,
397                                    doc.getId(), doc.getPathAsString()));
398                        }
399                        return matches;
400                    } else {
401                        if (log.isTraceEnabled()) {
402                            log.trace(String.format("Facet factory %s matches for doc %s (path: %s)", factory,
403                                    doc.getId(), doc.getPathAsString()));
404                        }
405                        return true;
406                    }
407                }
408            }
409        }
410        return false;
411    }
412
413    @SuppressWarnings("unchecked")
414    protected boolean syncRootFactoryMatches(DocumentModel doc, boolean relaxSyncRootConstraint) {
415        String userName = doc.getCoreSession().getPrincipal().getName();
416        List<Map<String, Object>> subscriptions = (List<Map<String, Object>>) doc.getPropertyValue(NuxeoDriveManagerImpl.DRIVE_SUBSCRIPTIONS_PROPERTY);
417        for (Map<String, Object> subscription : subscriptions) {
418            if (Boolean.TRUE.equals(subscription.get("enabled"))
419                    && (userName.equals(subscription.get("username")) || relaxSyncRootConstraint)) {
420                if (log.isTraceEnabled()) {
421                    log.trace(String.format("Doc %s (path: %s) registered as a sync root for user %s", doc.getId(),
422                            doc.getPathAsString(), userName));
423                }
424                return true;
425            }
426        }
427        return false;
428    }
429
430}