/*
* (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*/
package org.nuxeo.ecm.platform.filemanager.service;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentLocation;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.DocumentSecurityException;
import org.nuxeo.ecm.core.api.PathRef;
import org.nuxeo.ecm.core.api.VersioningOption;
import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
import org.nuxeo.ecm.core.api.repository.RepositoryManager;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.platform.filemanager.api.FileManager;
import org.nuxeo.ecm.platform.filemanager.service.extension.CreationContainerListProvider;
import org.nuxeo.ecm.platform.filemanager.service.extension.CreationContainerListProviderDescriptor;
import org.nuxeo.ecm.platform.filemanager.service.extension.FileImporter;
import org.nuxeo.ecm.platform.filemanager.service.extension.FileImporterDescriptor;
import org.nuxeo.ecm.platform.filemanager.service.extension.FolderImporter;
import org.nuxeo.ecm.platform.filemanager.service.extension.FolderImporterDescriptor;
import org.nuxeo.ecm.platform.filemanager.service.extension.UnicityExtension;
import org.nuxeo.ecm.platform.filemanager.service.extension.VersioningDescriptor;
import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils;
import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
import org.nuxeo.ecm.platform.types.TypeManager;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.logging.DeprecationLogger;
import org.nuxeo.runtime.model.ComponentName;
import org.nuxeo.runtime.model.DefaultComponent;
import org.nuxeo.runtime.model.Extension;
/**
* FileManager registry service.
* <p>
* This is the component to request to perform transformations. See API.
*
* @author <a href="mailto:andreas.kalogeropoulos@nuxeo.com">Andreas Kalogeropoulos</a>
*/
public class FileManagerService extends DefaultComponent implements FileManager {
public static final ComponentName NAME = new ComponentName(
"org.nuxeo.ecm.platform.filemanager.service.FileManagerService");
public static final String DEFAULT_FOLDER_TYPE_NAME = "Folder";
// TODO: OG: we should use an overridable query model instead of hardcoding
// the NXQL query
public static final String QUERY = "SELECT * FROM Document WHERE file:content/digest = '%s'";
public static final int MAX = 15;
private static final Log log = LogFactory.getLog(FileManagerService.class);
private final Map<String, FileImporter> fileImporters;
private final List<FolderImporter> folderImporters;
private final List<CreationContainerListProvider> creationContainerListProviders;
private List<String> fieldsXPath = new ArrayList<>();
private MimetypeRegistry mimeService;
private boolean unicityEnabled = false;
private String digestAlgorithm = "sha-256";
private boolean computeDigest = false;
public static final VersioningOption DEF_VERSIONING_OPTION = VersioningOption.MINOR;
public static final boolean DEF_VERSIONING_AFTER_ADD = false;
/**
* @since 5.7
* @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
* behaviors from importers
*/
@Deprecated
private VersioningOption defaultVersioningOption = DEF_VERSIONING_OPTION;
/**
* @since 5.7
* @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
* behaviors from importers
*/
@Deprecated
private boolean versioningAfterAdd = DEF_VERSIONING_AFTER_ADD;
private TypeManager typeService;
public FileManagerService() {
fileImporters = new HashMap<>();
folderImporters = new LinkedList<>();
creationContainerListProviders = new LinkedList<>();
}
private MimetypeRegistry getMimeService() {
if (mimeService == null) {
mimeService = Framework.getService(MimetypeRegistry.class);
}
return mimeService;
}
private TypeManager getTypeService() {
if (typeService == null) {
typeService = Framework.getService(TypeManager.class);
}
return typeService;
}
private Blob checkMimeType(Blob blob, String fullname) {
String filename = FileManagerUtils.fetchFileName(fullname);
blob = getMimeService().updateMimetype(blob, filename, true);
return blob;
}
@Override
public DocumentModel createFolder(CoreSession documentManager, String fullname, String path, boolean overwrite)
throws IOException {
if (folderImporters.isEmpty()) {
return defaultCreateFolder(documentManager, fullname, path, overwrite);
} else {
// use the last registered folder importer
FolderImporter folderImporter = folderImporters.get(folderImporters.size() - 1);
return folderImporter.create(documentManager, fullname, path, overwrite, getTypeService());
}
}
/**
* @deprecated since 9.1, use {@link #defaultCreateFolder(CoreSession, String, String, boolean)} instead
*/
@Deprecated
public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path) {
return defaultCreateFolder(documentManager, fullname, path, true);
}
/**
* @since 9.1
*/
public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
boolean overwrite) {
return defaultCreateFolder(documentManager, fullname, path, DEFAULT_FOLDER_TYPE_NAME, true, overwrite);
}
/**
* @deprecated since 9.1, use {@link #defaultCreateFolder(CoreSession, String, String, String, boolean, boolean)}
* instead
*/
@Deprecated
public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
String containerTypeName, boolean checkAllowedSubTypes) {
return defaultCreateFolder(documentManager, fullname, path, containerTypeName, checkAllowedSubTypes, true);
}
/**
* @since 9.1
*/
public DocumentModel defaultCreateFolder(CoreSession documentManager, String fullname, String path,
String containerTypeName, boolean checkAllowedSubTypes, boolean overwrite) {
// Fetching filename
String title = FileManagerUtils.fetchFileName(fullname);
if (overwrite) {
// Looking if an existing Folder with the same filename exists.
DocumentModel docModel = FileManagerUtils.getExistingDocByTitle(documentManager, path, title);
if (docModel != null) {
return docModel;
}
}
// check permissions
PathRef containerRef = new PathRef(path);
if (!documentManager.hasPermission(containerRef, SecurityConstants.READ_PROPERTIES)
|| !documentManager.hasPermission(containerRef, SecurityConstants.ADD_CHILDREN)) {
throw new DocumentSecurityException("Not enough rights to create folder");
}
// check allowed sub types
DocumentModel container = documentManager.getDocument(containerRef);
if (checkAllowedSubTypes
&& !getTypeService().isAllowedSubType(containerTypeName, container.getType(), container)) {
// cannot create document file here
// TODO: we should better raise a dedicated exception to be
// catched by the FileManageActionsBean instead of returning
// null
return null;
}
PathSegmentService pss = Framework.getService(PathSegmentService.class);
DocumentModel docModel = documentManager.createDocumentModel(containerTypeName);
docModel.setProperty("dublincore", "title", title);
// writing changes
docModel.setPathInfo(path, pss.generatePathSegment(docModel));
docModel = documentManager.createDocument(docModel);
documentManager.save();
log.debug("Created container: " + docModel.getName() + " with type " + containerTypeName);
return docModel;
}
@Override
public DocumentModel createDocumentFromBlob(CoreSession documentManager, Blob input, String path, boolean overwrite,
String fullName) throws IOException {
return createDocumentFromBlob(documentManager, input, path, overwrite, fullName, false);
}
@Override
public DocumentModel createDocumentFromBlob(CoreSession documentManager, Blob input, String path, boolean overwrite,
String fullName, boolean noMimeTypeCheck) throws IOException {
// check mime type to be able to select the best importer plugin
if (!noMimeTypeCheck) {
input = checkMimeType(input, fullName);
}
List<FileImporter> importers = new ArrayList<>(fileImporters.values());
Collections.sort(importers);
String normalizedMimeType = getMimeService().getMimetypeEntryByMimeType(input.getMimeType()).getNormalized();
for (FileImporter importer : importers) {
if (importer.isEnabled()
&& (importer.matches(normalizedMimeType) || importer.matches(input.getMimeType()))) {
DocumentModel doc = importer.create(documentManager, input, path, overwrite, fullName,
getTypeService());
if (doc != null) {
return doc;
}
}
}
return null;
}
@Override
public DocumentModel updateDocumentFromBlob(CoreSession documentManager, Blob input, String path, String fullName) {
String filename = FileManagerUtils.fetchFileName(fullName);
DocumentModel doc = FileManagerUtils.getExistingDocByFileName(documentManager, path, filename);
if (doc != null) {
doc.setProperty("file", "content", input);
documentManager.saveDocument(doc);
documentManager.save();
log.debug("Updated the document: " + doc.getName());
}
return doc;
}
public FileImporter getPluginByName(String name) {
return fileImporters.get(name);
}
@Override
public void registerExtension(Extension extension) {
if (extension.getExtensionPoint().equals("plugins")) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
if (contrib instanceof FileImporterDescriptor) {
registerFileImporter((FileImporterDescriptor) contrib, extension);
} else if (contrib instanceof FolderImporterDescriptor) {
registerFolderImporter((FolderImporterDescriptor) contrib, extension);
} else if (contrib instanceof CreationContainerListProviderDescriptor) {
registerCreationContainerListProvider((CreationContainerListProviderDescriptor) contrib, extension);
}
}
} else if (extension.getExtensionPoint().equals("unicity")) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
if (contrib instanceof UnicityExtension) {
registerUnicityOptions((UnicityExtension) contrib, extension);
}
}
} else if (extension.getExtensionPoint().equals("versioning")) {
String message = "Extension point 'versioning' has been deprecated and corresponding behavior removed from "
+ "Nuxeo Platform. Please use versioning policy instead.";
DeprecationLogger.log(message, "9.1");
Framework.getRuntime().getWarnings().add(message);
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
if (contrib instanceof VersioningDescriptor) {
VersioningDescriptor descr = (VersioningDescriptor) contrib;
String defver = descr.defaultVersioningOption;
if (!StringUtils.isBlank(defver)) {
try {
defaultVersioningOption = VersioningOption.valueOf(defver.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException e) {
log.warn(String.format("Illegal versioning option: %s, using %s instead", defver,
DEF_VERSIONING_OPTION));
defaultVersioningOption = DEF_VERSIONING_OPTION;
}
}
Boolean veradd = descr.versionAfterAdd;
if (veradd != null) {
versioningAfterAdd = veradd.booleanValue();
}
}
}
} else {
log.warn(String.format("Unknown contribution %s: ignored", extension.getExtensionPoint()));
}
}
@Override
public void unregisterExtension(Extension extension) {
if (extension.getExtensionPoint().equals("plugins")) {
Object[] contribs = extension.getContributions();
for (Object contrib : contribs) {
if (contrib instanceof FileImporterDescriptor) {
unregisterFileImporter((FileImporterDescriptor) contrib);
} else if (contrib instanceof FolderImporterDescriptor) {
unregisterFolderImporter((FolderImporterDescriptor) contrib);
} else if (contrib instanceof CreationContainerListProviderDescriptor) {
unregisterCreationContainerListProvider((CreationContainerListProviderDescriptor) contrib);
}
}
} else if (extension.getExtensionPoint().equals("unicity")) {
} else if (extension.getExtensionPoint().equals("versioning")) {
// set to default value
defaultVersioningOption = DEF_VERSIONING_OPTION;
versioningAfterAdd = DEF_VERSIONING_AFTER_ADD;
} else {
log.warn(String.format("Unknown contribution %s: ignored", extension.getExtensionPoint()));
}
}
private void registerUnicityOptions(UnicityExtension unicityExtension, Extension extension) {
if (unicityExtension.getAlgo() != null) {
digestAlgorithm = unicityExtension.getAlgo();
}
if (unicityExtension.getEnabled() != null) {
unicityEnabled = unicityExtension.getEnabled().booleanValue();
}
if (unicityExtension.getFields() != null) {
fieldsXPath = unicityExtension.getFields();
} else {
fieldsXPath.add("file:content");
}
if (unicityExtension.getComputeDigest() != null) {
computeDigest = unicityExtension.getComputeDigest().booleanValue();
}
}
private void registerFileImporter(FileImporterDescriptor pluginExtension, Extension extension) {
String name = pluginExtension.getName();
if (name == null) {
log.error("Cannot register file importer without a name");
return;
}
String className = pluginExtension.getClassName();
if (fileImporters.containsKey(name)) {
log.info("Overriding file importer plugin " + name);
FileImporter oldPlugin = fileImporters.get(name);
FileImporter newPlugin;
try {
newPlugin = className != null ? (FileImporter) extension.getContext().loadClass(className).newInstance()
: oldPlugin;
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
if (pluginExtension.isMerge()) {
newPlugin = mergeFileImporters(oldPlugin, newPlugin, pluginExtension);
} else {
newPlugin = fillImporterWithDescriptor(newPlugin, pluginExtension);
}
fileImporters.put(name, newPlugin);
log.info("Registered file importer " + name);
} else if (className != null) {
FileImporter plugin;
try {
plugin = (FileImporter) extension.getContext().loadClass(className).newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
plugin = fillImporterWithDescriptor(plugin, pluginExtension);
fileImporters.put(name, plugin);
log.info("Registered file importer " + name);
} else {
log.info(
"Unable to register file importer " + name + ", className is null or plugin is not yet registered");
}
}
private FileImporter mergeFileImporters(FileImporter oldPlugin, FileImporter newPlugin,
FileImporterDescriptor desc) {
List<String> filters = desc.getFilters();
if (filters != null && !filters.isEmpty()) {
List<String> oldFilters = oldPlugin.getFilters();
oldFilters.addAll(filters);
newPlugin.setFilters(oldFilters);
}
newPlugin.setName(desc.getName());
String docType = desc.getDocType();
if (docType != null) {
newPlugin.setDocType(docType);
}
newPlugin.setFileManagerService(this);
newPlugin.setEnabled(desc.isEnabled());
Integer order = desc.getOrder();
if (order != null) {
newPlugin.setOrder(desc.getOrder());
}
return newPlugin;
}
private FileImporter fillImporterWithDescriptor(FileImporter fileImporter, FileImporterDescriptor desc) {
List<String> filters = desc.getFilters();
if (filters != null && !filters.isEmpty()) {
fileImporter.setFilters(filters);
}
fileImporter.setName(desc.getName());
fileImporter.setDocType(desc.getDocType());
fileImporter.setFileManagerService(this);
fileImporter.setEnabled(desc.isEnabled());
fileImporter.setOrder(desc.getOrder());
return fileImporter;
}
private void unregisterFileImporter(FileImporterDescriptor pluginExtension) {
String name = pluginExtension.getName();
fileImporters.remove(name);
log.info("unregistered file importer: " + name);
}
private void registerFolderImporter(FolderImporterDescriptor folderImporterDescriptor, Extension extension) {
String name = folderImporterDescriptor.getName();
String className = folderImporterDescriptor.getClassName();
FolderImporter folderImporter;
try {
folderImporter = (FolderImporter) extension.getContext().loadClass(className).newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
folderImporter.setName(name);
folderImporter.setFileManagerService(this);
folderImporters.add(folderImporter);
log.info("registered folder importer: " + name);
}
private void unregisterFolderImporter(FolderImporterDescriptor folderImporterDescriptor) {
String name = folderImporterDescriptor.getName();
FolderImporter folderImporterToRemove = null;
for (FolderImporter folderImporter : folderImporters) {
if (name.equals(folderImporter.getName())) {
folderImporterToRemove = folderImporter;
}
}
if (folderImporterToRemove != null) {
folderImporters.remove(folderImporterToRemove);
}
log.info("unregistered folder importer: " + name);
}
private void registerCreationContainerListProvider(CreationContainerListProviderDescriptor ccListProviderDescriptor,
Extension extension) {
String name = ccListProviderDescriptor.getName();
String[] docTypes = ccListProviderDescriptor.getDocTypes();
String className = ccListProviderDescriptor.getClassName();
CreationContainerListProvider provider;
try {
provider = (CreationContainerListProvider) extension.getContext().loadClass(className).newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
provider.setName(name);
provider.setDocTypes(docTypes);
if (creationContainerListProviders.contains(provider)) {
// equality and containment tests are based on unique names
creationContainerListProviders.remove(provider);
}
// add the new provider at the beginning of the list
creationContainerListProviders.add(0, provider);
log.info("registered creationContaineterList provider: " + name);
}
private void unregisterCreationContainerListProvider(
CreationContainerListProviderDescriptor ccListProviderDescriptor) {
String name = ccListProviderDescriptor.getName();
CreationContainerListProvider providerToRemove = null;
for (CreationContainerListProvider provider : creationContainerListProviders) {
if (name.equals(provider.getName())) {
providerToRemove = provider;
break;
}
}
if (providerToRemove != null) {
creationContainerListProviders.remove(providerToRemove);
}
log.info("unregistered creationContaineterList provider: " + name);
}
@Override
public List<DocumentLocation> findExistingDocumentWithFile(CoreSession documentManager, String path, String digest,
Principal principal) {
String nxql = String.format(QUERY, digest);
DocumentModelList documentModelList = documentManager.query(nxql, MAX);
List<DocumentLocation> docLocationList = new ArrayList<>(documentModelList.size());
for (DocumentModel documentModel : documentModelList) {
docLocationList.add(new DocumentLocationImpl(documentModel));
}
return docLocationList;
}
@Override
public boolean isUnicityEnabled() {
return unicityEnabled;
}
@Override
public boolean isDigestComputingEnabled() {
return computeDigest;
}
@Override
public List<String> getFields() {
return fieldsXPath;
}
@Override
public DocumentModelList getCreationContainers(Principal principal, String docType) {
DocumentModelList containers = new DocumentModelListImpl();
RepositoryManager repositoryManager = Framework.getLocalService(RepositoryManager.class);
for (String repositoryName : repositoryManager.getRepositoryNames()) {
try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) {
containers.addAll(getCreationContainers(session, docType));
}
}
return containers;
}
@Override
public DocumentModelList getCreationContainers(CoreSession documentManager, String docType) {
for (CreationContainerListProvider provider : creationContainerListProviders) {
if (provider.accept(docType)) {
return provider.getCreationContainerList(documentManager, docType);
}
}
return new DocumentModelListImpl();
}
@Override
public String getDigestAlgorithm() {
return digestAlgorithm;
}
/**
* @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
* behaviors from importers
*/
@Override
@Deprecated
public VersioningOption getVersioningOption() {
return defaultVersioningOption;
}
/**
* @deprecated since 9.1 automatic versioning is now handled at versioning service level, remove versioning
* behaviors from importers
*/
@Override
@Deprecated
public boolean doVersioningAfterAdd() {
return versioningAfterAdd;
}
}