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