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