001/*
002 * (C) Copyright 2019 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.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.function.Function;
028import java.util.stream.Collectors;
029
030import org.apache.commons.lang3.StringUtils;
031import org.apache.logging.log4j.LogManager;
032import org.apache.logging.log4j.Logger;
033import org.nuxeo.ecm.core.api.Blob;
034import org.nuxeo.ecm.core.api.CoreInstance;
035import org.nuxeo.ecm.core.api.CoreSession;
036import org.nuxeo.ecm.core.api.DocumentLocation;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.DocumentModelList;
039import org.nuxeo.ecm.core.api.DocumentSecurityException;
040import org.nuxeo.ecm.core.api.NuxeoPrincipal;
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.FileImporterContext;
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.RuntimeMessage.Level;
062import org.nuxeo.runtime.api.Framework;
063import org.nuxeo.runtime.logging.DeprecationLogger;
064import org.nuxeo.runtime.model.ComponentContext;
065import org.nuxeo.runtime.model.ComponentInstance;
066import org.nuxeo.runtime.model.ComponentName;
067import org.nuxeo.runtime.model.DefaultComponent;
068
069/**
070 * FileManager registry service.
071 * <p>
072 * This is the component to request to perform transformations. See API.
073 *
074 * @author <a href="mailto:andreas.kalogeropoulos@nuxeo.com">Andreas Kalogeropoulos</a>
075 */
076public class FileManagerService extends DefaultComponent implements FileManager {
077
078    public static final ComponentName NAME = new ComponentName(
079            "org.nuxeo.ecm.platform.filemanager.service.FileManagerService");
080
081    public static final String DEFAULT_FOLDER_TYPE_NAME = "Folder";
082
083    // TODO: OG: we should use an overridable query model instead of hardcoding
084    // the NXQL query
085    public static final String QUERY = "SELECT * FROM Document WHERE file:content/digest = '%s'";
086
087    public static final int MAX = 15;
088
089    /** @since 11.1 */
090    public static final String PLUGINS_EP = "plugins";
091
092    /** @since 11.1 */
093    public static final String UNICITY_EP = "unicity";
094
095    /** @since 11.1 */
096    public static final String VERSIONING_EP = "versioning";
097
098    private static final Logger log = LogManager.getLogger(FileManagerService.class);
099
100    private Map<String, FileImporter> fileImporters;
101
102    private List<FolderImporter> folderImporters;
103
104    private List<CreationContainerListProvider> creationContainerListProviders;
105
106    private List<String> fieldsXPath = new ArrayList<>();
107
108    private boolean unicityEnabled = false;
109
110    private String digestAlgorithm = "sha-256";
111
112    private boolean computeDigest = false;
113
114    public static final VersioningOption DEF_VERSIONING_OPTION = VersioningOption.MINOR;
115
116    public static final boolean DEF_VERSIONING_AFTER_ADD = false;
117
118    /**
119     * @since 5.7
120     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
121     *             behaviors from importers
122     */
123    @Deprecated(since = "9.1")
124    private VersioningOption defaultVersioningOption = DEF_VERSIONING_OPTION;
125
126    /**
127     * @since 5.7
128     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
129     *             behaviors from importers
130     */
131    @Deprecated(since = "9.1")
132    private boolean versioningAfterAdd = DEF_VERSIONING_AFTER_ADD;
133
134    @Override
135    public void registerContribution(Object contribution, String xp, ComponentInstance component) {
136        if (PLUGINS_EP.equals(xp)) {
137            xp = computePluginsExtensionPoint(contribution.getClass());
138        }
139        super.registerContribution(contribution, xp, component);
140    }
141
142    @Override
143    public void unregisterContribution(Object contribution, String xp, ComponentInstance component) {
144        if (PLUGINS_EP.equals(xp)) {
145            xp = computePluginsExtensionPoint(contribution.getClass());
146        }
147        super.unregisterContribution(contribution, xp, component);
148    }
149
150    @Override
151    public void start(ComponentContext context) {
152        super.start(context);
153
154        registerFileImporters();
155        registerFolderImporters();
156        registerCreationContainerListProviders();
157        registerUnicity();
158        registerVersioning();
159    }
160
161    protected void registerFileImporters() {
162        String xp = computePluginsExtensionPoint(FileImporterDescriptor.class);
163        fileImporters = getDescriptors(xp).stream()
164                                          .map(FileImporterDescriptor.class::cast)
165                                          .map(FileImporterDescriptor::newInstance)
166                                          .collect(Collectors.toMap(FileImporter::getName, Function.identity()));
167    }
168
169    protected void registerFolderImporters() {
170        String xp = computePluginsExtensionPoint(FolderImporterDescriptor.class);
171        folderImporters = getDescriptors(xp).stream()
172                                            .map(FolderImporterDescriptor.class::cast)
173                                            .map(FolderImporterDescriptor::newInstance)
174                                            .collect(Collectors.toList());
175    }
176
177    protected void registerCreationContainerListProviders() {
178        String xp = computePluginsExtensionPoint(CreationContainerListProviderDescriptor.class);
179        creationContainerListProviders = getDescriptors(xp).stream()
180                                                           .map(CreationContainerListProviderDescriptor.class::cast)
181                                                           .map(CreationContainerListProviderDescriptor::newInstance)
182                                                           .collect(Collectors.toList());
183    }
184
185    protected void registerUnicity() {
186        getDescriptors(UNICITY_EP).stream().map(UnicityExtension.class::cast).forEach(unicityExtension -> {
187            if (unicityExtension.getAlgo() != null) {
188                digestAlgorithm = unicityExtension.getAlgo();
189            }
190            if (unicityExtension.getEnabled() != null) {
191                unicityEnabled = unicityExtension.getEnabled();
192            }
193            if (unicityExtension.getFields() != null) {
194                fieldsXPath = unicityExtension.getFields();
195            } else {
196                fieldsXPath.add("file:content");
197            }
198            if (unicityExtension.getComputeDigest() != null) {
199                computeDigest = unicityExtension.getComputeDigest();
200            }
201        });
202    }
203
204    /**
205     * @deprecated since 9.1
206     */
207    @Deprecated(since = "9.1")
208    protected void registerVersioning() {
209        getDescriptors(VERSIONING_EP).stream().map(VersioningDescriptor.class::cast).forEach(versioningDescriptor -> {
210            String message = "Extension point 'versioning' has been deprecated and corresponding behavior removed from "
211                    + "Nuxeo Platform. Please use versioning policy instead.";
212            DeprecationLogger.log(message, "9.1");
213            addRuntimeMessage(Level.WARNING, message);
214
215            String defver = versioningDescriptor.defaultVersioningOption;
216            if (!StringUtils.isBlank(defver)) {
217                try {
218                    defaultVersioningOption = VersioningOption.valueOf(defver.toUpperCase(Locale.ENGLISH));
219                } catch (IllegalArgumentException e) {
220                    log.warn("Illegal versioning option: {}, using {} instead", defver, DEF_VERSIONING_OPTION);
221                    defaultVersioningOption = DEF_VERSIONING_OPTION;
222                }
223            }
224            if (versioningDescriptor.versionAfterAdd != null) {
225                versioningAfterAdd = versioningDescriptor.versionAfterAdd;
226            }
227        });
228    }
229
230    protected String computePluginsExtensionPoint(Class<?> klass) {
231        return String.format("%s-%s", PLUGINS_EP, klass.getSimpleName());
232    }
233
234    private Blob checkMimeType(Blob blob, String fullname) {
235        String filename = FileManagerUtils.fetchFileName(fullname);
236        blob = Framework.getService(MimetypeRegistry.class).updateMimetype(blob, filename, true);
237        return blob;
238    }
239
240    @Override
241    public DocumentModel createFolder(CoreSession documentManager, String fullname, String path, boolean overwrite)
242            throws IOException {
243
244        if (folderImporters.isEmpty()) {
245            return defaultCreateFolder(documentManager, fullname, path, overwrite);
246        } else {
247            // use the last registered folder importer
248            FolderImporter folderImporter = folderImporters.get(folderImporters.size() - 1);
249            return folderImporter.create(documentManager, fullname, path, overwrite,
250                    Framework.getService(TypeManager.class));
251        }
252    }
253
254    /**
255     * @deprecated since 9.1, use {@link #defaultCreateFolder(CoreSession, String, String, boolean)} instead
256     */
257    @Deprecated(since = "9.1")
258    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path) {
259        return defaultCreateFolder(documentManager, fullname, path, true);
260    }
261
262    /**
263     * @since 9.1
264     */
265    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
266            boolean overwrite) {
267        return defaultCreateFolder(documentManager, fullname, path, DEFAULT_FOLDER_TYPE_NAME, true, overwrite);
268    }
269
270    /**
271     * @deprecated since 9.1, use {@link #defaultCreateFolder(CoreSession, String, String, String, boolean, boolean)}
272     *             instead
273     */
274    @Deprecated(since = "9.1")
275    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
276            String containerTypeName, boolean checkAllowedSubTypes) {
277        return defaultCreateFolder(documentManager, fullname, path, containerTypeName, checkAllowedSubTypes, true);
278    }
279
280    /**
281     * @since 9.1
282     */
283    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
284            String containerTypeName, boolean checkAllowedSubTypes, boolean overwrite) {
285
286        // Fetching filename
287        String title = FileManagerUtils.fetchFileName(fullname);
288
289        if (overwrite) {
290            // Looking if an existing Folder with the same filename exists.
291            DocumentModel docModel = FileManagerUtils.getExistingDocByTitle(documentManager, path, title);
292            if (docModel != null) {
293                return docModel;
294            }
295        }
296
297        // check permissions
298        PathRef containerRef = new PathRef(path);
299        if (!documentManager.hasPermission(containerRef, SecurityConstants.READ_PROPERTIES)
300                || !documentManager.hasPermission(containerRef, SecurityConstants.ADD_CHILDREN)) {
301            throw new DocumentSecurityException("Not enough rights to create folder");
302        }
303
304        // check allowed sub types
305        DocumentModel container = documentManager.getDocument(containerRef);
306        if (checkAllowedSubTypes && !Framework.getService(TypeManager.class)
307                                              .isAllowedSubType(containerTypeName, container.getType(), container)) {
308            // cannot create document file here
309            // TODO: we should better raise a dedicated exception to be
310            // catched by the FileManageActionsBean instead of returning
311            // null
312            return null;
313        }
314
315        PathSegmentService pss = Framework.getService(PathSegmentService.class);
316        DocumentModel docModel = documentManager.createDocumentModel(containerTypeName);
317        docModel.setProperty("dublincore", "title", title);
318
319        // writing changes
320        docModel.setPathInfo(path, pss.generatePathSegment(docModel));
321        docModel = documentManager.createDocument(docModel);
322        documentManager.save();
323
324        log.debug("Created container: {} with type {}", docModel::getName, () -> containerTypeName);
325        return docModel;
326    }
327
328    @Override
329    public DocumentModel createDocumentFromBlob(CoreSession documentManager, Blob input, String path, boolean overwrite,
330            String fullName) throws IOException {
331        return createDocumentFromBlob(documentManager, input, path, overwrite, fullName, false);
332    }
333
334    @Override
335    public DocumentModel createDocumentFromBlob(CoreSession documentManager, Blob input, String path, boolean overwrite,
336            String fullName, boolean noMimeTypeCheck) throws IOException {
337        FileImporterContext context = FileImporterContext.builder(documentManager, input, path)
338                                                         .overwrite(overwrite)
339                                                         .fileName(fullName)
340                                                         .mimeTypeCheck(!noMimeTypeCheck)
341                                                         .build();
342        return createOrUpdateDocument(context);
343    }
344
345    @Override
346    public DocumentModel createOrUpdateDocument(FileImporterContext context) throws IOException {
347        Blob blob = context.getBlob();
348
349        // check mime type to be able to select the best importer plugin
350        if (context.isMimeTypeCheck()) {
351            blob = checkMimeType(blob, context.getFileName());
352        }
353
354        List<FileImporter> importers = new ArrayList<>(fileImporters.values());
355        Collections.sort(importers);
356        String mimeType = blob.getMimeType();
357        String normalizedMimeType = Framework.getService(MimetypeRegistry.class)
358                                             .getMimetypeEntryByMimeType(mimeType)
359                                             .getNormalized();
360        for (FileImporter importer : importers) {
361            if (isImporterAvailable(importer, normalizedMimeType, mimeType, context.isExcludeOneToMany())) {
362                DocumentModel doc = importer.createOrUpdate(context);
363                if (doc != null) {
364                    return doc;
365                }
366            }
367        }
368        return null;
369    }
370
371    protected boolean isImporterAvailable(FileImporter importer, String normalizedMimeType, String mimeType,
372            boolean excludeOneToMany) {
373        return importer.isEnabled() && !(importer.isOneToMany() && excludeOneToMany)
374                && (importer.matches(normalizedMimeType) || importer.matches(mimeType));
375    }
376
377    @Override
378    public DocumentModel updateDocumentFromBlob(CoreSession documentManager, Blob input, String path, String fullName) {
379        String filename = FileManagerUtils.fetchFileName(fullName);
380        DocumentModel doc = FileManagerUtils.getExistingDocByFileName(documentManager, path, filename);
381        if (doc != null) {
382            doc.setProperty("file", "content", input);
383
384            documentManager.saveDocument(doc);
385            documentManager.save();
386
387            log.debug("Updated the document: {}", doc::getName);
388        }
389        return doc;
390    }
391
392    public FileImporter getPluginByName(String name) {
393        return fileImporters.get(name);
394    }
395
396    @Override
397    public List<DocumentLocation> findExistingDocumentWithFile(CoreSession documentManager, String path, String digest,
398            NuxeoPrincipal principal) {
399        String nxql = String.format(QUERY, digest);
400        DocumentModelList documentModelList = documentManager.query(nxql, MAX);
401        List<DocumentLocation> docLocationList = new ArrayList<>(documentModelList.size());
402        for (DocumentModel documentModel : documentModelList) {
403            docLocationList.add(new DocumentLocationImpl(documentModel));
404        }
405        return docLocationList;
406    }
407
408    @Override
409    public boolean isUnicityEnabled() {
410        return unicityEnabled;
411    }
412
413    @Override
414    public boolean isDigestComputingEnabled() {
415        return computeDigest;
416    }
417
418    @Override
419    public List<String> getFields() {
420        return fieldsXPath;
421    }
422
423    @Override
424    public DocumentModelList getCreationContainers(NuxeoPrincipal principal, String docType) {
425        DocumentModelList containers = new DocumentModelListImpl();
426        RepositoryManager repositoryManager = Framework.getService(RepositoryManager.class);
427        for (String repositoryName : repositoryManager.getRepositoryNames()) {
428            CoreSession session = CoreInstance.getCoreSession(repositoryName, principal);
429            DocumentModelList docs = getCreationContainers(session, docType);
430            docs.forEach(doc -> doc.detach(true));
431            containers.addAll(docs);
432        }
433        return containers;
434    }
435
436    @Override
437    public DocumentModelList getCreationContainers(CoreSession documentManager, String docType) {
438        for (CreationContainerListProvider provider : creationContainerListProviders) {
439            if (provider.accept(docType)) {
440                return provider.getCreationContainerList(documentManager, docType);
441            }
442        }
443        return new DocumentModelListImpl();
444    }
445
446    @Override
447    public String getDigestAlgorithm() {
448        return digestAlgorithm;
449    }
450
451    /**
452     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
453     *             behaviors from importers
454     */
455    @Override
456    @Deprecated(since = "9.1")
457    public VersioningOption getVersioningOption() {
458        return defaultVersioningOption;
459    }
460
461    /**
462     * @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
463     *             behaviors from importers
464     */
465    @Override
466    @Deprecated(since = "9.1")
467    public boolean doVersioningAfterAdd() {
468        return versioningAfterAdd;
469    }
470
471}