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