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