001/*
002 * (C) Copyright 2002 - 2006 Nuxeo SARL <http://nuxeo.com> and others
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Nuxeo - initial API and implementation
011 *
012 *
013 * $Id: Registry.java 2531 2006-09-04 23:01:57Z janguenot $
014 */
015
016package org.nuxeo.ecm.platform.filemanager.service;
017
018import java.io.IOException;
019import java.security.Principal;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027
028import org.apache.commons.lang.StringUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.ecm.core.api.Blob;
032import org.nuxeo.ecm.core.api.CoreInstance;
033import org.nuxeo.ecm.core.api.CoreSession;
034import org.nuxeo.ecm.core.api.DocumentLocation;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentModelList;
037import org.nuxeo.ecm.core.api.DocumentSecurityException;
038import org.nuxeo.ecm.core.api.PathRef;
039import org.nuxeo.ecm.core.api.VersioningOption;
040import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl;
041import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
042import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
043import org.nuxeo.ecm.core.api.repository.RepositoryManager;
044import org.nuxeo.ecm.core.api.security.SecurityConstants;
045import org.nuxeo.ecm.platform.filemanager.api.FileManager;
046import org.nuxeo.ecm.platform.filemanager.service.extension.CreationContainerListProvider;
047import org.nuxeo.ecm.platform.filemanager.service.extension.CreationContainerListProviderDescriptor;
048import org.nuxeo.ecm.platform.filemanager.service.extension.FileImporter;
049import org.nuxeo.ecm.platform.filemanager.service.extension.FileImporterDescriptor;
050import org.nuxeo.ecm.platform.filemanager.service.extension.FolderImporter;
051import org.nuxeo.ecm.platform.filemanager.service.extension.FolderImporterDescriptor;
052import org.nuxeo.ecm.platform.filemanager.service.extension.UnicityExtension;
053import org.nuxeo.ecm.platform.filemanager.service.extension.VersioningDescriptor;
054import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils;
055import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException;
056import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
057import org.nuxeo.ecm.platform.types.TypeManager;
058import org.nuxeo.runtime.api.Framework;
059import org.nuxeo.runtime.model.ComponentName;
060import org.nuxeo.runtime.model.DefaultComponent;
061import org.nuxeo.runtime.model.Extension;
062
063/**
064 * FileManager registry service.
065 * <p>
066 * This is the component to request to perform transformations. See API.
067 *
068 * @author <a href="mailto:andreas.kalogeropoulos@nuxeo.com">Andreas Kalogeropoulos</a>
069 */
070public class FileManagerService extends DefaultComponent implements FileManager {
071
072    public static final ComponentName NAME = new ComponentName(
073            "org.nuxeo.ecm.platform.filemanager.service.FileManagerService");
074
075    public static final String DEFAULT_FOLDER_TYPE_NAME = "Folder";
076
077    // TODO: OG: we should use an overridable query model instead of hardcoding
078    // the NXQL query
079    public static final String QUERY = "SELECT * FROM Document WHERE file:content/digest = '%s'";
080
081    public static final int MAX = 15;
082
083    private static final Log log = LogFactory.getLog(FileManagerService.class);
084
085    private final Map<String, FileImporter> fileImporters;
086
087    private final List<FolderImporter> folderImporters;
088
089    private final List<CreationContainerListProvider> creationContainerListProviders;
090
091    private List<String> fieldsXPath = new ArrayList<String>();
092
093    private MimetypeRegistry mimeService;
094
095    private boolean unicityEnabled = false;
096
097    private String digestAlgorithm = "sha-256";
098
099    private boolean computeDigest = false;
100
101    public static final VersioningOption DEF_VERSIONING_OPTION = VersioningOption.MINOR;
102
103    public static final boolean DEF_VERSIONING_AFTER_ADD = false;
104
105    /**
106     * @since 5.7
107     */
108    private VersioningOption defaultVersioningOption = DEF_VERSIONING_OPTION;
109
110    /**
111     * @since 5.7
112     */
113    private boolean versioningAfterAdd = DEF_VERSIONING_AFTER_ADD;
114
115    private TypeManager typeService;
116
117    public FileManagerService() {
118        fileImporters = new HashMap<String, FileImporter>();
119        folderImporters = new LinkedList<FolderImporter>();
120        creationContainerListProviders = new LinkedList<CreationContainerListProvider>();
121    }
122
123    private MimetypeRegistry getMimeService() {
124        if (mimeService == null) {
125            mimeService = Framework.getService(MimetypeRegistry.class);
126        }
127        return mimeService;
128    }
129
130    private TypeManager getTypeService() {
131        if (typeService == null) {
132            typeService = Framework.getService(TypeManager.class);
133        }
134        return typeService;
135    }
136
137    private Blob checkMimeType(Blob blob, String fullname) {
138        final String mimeType = blob.getMimeType();
139        if (mimeType != null && !mimeType.isEmpty() && !mimeType.equals("application/octet-stream")
140                && !mimeType.equals("application/octetstream")) {
141            return blob;
142        }
143        String filename = FileManagerUtils.fetchFileName(fullname);
144        blob = getMimeService().updateMimetype(blob, filename);
145        return blob;
146    }
147
148    public DocumentModel createFolder(CoreSession documentManager, String fullname, String path)
149            throws IOException {
150
151        if (folderImporters.isEmpty()) {
152            return defaultCreateFolder(documentManager, fullname, path);
153        } else {
154            // use the last registered folder importer
155            FolderImporter folderImporter = folderImporters.get(folderImporters.size() - 1);
156            return folderImporter.create(documentManager, fullname, path, true, getTypeService());
157        }
158    }
159
160    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path)
161            {
162        return defaultCreateFolder(documentManager, fullname, path, DEFAULT_FOLDER_TYPE_NAME, true);
163    }
164
165    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
166            String containerTypeName, boolean checkAllowedSubTypes) {
167
168        // Fetching filename
169        String title = FileManagerUtils.fetchFileName(fullname);
170
171        // Looking if an existing Folder with the same filename exists.
172        DocumentModel docModel = FileManagerUtils.getExistingDocByTitle(documentManager, path, title);
173
174        if (docModel == null) {
175            // check permissions
176            PathRef containerRef = new PathRef(path);
177            if (!documentManager.hasPermission(containerRef, SecurityConstants.READ_PROPERTIES)
178                    || !documentManager.hasPermission(containerRef, SecurityConstants.ADD_CHILDREN)) {
179                throw new DocumentSecurityException("Not enough rights to create folder");
180            }
181
182            // check allowed sub types
183            DocumentModel container = documentManager.getDocument(containerRef);
184            if (checkAllowedSubTypes
185                    && !getTypeService().isAllowedSubType(containerTypeName, container.getType(), container)) {
186                // cannot create document file here
187                // TODO: we should better raise a dedicated exception to be
188                // catched by the FileManageActionsBean instead of returning
189                // null
190                return null;
191            }
192
193            PathSegmentService pss = Framework.getService(PathSegmentService.class);
194            docModel = documentManager.createDocumentModel(containerTypeName);
195            docModel.setProperty("dublincore", "title", title);
196
197            // writing changes
198            docModel.setPathInfo(path, pss.generatePathSegment(docModel));
199            docModel = documentManager.createDocument(docModel);
200            documentManager.save();
201
202            log.debug("Created container: " + docModel.getName() + " with type " + containerTypeName);
203        }
204        return docModel;
205    }
206
207    public DocumentModel createDocumentFromBlob(CoreSession documentManager, Blob input, String path,
208            boolean overwrite, String fullName) throws IOException {
209
210        // check mime type to be able to select the best importer plugin
211        input = checkMimeType(input, fullName);
212
213        List<FileImporter> importers = new ArrayList<FileImporter>(fileImporters.values());
214        Collections.sort(importers);
215        String normalizedMimeType = getMimeService().getMimetypeEntryByMimeType(input.getMimeType()).getNormalized();
216        for (FileImporter importer : importers) {
217            if (importer.isEnabled() && (importer.matches(normalizedMimeType) || importer.matches(input.getMimeType()))) {
218                DocumentModel doc = importer.create(documentManager, input, path, overwrite, fullName, getTypeService());
219                if (doc != null) {
220                    return doc;
221                }
222            }
223        }
224        return null;
225    }
226
227    public DocumentModel updateDocumentFromBlob(CoreSession documentManager, Blob input, String path, String fullName)
228            {
229        String filename = FileManagerUtils.fetchFileName(fullName);
230        DocumentModel doc = FileManagerUtils.getExistingDocByFileName(documentManager, path, filename);
231        if (doc != null) {
232            doc.setProperty("file", "content", input);
233
234            documentManager.saveDocument(doc);
235            documentManager.save();
236
237            log.debug("Updated the document: " + doc.getName());
238        }
239        return doc;
240    }
241
242    public FileImporter getPluginByName(String name) {
243        return fileImporters.get(name);
244    }
245
246    @Override
247    public void registerExtension(Extension extension) {
248        if (extension.getExtensionPoint().equals("plugins")) {
249            Object[] contribs = extension.getContributions();
250            for (Object contrib : contribs) {
251                if (contrib instanceof FileImporterDescriptor) {
252                    registerFileImporter((FileImporterDescriptor) contrib, extension);
253                } else if (contrib instanceof FolderImporterDescriptor) {
254                    registerFolderImporter((FolderImporterDescriptor) contrib, extension);
255                } else if (contrib instanceof CreationContainerListProviderDescriptor) {
256                    registerCreationContainerListProvider((CreationContainerListProviderDescriptor) contrib, extension);
257                }
258            }
259        } else if (extension.getExtensionPoint().equals("unicity")) {
260            Object[] contribs = extension.getContributions();
261            for (Object contrib : contribs) {
262                if (contrib instanceof UnicityExtension) {
263                    registerUnicityOptions((UnicityExtension) contrib, extension);
264                }
265            }
266        } else if (extension.getExtensionPoint().equals("versioning")) {
267            Object[] contribs = extension.getContributions();
268            for (Object contrib : contribs) {
269                if (contrib instanceof VersioningDescriptor) {
270                    VersioningDescriptor descr = (VersioningDescriptor) contrib;
271                    String defver = descr.defaultVersioningOption;
272                    if (!StringUtils.isBlank(defver)) {
273                        try {
274                            defaultVersioningOption = VersioningOption.valueOf(defver.toUpperCase(Locale.ENGLISH));
275                        } catch (IllegalArgumentException e) {
276                            log.warn(String.format("Illegal versioning option: %s, using %s instead", defver,
277                                    DEF_VERSIONING_OPTION));
278                            defaultVersioningOption = DEF_VERSIONING_OPTION;
279                        }
280                    }
281                    Boolean veradd = descr.versionAfterAdd;
282                    if (veradd != null) {
283                        versioningAfterAdd = veradd.booleanValue();
284                    }
285                }
286            }
287        } else {
288            log.warn(String.format("Unknown contribution %s: ignored", extension.getExtensionPoint()));
289        }
290    }
291
292    @Override
293    public void unregisterExtension(Extension extension) {
294        if (extension.getExtensionPoint().equals("plugins")) {
295            Object[] contribs = extension.getContributions();
296
297            for (Object contrib : contribs) {
298                if (contrib instanceof FileImporterDescriptor) {
299                    unregisterFileImporter((FileImporterDescriptor) contrib);
300                } else if (contrib instanceof FolderImporterDescriptor) {
301                    unregisterFolderImporter((FolderImporterDescriptor) contrib);
302                } else if (contrib instanceof CreationContainerListProviderDescriptor) {
303                    unregisterCreationContainerListProvider((CreationContainerListProviderDescriptor) contrib);
304                }
305            }
306        } else if (extension.getExtensionPoint().equals("unicity")) {
307
308        } else if (extension.getExtensionPoint().equals("versioning")) {
309            // set to default value
310            defaultVersioningOption = DEF_VERSIONING_OPTION;
311            versioningAfterAdd = DEF_VERSIONING_AFTER_ADD;
312        } else {
313            log.warn(String.format("Unknown contribution %s: ignored", extension.getExtensionPoint()));
314        }
315    }
316
317    private void registerUnicityOptions(UnicityExtension unicityExtension, Extension extension) {
318        if (unicityExtension.getAlgo() != null) {
319            digestAlgorithm = unicityExtension.getAlgo();
320        }
321        if (unicityExtension.getEnabled() != null) {
322            unicityEnabled = unicityExtension.getEnabled().booleanValue();
323        }
324        if (unicityExtension.getFields() != null) {
325            fieldsXPath = unicityExtension.getFields();
326        } else {
327            fieldsXPath.add("file:content");
328        }
329        if (unicityExtension.getComputeDigest() != null) {
330            computeDigest = unicityExtension.getComputeDigest().booleanValue();
331        }
332    }
333
334    private void registerFileImporter(FileImporterDescriptor pluginExtension, Extension extension) {
335        String name = pluginExtension.getName();
336        if (name == null) {
337            log.error("Cannot register file importer without a name");
338            return;
339        }
340
341        String className = pluginExtension.getClassName();
342        if (fileImporters.containsKey(name)) {
343            log.info("Overriding file importer plugin " + name);
344            FileImporter oldPlugin = fileImporters.get(name);
345            FileImporter newPlugin;
346            try {
347                newPlugin = className != null ? (FileImporter) extension.getContext().loadClass(className).newInstance()
348                        : oldPlugin;
349            } catch (ReflectiveOperationException e) {
350                throw new RuntimeException(e);
351            }
352            if (pluginExtension.isMerge()) {
353                newPlugin = mergeFileImporters(oldPlugin, newPlugin, pluginExtension);
354            } else {
355                newPlugin = fillImporterWithDescriptor(newPlugin, pluginExtension);
356            }
357            fileImporters.put(name, newPlugin);
358            log.info("Registered file importer " + name);
359        } else if (className != null) {
360            FileImporter plugin;
361            try {
362                plugin = (FileImporter) extension.getContext().loadClass(className).newInstance();
363            } catch (ReflectiveOperationException e) {
364                throw new RuntimeException(e);
365            }
366            plugin = fillImporterWithDescriptor(plugin, pluginExtension);
367            fileImporters.put(name, plugin);
368            log.info("Registered file importer " + name);
369        } else {
370            log.info("Unable to register file importer " + name + ", className is null or plugin is not yet registered");
371        }
372    }
373
374    private FileImporter mergeFileImporters(FileImporter oldPlugin, FileImporter newPlugin, FileImporterDescriptor desc) {
375        List<String> filters = desc.getFilters();
376        if (filters != null && !filters.isEmpty()) {
377            List<String> oldFilters = oldPlugin.getFilters();
378            oldFilters.addAll(filters);
379            newPlugin.setFilters(oldFilters);
380        }
381        newPlugin.setName(desc.getName());
382        String docType = desc.getDocType();
383        if (docType != null) {
384            newPlugin.setDocType(docType);
385        }
386        newPlugin.setFileManagerService(this);
387        newPlugin.setEnabled(desc.isEnabled());
388        Integer order = desc.getOrder();
389        if (order != null) {
390            newPlugin.setOrder(desc.getOrder());
391        }
392        return newPlugin;
393    }
394
395    private FileImporter fillImporterWithDescriptor(FileImporter fileImporter, FileImporterDescriptor desc) {
396        List<String> filters = desc.getFilters();
397        if (filters != null && !filters.isEmpty()) {
398            fileImporter.setFilters(filters);
399        }
400        fileImporter.setName(desc.getName());
401        fileImporter.setDocType(desc.getDocType());
402        fileImporter.setFileManagerService(this);
403        fileImporter.setEnabled(desc.isEnabled());
404        fileImporter.setOrder(desc.getOrder());
405        return fileImporter;
406    }
407
408    private void unregisterFileImporter(FileImporterDescriptor pluginExtension) {
409        String name = pluginExtension.getName();
410        fileImporters.remove(name);
411        log.info("unregistered file importer: " + name);
412    }
413
414    private void registerFolderImporter(FolderImporterDescriptor folderImporterDescriptor, Extension extension) {
415
416        String name = folderImporterDescriptor.getName();
417        String className = folderImporterDescriptor.getClassName();
418
419        FolderImporter folderImporter;
420        try {
421            folderImporter = (FolderImporter) extension.getContext().loadClass(className).newInstance();
422        } catch (ReflectiveOperationException e) {
423            throw new RuntimeException(e);
424        }
425        folderImporter.setName(name);
426        folderImporter.setFileManagerService(this);
427        folderImporters.add(folderImporter);
428        log.info("registered folder importer: " + name);
429    }
430
431    private void unregisterFolderImporter(FolderImporterDescriptor folderImporterDescriptor) {
432        String name = folderImporterDescriptor.getName();
433        FolderImporter folderImporterToRemove = null;
434        for (FolderImporter folderImporter : folderImporters) {
435            if (name.equals(folderImporter.getName())) {
436                folderImporterToRemove = folderImporter;
437            }
438        }
439        if (folderImporterToRemove != null) {
440            folderImporters.remove(folderImporterToRemove);
441        }
442        log.info("unregistered folder importer: " + name);
443    }
444
445    private void registerCreationContainerListProvider(
446            CreationContainerListProviderDescriptor ccListProviderDescriptor, Extension extension) {
447
448        String name = ccListProviderDescriptor.getName();
449        String[] docTypes = ccListProviderDescriptor.getDocTypes();
450        String className = ccListProviderDescriptor.getClassName();
451
452        CreationContainerListProvider provider;
453        try {
454            provider = (CreationContainerListProvider) extension.getContext().loadClass(className).newInstance();
455        } catch (ReflectiveOperationException e) {
456            throw new RuntimeException(e);
457        }
458        provider.setName(name);
459        provider.setDocTypes(docTypes);
460        if (creationContainerListProviders.contains(provider)) {
461            // equality and containment tests are based on unique names
462            creationContainerListProviders.remove(provider);
463        }
464        // add the new provider at the beginning of the list
465        creationContainerListProviders.add(0, provider);
466        log.info("registered creationContaineterList provider: " + name);
467    }
468
469    private void unregisterCreationContainerListProvider(
470            CreationContainerListProviderDescriptor ccListProviderDescriptor) {
471        String name = ccListProviderDescriptor.getName();
472        CreationContainerListProvider providerToRemove = null;
473        for (CreationContainerListProvider provider : creationContainerListProviders) {
474            if (name.equals(provider.getName())) {
475                providerToRemove = provider;
476                break;
477            }
478        }
479        if (providerToRemove != null) {
480            creationContainerListProviders.remove(providerToRemove);
481        }
482        log.info("unregistered creationContaineterList provider: " + name);
483    }
484
485    public List<DocumentLocation> findExistingDocumentWithFile(CoreSession documentManager, String path, String digest,
486            Principal principal) {
487        String nxql = String.format(QUERY, digest);
488        DocumentModelList documentModelList = documentManager.query(nxql, MAX);
489        List<DocumentLocation> docLocationList = new ArrayList<DocumentLocation>(documentModelList.size());
490        for (DocumentModel documentModel : documentModelList) {
491            docLocationList.add(new DocumentLocationImpl(documentModel));
492        }
493        return docLocationList;
494    }
495
496    public boolean isUnicityEnabled() {
497        return unicityEnabled;
498    }
499
500    public boolean isDigestComputingEnabled() {
501        return computeDigest;
502    }
503
504    public List<String> getFields() {
505        return fieldsXPath;
506    }
507
508    public DocumentModelList getCreationContainers(Principal principal, String docType) {
509        DocumentModelList containers = new DocumentModelListImpl();
510        RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class);
511        for (String repositoryName : repositoryManager.getRepositoryNames()) {
512            try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) {
513                containers.addAll(getCreationContainers(session, docType));
514            }
515        }
516        return containers;
517    }
518
519    public DocumentModelList getCreationContainers(CoreSession documentManager, String docType) {
520        for (CreationContainerListProvider provider : creationContainerListProviders) {
521            if (provider.accept(docType)) {
522                return provider.getCreationContainerList(documentManager, docType);
523            }
524        }
525        return new DocumentModelListImpl();
526    }
527
528    public String getDigestAlgorithm() {
529        return digestAlgorithm;
530    }
531
532    @Override
533    public VersioningOption getVersioningOption() {
534        return defaultVersioningOption;
535    }
536
537    @Override
538    public boolean doVersioningAfterAdd() {
539        return versioningAfterAdd;
540    }
541
542}