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}