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