001/*
002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Antoine Taillefer <ataillefer@nuxeo.com>
016 */
017package org.nuxeo.drive.service.impl;
018
019import java.util.ArrayList;
020import java.util.Iterator;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.commons.lang.StringUtils;
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.nuxeo.drive.adapter.FileSystemItem;
029import org.nuxeo.drive.adapter.FolderItem;
030import org.nuxeo.drive.adapter.NuxeoDriveContribException;
031import org.nuxeo.drive.adapter.RootlessItemException;
032import org.nuxeo.drive.service.FileSystemItemAdapterService;
033import org.nuxeo.drive.service.FileSystemItemFactory;
034import org.nuxeo.drive.service.TopLevelFolderItemFactory;
035import org.nuxeo.drive.service.VirtualFolderItemFactory;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.runtime.model.ComponentContext;
038import org.nuxeo.runtime.model.ComponentInstance;
039import org.nuxeo.runtime.model.DefaultComponent;
040
041/**
042 * Default implementation of the {@link FileSystemItemAdapterService}.
043 *
044 * @author Antoine Taillefer
045 */
046public class FileSystemItemAdapterServiceImpl extends DefaultComponent implements FileSystemItemAdapterService {
047
048    private static final Log log = LogFactory.getLog(FileSystemItemAdapterServiceImpl.class);
049
050    public static final String FILE_SYSTEM_ITEM_FACTORY_EP = "fileSystemItemFactory";
051
052    public static final String TOP_LEVEL_FOLDER_ITEM_FACTORY_EP = "topLevelFolderItemFactory";
053
054    public static final String ACTIVE_FILE_SYSTEM_ITEM_FACTORIES_EP = "activeFileSystemItemFactories";
055
056    protected TopLevelFolderItemFactoryRegistry topLevelFolderItemFactoryRegistry;
057
058    protected FileSystemItemFactoryRegistry fileSystemItemFactoryRegistry;
059
060    protected ActiveTopLevelFolderItemFactoryRegistry activeTopLevelFolderItemFactoryRegistry;
061
062    protected ActiveFileSystemItemFactoryRegistry activeFileSystemItemFactoryRegistry;
063
064    protected TopLevelFolderItemFactory topLevelFolderItemFactory;
065
066    protected List<FileSystemItemFactoryWrapper> fileSystemItemFactories;
067
068    /*------------------------ DefaultComponent -----------------------------*/
069    @Override
070    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
071        if (FILE_SYSTEM_ITEM_FACTORY_EP.equals(extensionPoint)) {
072            fileSystemItemFactoryRegistry.addContribution((FileSystemItemFactoryDescriptor) contribution);
073        } else if (TOP_LEVEL_FOLDER_ITEM_FACTORY_EP.equals(extensionPoint)) {
074            topLevelFolderItemFactoryRegistry.addContribution((TopLevelFolderItemFactoryDescriptor) contribution);
075        } else if (ACTIVE_FILE_SYSTEM_ITEM_FACTORIES_EP.equals(extensionPoint)) {
076            if (contribution instanceof ActiveTopLevelFolderItemFactoryDescriptor) {
077                activeTopLevelFolderItemFactoryRegistry.addContribution((ActiveTopLevelFolderItemFactoryDescriptor) contribution);
078            } else if (contribution instanceof ActiveFileSystemItemFactoriesDescriptor) {
079                activeFileSystemItemFactoryRegistry.addContribution((ActiveFileSystemItemFactoriesDescriptor) contribution);
080            }
081        } else {
082            log.error("Unknown extension point " + extensionPoint);
083        }
084    }
085
086    @Override
087    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
088        if (FILE_SYSTEM_ITEM_FACTORY_EP.equals(extensionPoint)) {
089            fileSystemItemFactoryRegistry.removeContribution((FileSystemItemFactoryDescriptor) contribution);
090        } else if (TOP_LEVEL_FOLDER_ITEM_FACTORY_EP.equals(extensionPoint)) {
091            topLevelFolderItemFactoryRegistry.removeContribution((TopLevelFolderItemFactoryDescriptor) contribution);
092        } else if (ACTIVE_FILE_SYSTEM_ITEM_FACTORIES_EP.equals(extensionPoint)) {
093            if (contribution instanceof ActiveTopLevelFolderItemFactoryDescriptor) {
094                activeTopLevelFolderItemFactoryRegistry.removeContribution((ActiveTopLevelFolderItemFactoryDescriptor) contribution);
095            } else if (contribution instanceof ActiveFileSystemItemFactoriesDescriptor) {
096                activeFileSystemItemFactoryRegistry.removeContribution((ActiveFileSystemItemFactoriesDescriptor) contribution);
097            }
098        } else {
099            log.error("Unknown extension point " + extensionPoint);
100        }
101    }
102
103    @Override
104    public void activate(ComponentContext context) {
105        fileSystemItemFactoryRegistry = new FileSystemItemFactoryRegistry();
106        topLevelFolderItemFactoryRegistry = new TopLevelFolderItemFactoryRegistry();
107        activeTopLevelFolderItemFactoryRegistry = new ActiveTopLevelFolderItemFactoryRegistry();
108        activeFileSystemItemFactoryRegistry = new ActiveFileSystemItemFactoryRegistry();
109        fileSystemItemFactories = new ArrayList<FileSystemItemFactoryWrapper>();
110    }
111
112    @Override
113    public void deactivate(ComponentContext context) {
114        super.deactivate(context);
115        fileSystemItemFactoryRegistry = null;
116        topLevelFolderItemFactoryRegistry = null;
117        activeTopLevelFolderItemFactoryRegistry = null;
118        activeFileSystemItemFactoryRegistry = null;
119        fileSystemItemFactories = null;
120    }
121
122    /**
123     * Sorts the contributed factories according to their order.
124     */
125    @Override
126    public void applicationStarted(ComponentContext context) {
127        setActiveFactories();
128    }
129
130    /*------------------------ FileSystemItemAdapterService -----------------------*/
131    @Override
132    public FileSystemItem getFileSystemItem(DocumentModel doc) {
133        return getFileSystemItem(doc, false, null, false, false);
134    }
135
136    @Override
137    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted) {
138        return getFileSystemItem(doc, false, null, includeDeleted, false);
139    }
140
141    @Override
142    public FileSystemItem getFileSystemItem(DocumentModel doc, boolean includeDeleted, boolean relaxSyncRootConstraint) {
143        return getFileSystemItem(doc, false, null, includeDeleted, relaxSyncRootConstraint);
144    }
145
146    @Override
147    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem) {
148        return getFileSystemItem(doc, true, parentItem, false, false);
149    }
150
151    @Override
152    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted) {
153        return getFileSystemItem(doc, true, parentItem, includeDeleted, false);
154    }
155
156    @Override
157    public FileSystemItem getFileSystemItem(DocumentModel doc, FolderItem parentItem, boolean includeDeleted,
158            boolean relaxSyncRootConstraint) {
159        return getFileSystemItem(doc, true, parentItem, includeDeleted, relaxSyncRootConstraint);
160    }
161
162    /**
163     * Iterates on the ordered contributed file system item factories until if finds one that can handle the given
164     * {@link FileSystemItem} id.
165     */
166    @Override
167    public FileSystemItemFactory getFileSystemItemFactoryForId(String id) {
168        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
169        while (factoriesIt.hasNext()) {
170            FileSystemItemFactoryWrapper factoryWrapper = factoriesIt.next();
171            FileSystemItemFactory factory = factoryWrapper.getFactory();
172            if (factory.canHandleFileSystemItemId(id)) {
173                return factory;
174            }
175        }
176        // No fileSystemItemFactory found, try the topLevelFolderItemFactory
177        if (getTopLevelFolderItemFactory().canHandleFileSystemItemId(id)) {
178            return getTopLevelFolderItemFactory();
179        }
180        throw new NuxeoDriveContribException(
181                String.format(
182                        "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.",
183                        id));
184    }
185
186    @Override
187    public TopLevelFolderItemFactory getTopLevelFolderItemFactory() {
188        if (topLevelFolderItemFactory == null) {
189            throw new NuxeoDriveContribException(
190                    "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\">.");
191        }
192        return topLevelFolderItemFactory;
193    }
194
195    @Override
196    public VirtualFolderItemFactory getVirtualFolderItemFactory(String factoryName) {
197        FileSystemItemFactory factory = getFileSystemItemFactory(factoryName);
198        if (factory == null) {
199            throw new NuxeoDriveContribException(
200                    String.format(
201                            "No factory named %s. Please check the contributions to the following extension point: <extension target=\"org.nuxeo.drive.service.FileSystemItemAdapterService\" point=\"fileSystemItemFactory\">.",
202                            factoryName));
203        }
204        if (!(factory instanceof VirtualFolderItemFactory)) {
205            throw new NuxeoDriveContribException(String.format(
206                    "Factory class %s for factory %s is not a VirtualFolderItemFactory.", factory.getClass().getName(),
207                    factory.getName()));
208        }
209        return (VirtualFolderItemFactory) factory;
210    }
211
212    @Override
213    public Set<String> getActiveFileSystemItemFactories() {
214        if (activeFileSystemItemFactoryRegistry.activeFactories.isEmpty()) {
215            throw new NuxeoDriveContribException(
216                    "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.");
217        }
218        return activeFileSystemItemFactoryRegistry.activeFactories;
219    }
220
221    /*------------------------- For test purpose ----------------------------------*/
222    public Map<String, FileSystemItemFactoryDescriptor> getFileSystemItemFactoryDescriptors() {
223        return fileSystemItemFactoryRegistry.factoryDescriptors;
224    }
225
226    public List<FileSystemItemFactoryWrapper> getFileSystemItemFactories() {
227        return fileSystemItemFactories;
228    }
229
230    public FileSystemItemFactory getFileSystemItemFactory(String name) {
231        for (FileSystemItemFactoryWrapper factoryWrapper : fileSystemItemFactories) {
232            FileSystemItemFactory factory = factoryWrapper.getFactory();
233            if (name.equals(factory.getName())) {
234                return factory;
235            }
236        }
237        if (log.isDebugEnabled()) {
238            log.debug(String.format("No fileSystemItemFactory named %s, returning null.", name));
239        }
240        return null;
241    }
242
243    /*--------------------------- Protected ---------------------------------------*/
244    protected void setActiveFactories() {
245        topLevelFolderItemFactory = topLevelFolderItemFactoryRegistry.getActiveFactory(activeTopLevelFolderItemFactoryRegistry.activeFactory);
246        fileSystemItemFactories = fileSystemItemFactoryRegistry.getOrderedActiveFactories(activeFileSystemItemFactoryRegistry.activeFactories);
247    }
248
249    /**
250     * Tries to adapt the given document as the top level {@link FolderItem}. If it doesn't match, iterates on the
251     * ordered contributed file system item factories until it finds one that matches and retrieves a non null
252     * {@link FileSystemItem} for the given document. A file system item factory matches if:
253     * <ul>
254     * <li>It is not bound to any docType nor facet (this is the case for the default factory contribution
255     * {@code defaultFileSystemItemFactory} bound to {@link DefaultFileSystemItemFactory})</li>
256     * <li>It is bound to a docType that matches the given doc's type</li>
257     * <li>It is bound to a facet that matches one of the given doc's facets</li>
258     * </ul>
259     */
260    protected FileSystemItem getFileSystemItem(DocumentModel doc, boolean forceParentItem, FolderItem parentItem,
261            boolean includeDeleted, boolean relaxSyncRootConstraint) {
262
263        FileSystemItem fileSystemItem = null;
264
265        // Try the topLevelFolderItemFactory
266        if (forceParentItem) {
267            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, parentItem, includeDeleted);
268        } else {
269            fileSystemItem = getTopLevelFolderItemFactory().getFileSystemItem(doc, includeDeleted);
270        }
271        if (fileSystemItem != null) {
272            return fileSystemItem;
273        } else {
274            if (log.isDebugEnabled()) {
275                log.debug(String.format(
276                        "The topLevelFolderItemFactory is not able to adapt document %s as a FileSystemItem => trying fileSystemItemFactories.",
277                        doc.getId()));
278            }
279        }
280
281        // Try the fileSystemItemFactories
282        FileSystemItemFactoryWrapper matchingFactory = null;
283        Iterator<FileSystemItemFactoryWrapper> factoriesIt = fileSystemItemFactories.iterator();
284        while (factoriesIt.hasNext()) {
285            FileSystemItemFactoryWrapper factory = factoriesIt.next();
286            if (log.isDebugEnabled()) {
287                log.debug(String.format("Trying to adapt document %s (path: %s) as a FileSystemItem with factory %s",
288                        doc.getId(), doc.getPathAsString(), factory.getFactory().getName()));
289            }
290            if (generalFactoryMatches(factory) || docTypeFactoryMatches(factory, doc)
291                    || facetFactoryMatches(factory, doc, relaxSyncRootConstraint)) {
292                matchingFactory = factory;
293                try {
294                    if (forceParentItem) {
295                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, parentItem, includeDeleted,
296                                relaxSyncRootConstraint);
297                    } else {
298                        fileSystemItem = factory.getFactory().getFileSystemItem(doc, includeDeleted,
299                                relaxSyncRootConstraint);
300                    }
301                } catch (RootlessItemException e) {
302                    // Give more information in the exception message on the
303                    // document whose adaption failed to recursively find the
304                    // top level item.
305                    throw new RootlessItemException(String.format("Cannot find path to registered top"
306                            + " level when adapting document " + " '%s' (path: %s) with factory %s", doc.getTitle(),
307                            doc.getPathAsString(), factory.getFactory().getName()), e);
308                }
309                if (fileSystemItem != null) {
310                    if (log.isDebugEnabled()) {
311                        log.debug(String.format(
312                                "Adapted document '%s' (path: %s) to item with path %s with factory %s",
313                                doc.getTitle(), doc.getPathAsString(), fileSystemItem.getPath(),
314                                factory.getFactory().getName()));
315                    }
316                    return fileSystemItem;
317                }
318            }
319        }
320
321        if (matchingFactory == null) {
322            if (log.isDebugEnabled()) {
323                log.debug(String.format(
324                        "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\">.",
325                        doc.getId()));
326            }
327        } else {
328            if (log.isDebugEnabled()) {
329                log.debug(String.format(
330                        "None of the fileSystemItemFactories matching document %s were able to adapt this document as a FileSystemItem => returning null.",
331                        doc.getId()));
332            }
333        }
334        return fileSystemItem;
335    }
336
337    protected boolean generalFactoryMatches(FileSystemItemFactoryWrapper factory) {
338        boolean matches = StringUtils.isEmpty(factory.getDocType()) && StringUtils.isEmpty(factory.getFacet());
339        if (log.isTraceEnabled() && matches) {
340            log.trace(String.format("General factory %s matches", factory));
341        }
342        return matches;
343    }
344
345    protected boolean docTypeFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc) {
346        boolean matches = !StringUtils.isEmpty(factory.getDocType()) && factory.getDocType().equals(doc.getType());
347        if (log.isTraceEnabled() && matches) {
348            log.trace(String.format("DocType factory %s matches for doc %s (path: %s)", factory, doc.getId(),
349                    doc.getPathAsString()));
350        }
351        return matches;
352    }
353
354    protected boolean facetFactoryMatches(FileSystemItemFactoryWrapper factory, DocumentModel doc,
355            boolean relaxSyncRootConstraint) {
356        if (!StringUtils.isEmpty(factory.getFacet())) {
357            for (String docFacet : doc.getFacets()) {
358                if (factory.getFacet().equals(docFacet)) {
359                    // Handle synchronization root case
360                    if (NuxeoDriveManagerImpl.NUXEO_DRIVE_FACET.equals(docFacet)) {
361                        boolean matches = syncRootFactoryMatches(doc, relaxSyncRootConstraint);
362                        if (log.isTraceEnabled() && matches) {
363                            log.trace(String.format("Facet factory %s matches for doc %s (path: %s)", factory,
364                                    doc.getId(), doc.getPathAsString()));
365                        }
366                        return matches;
367                    } else {
368                        if (log.isTraceEnabled()) {
369                            log.trace(String.format("Facet factory %s matches for doc %s (path: %s)", factory,
370                                    doc.getId(), doc.getPathAsString()));
371                        }
372                        return true;
373                    }
374                }
375            }
376        }
377        return false;
378    }
379
380    @SuppressWarnings("unchecked")
381    protected boolean syncRootFactoryMatches(DocumentModel doc, boolean relaxSyncRootConstraint) {
382        String userName = doc.getCoreSession().getPrincipal().getName();
383        List<Map<String, Object>> subscriptions = (List<Map<String, Object>>) doc.getPropertyValue(NuxeoDriveManagerImpl.DRIVE_SUBSCRIPTIONS_PROPERTY);
384        for (Map<String, Object> subscription : subscriptions) {
385            if (Boolean.TRUE.equals(subscription.get("enabled"))
386                    && (userName.equals(subscription.get("username")) || relaxSyncRootConstraint)) {
387                if (log.isTraceEnabled()) {
388                    log.trace(String.format("Doc %s (path: %s) registered as a sync root for user %s", doc.getId(),
389                            doc.getPathAsString(), userName));
390                }
391                return true;
392            }
393        }
394        return false;
395    }
396
397}