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