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.interfaces.MimetypeRegistry;
063import org.nuxeo.ecm.platform.types.TypeManager;
064import org.nuxeo.runtime.api.Framework;
065import org.nuxeo.runtime.model.ComponentName;
066import org.nuxeo.runtime.model.DefaultComponent;
067import org.nuxeo.runtime.model.Extension;
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    private static final Log log = LogFactory.getLog(FileManagerService.class);
090
091    private final Map<String, FileImporter> fileImporters;
092
093    private final List<FolderImporter> folderImporters;
094
095    private final List<CreationContainerListProvider> creationContainerListProviders;
096
097    private List<String> fieldsXPath = new ArrayList<String>();
098
099    private MimetypeRegistry mimeService;
100
101    private boolean unicityEnabled = false;
102
103    private String digestAlgorithm = "sha-256";
104
105    private boolean computeDigest = false;
106
107    public static final VersioningOption DEF_VERSIONING_OPTION = VersioningOption.MINOR;
108
109    public static final boolean DEF_VERSIONING_AFTER_ADD = false;
110
111    /**
112     * @since 5.7
113     */
114    private VersioningOption defaultVersioningOption = DEF_VERSIONING_OPTION;
115
116    /**
117     * @since 5.7
118     */
119    private boolean versioningAfterAdd = DEF_VERSIONING_AFTER_ADD;
120
121    private TypeManager typeService;
122
123    public FileManagerService() {
124        fileImporters = new HashMap<String, FileImporter>();
125        folderImporters = new LinkedList<FolderImporter>();
126        creationContainerListProviders = new LinkedList<CreationContainerListProvider>();
127    }
128
129    private MimetypeRegistry getMimeService() {
130        if (mimeService == null) {
131            mimeService = Framework.getService(MimetypeRegistry.class);
132        }
133        return mimeService;
134    }
135
136    private TypeManager getTypeService() {
137        if (typeService == null) {
138            typeService = Framework.getService(TypeManager.class);
139        }
140        return typeService;
141    }
142
143    private Blob checkMimeType(Blob blob, String fullname) {
144        String filename = FileManagerUtils.fetchFileName(fullname);
145        blob = getMimeService().updateMimetype(blob, filename, true);
146        return blob;
147    }
148
149    public DocumentModel createFolder(CoreSession documentManager, String fullname, String path) throws IOException {
150
151        if (folderImporters.isEmpty()) {
152            return defaultCreateFolder(documentManager, fullname, path);
153        } else {
154            // use the last registered folder importer
155            FolderImporter folderImporter = folderImporters.get(folderImporters.size() - 1);
156            return folderImporter.create(documentManager, fullname, path, true, getTypeService());
157        }
158    }
159
160    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path) {
161        return defaultCreateFolder(documentManager, fullname, path, DEFAULT_FOLDER_TYPE_NAME, true);
162    }
163
164    public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
165            String containerTypeName, boolean checkAllowedSubTypes) {
166
167        // Fetching filename
168        String title = FileManagerUtils.fetchFileName(fullname);
169
170        // Looking if an existing Folder with the same filename exists.
171        DocumentModel docModel = FileManagerUtils.getExistingDocByTitle(documentManager, path, title);
172
173        if (docModel == null) {
174            // check permissions
175            PathRef containerRef = new PathRef(path);
176            if (!documentManager.hasPermission(containerRef, SecurityConstants.READ_PROPERTIES)
177                    || !documentManager.hasPermission(containerRef, SecurityConstants.ADD_CHILDREN)) {
178                throw new DocumentSecurityException("Not enough rights to create folder");
179            }
180
181            // check allowed sub types
182            DocumentModel container = documentManager.getDocument(containerRef);
183            if (checkAllowedSubTypes
184                    && !getTypeService().isAllowedSubType(containerTypeName, container.getType(), container)) {
185                // cannot create document file here
186                // TODO: we should better raise a dedicated exception to be
187                // catched by the FileManageActionsBean instead of returning
188                // null
189                return null;
190            }
191
192            PathSegmentService pss = Framework.getService(PathSegmentService.class);
193            docModel = documentManager.createDocumentModel(containerTypeName);
194            docModel.setProperty("dublincore", "title", title);
195
196            // writing changes
197            docModel.setPathInfo(path, pss.generatePathSegment(docModel));
198            docModel = documentManager.createDocument(docModel);
199            documentManager.save();
200
201            log.debug("Created container: " + docModel.getName() + " with type " + containerTypeName);
202        }
203        return docModel;
204    }
205
206    public DocumentModel createDocumentFromBlob(CoreSession documentManager, Blob input, String path, boolean overwrite,
207        String fullName) throws IOException {
208        return createDocumentFromBlob(documentManager, input, path, overwrite, fullName, false);
209    }
210
211    public DocumentModel createDocumentFromBlob(CoreSession documentManager, Blob input, String path, boolean overwrite,
212            String fullName, boolean noMimeTypeCheck) throws IOException {
213
214        // check mime type to be able to select the best importer plugin
215        if (!noMimeTypeCheck) {
216            input = checkMimeType(input, fullName);
217        }
218
219        List<FileImporter> importers = new ArrayList<FileImporter>(fileImporters.values());
220        Collections.sort(importers);
221        String normalizedMimeType = getMimeService().getMimetypeEntryByMimeType(input.getMimeType()).getNormalized();
222        for (FileImporter importer : importers) {
223            if (importer.isEnabled()
224                    && (importer.matches(normalizedMimeType) || importer.matches(input.getMimeType()))) {
225                DocumentModel doc = importer.create(documentManager, input, path, overwrite, fullName,
226                        getTypeService());
227                if (doc != null) {
228                    return doc;
229                }
230            }
231        }
232        return null;
233    }
234
235    public DocumentModel updateDocumentFromBlob(CoreSession documentManager, Blob input, String path, String fullName) {
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(
378                    "Unable to register file importer " + name + ", className is null or plugin is not yet registered");
379        }
380    }
381
382    private FileImporter mergeFileImporters(FileImporter oldPlugin, FileImporter newPlugin,
383            FileImporterDescriptor desc) {
384        List<String> filters = desc.getFilters();
385        if (filters != null && !filters.isEmpty()) {
386            List<String> oldFilters = oldPlugin.getFilters();
387            oldFilters.addAll(filters);
388            newPlugin.setFilters(oldFilters);
389        }
390        newPlugin.setName(desc.getName());
391        String docType = desc.getDocType();
392        if (docType != null) {
393            newPlugin.setDocType(docType);
394        }
395        newPlugin.setFileManagerService(this);
396        newPlugin.setEnabled(desc.isEnabled());
397        Integer order = desc.getOrder();
398        if (order != null) {
399            newPlugin.setOrder(desc.getOrder());
400        }
401        return newPlugin;
402    }
403
404    private FileImporter fillImporterWithDescriptor(FileImporter fileImporter, FileImporterDescriptor desc) {
405        List<String> filters = desc.getFilters();
406        if (filters != null && !filters.isEmpty()) {
407            fileImporter.setFilters(filters);
408        }
409        fileImporter.setName(desc.getName());
410        fileImporter.setDocType(desc.getDocType());
411        fileImporter.setFileManagerService(this);
412        fileImporter.setEnabled(desc.isEnabled());
413        fileImporter.setOrder(desc.getOrder());
414        return fileImporter;
415    }
416
417    private void unregisterFileImporter(FileImporterDescriptor pluginExtension) {
418        String name = pluginExtension.getName();
419        fileImporters.remove(name);
420        log.info("unregistered file importer: " + name);
421    }
422
423    private void registerFolderImporter(FolderImporterDescriptor folderImporterDescriptor, Extension extension) {
424
425        String name = folderImporterDescriptor.getName();
426        String className = folderImporterDescriptor.getClassName();
427
428        FolderImporter folderImporter;
429        try {
430            folderImporter = (FolderImporter) extension.getContext().loadClass(className).newInstance();
431        } catch (ReflectiveOperationException e) {
432            throw new RuntimeException(e);
433        }
434        folderImporter.setName(name);
435        folderImporter.setFileManagerService(this);
436        folderImporters.add(folderImporter);
437        log.info("registered folder importer: " + name);
438    }
439
440    private void unregisterFolderImporter(FolderImporterDescriptor folderImporterDescriptor) {
441        String name = folderImporterDescriptor.getName();
442        FolderImporter folderImporterToRemove = null;
443        for (FolderImporter folderImporter : folderImporters) {
444            if (name.equals(folderImporter.getName())) {
445                folderImporterToRemove = folderImporter;
446            }
447        }
448        if (folderImporterToRemove != null) {
449            folderImporters.remove(folderImporterToRemove);
450        }
451        log.info("unregistered folder importer: " + name);
452    }
453
454    private void registerCreationContainerListProvider(CreationContainerListProviderDescriptor ccListProviderDescriptor,
455            Extension extension) {
456
457        String name = ccListProviderDescriptor.getName();
458        String[] docTypes = ccListProviderDescriptor.getDocTypes();
459        String className = ccListProviderDescriptor.getClassName();
460
461        CreationContainerListProvider provider;
462        try {
463            provider = (CreationContainerListProvider) extension.getContext().loadClass(className).newInstance();
464        } catch (ReflectiveOperationException e) {
465            throw new RuntimeException(e);
466        }
467        provider.setName(name);
468        provider.setDocTypes(docTypes);
469        if (creationContainerListProviders.contains(provider)) {
470            // equality and containment tests are based on unique names
471            creationContainerListProviders.remove(provider);
472        }
473        // add the new provider at the beginning of the list
474        creationContainerListProviders.add(0, provider);
475        log.info("registered creationContaineterList provider: " + name);
476    }
477
478    private void unregisterCreationContainerListProvider(
479            CreationContainerListProviderDescriptor ccListProviderDescriptor) {
480        String name = ccListProviderDescriptor.getName();
481        CreationContainerListProvider providerToRemove = null;
482        for (CreationContainerListProvider provider : creationContainerListProviders) {
483            if (name.equals(provider.getName())) {
484                providerToRemove = provider;
485                break;
486            }
487        }
488        if (providerToRemove != null) {
489            creationContainerListProviders.remove(providerToRemove);
490        }
491        log.info("unregistered creationContaineterList provider: " + name);
492    }
493
494    public List<DocumentLocation> findExistingDocumentWithFile(CoreSession documentManager, String path, String digest,
495            Principal principal) {
496        String nxql = String.format(QUERY, digest);
497        DocumentModelList documentModelList = documentManager.query(nxql, MAX);
498        List<DocumentLocation> docLocationList = new ArrayList<DocumentLocation>(documentModelList.size());
499        for (DocumentModel documentModel : documentModelList) {
500            docLocationList.add(new DocumentLocationImpl(documentModel));
501        }
502        return docLocationList;
503    }
504
505    public boolean isUnicityEnabled() {
506        return unicityEnabled;
507    }
508
509    public boolean isDigestComputingEnabled() {
510        return computeDigest;
511    }
512
513    public List<String> getFields() {
514        return fieldsXPath;
515    }
516
517    public DocumentModelList getCreationContainers(Principal principal, String docType) {
518        DocumentModelList containers = new DocumentModelListImpl();
519        RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class);
520        for (String repositoryName : repositoryManager.getRepositoryNames()) {
521            try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) {
522                containers.addAll(getCreationContainers(session, docType));
523            }
524        }
525        return containers;
526    }
527
528    public DocumentModelList getCreationContainers(CoreSession documentManager, String docType) {
529        for (CreationContainerListProvider provider : creationContainerListProviders) {
530            if (provider.accept(docType)) {
531                return provider.getCreationContainerList(documentManager, docType);
532            }
533        }
534        return new DocumentModelListImpl();
535    }
536
537    public String getDigestAlgorithm() {
538        return digestAlgorithm;
539    }
540
541    @Override
542    public VersioningOption getVersioningOption() {
543        return defaultVersioningOption;
544    }
545
546    @Override
547    public boolean doVersioningAfterAdd() {
548        return versioningAfterAdd;
549    }
550
551}