001/* 002 * Copyright 2019 Anyware Services 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 */ 016package org.ametys.plugins.contentio.archive; 017 018import java.io.IOException; 019import java.net.URI; 020import java.nio.file.Path; 021import java.nio.file.attribute.BasicFileAttributes; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Date; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Objects; 029import java.util.Optional; 030import java.util.function.Predicate; 031import java.util.stream.Collectors; 032import java.util.stream.Stream; 033import java.util.zip.ZipEntry; 034import java.util.zip.ZipOutputStream; 035 036import javax.jcr.AccessDeniedException; 037import javax.jcr.ItemNotFoundException; 038import javax.jcr.Node; 039import javax.jcr.RepositoryException; 040import javax.xml.parsers.DocumentBuilder; 041import javax.xml.parsers.DocumentBuilderFactory; 042import javax.xml.parsers.ParserConfigurationException; 043import javax.xml.transform.TransformerConfigurationException; 044import javax.xml.transform.TransformerException; 045import javax.xml.transform.sax.TransformerHandler; 046import javax.xml.transform.stream.StreamResult; 047 048import org.apache.avalon.framework.component.Component; 049import org.apache.avalon.framework.service.ServiceException; 050import org.apache.avalon.framework.service.ServiceManager; 051import org.apache.avalon.framework.service.Serviceable; 052import org.apache.commons.lang3.StringUtils; 053import org.slf4j.Logger; 054import org.w3c.dom.Document; 055import org.xml.sax.SAXException; 056 057import org.ametys.cms.content.ContentSaxer; 058import org.ametys.cms.content.references.OutgoingReferences; 059import org.ametys.cms.content.references.OutgoingReferencesExtractor; 060import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 061import org.ametys.cms.repository.Content; 062import org.ametys.cms.repository.DefaultContent; 063import org.ametys.cms.repository.ModifiableContent; 064import org.ametys.cms.repository.ModifiableContentHelper; 065import org.ametys.cms.repository.WorkflowAwareContent; 066import org.ametys.cms.repository.WorkflowAwareContentHelper; 067import org.ametys.core.user.UserIdentity; 068import org.ametys.plugins.contentio.archive.Archivers.AmetysObjectNotImportedException; 069import org.ametys.plugins.repository.AmetysObjectIterable; 070import org.ametys.plugins.repository.AmetysObjectResolver; 071import org.ametys.plugins.repository.TraversableAmetysObject; 072import org.ametys.plugins.repository.collection.AmetysObjectCollection; 073import org.ametys.plugins.repository.data.extractor.xml.XMLValuesExtractorAdditionalDataGetter; 074import org.ametys.plugins.repository.jcr.JCRAmetysObject; 075import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject; 076import org.ametys.plugins.workflow.support.WorkflowProvider; 077import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 078import org.ametys.runtime.plugin.component.AbstractLogEnabled; 079 080import com.opensymphony.workflow.WorkflowException; 081import com.opensymphony.workflow.spi.Step; 082 083/** 084 * Export a contents collection as individual XML files. 085 */ 086public class ContentsArchiverHelper extends AbstractLogEnabled implements Component, Serviceable 087{ 088 /** Avalon role. */ 089 public static final String ROLE = ContentsArchiverHelper.class.getName(); 090 091 private static final String __CONTENT_ZIP_ENTRY_FILENAME = "content.xml"; 092 private static final String __ACL_ZIP_ENTRY_FILENAME = "_acl.xml"; 093 094 private AmetysObjectResolver _resolver; 095 private ResourcesArchiverHelper _resourcesArchiverHelper; 096 private WorkflowProvider _workflowProvider; 097 private ModifiableContentHelper _modifiableContentHelper; 098 private OutgoingReferencesExtractor _outgoingReferencesExtractor; 099 private ContentSaxer _contentSaxer; 100 private ContentTypeExtensionPoint _contentTypeEP; 101 102 @Override 103 public void service(ServiceManager manager) throws ServiceException 104 { 105 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 106 _resourcesArchiverHelper = (ResourcesArchiverHelper) manager.lookup(ResourcesArchiverHelper.ROLE); 107 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 108 _modifiableContentHelper = (ModifiableContentHelper) manager.lookup(ModifiableContentHelper.ROLE); 109 _outgoingReferencesExtractor = (OutgoingReferencesExtractor) manager.lookup(OutgoingReferencesExtractor.ROLE); 110 _contentSaxer = (ContentSaxer) manager.lookup(ContentSaxer.ROLE); 111 _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE); 112 } 113 114 /** 115 * Exports contents from a root Node. 116 * @param prefix the prefix for the ZIP archive. 117 * @param rootNode the root JCR Node holding the contents collection. 118 * @param zos the ZIP OutputStream. 119 * @throws RepositoryException if an error occurs while resolving Node. 120 * @throws IOException if an error occurs while archiving 121 */ 122 public void exportContents(String prefix, Node rootNode, ZipOutputStream zos) throws RepositoryException, IOException 123 { 124 TraversableAmetysObject rootContents = _resolver.resolve(rootNode, false); 125 exportContents(prefix, rootContents, zos); 126 } 127 128 /** 129 * Exports contents from a root AmetysObject. 130 * @param prefix the prefix for the ZIP archive. 131 * @param rootContents the root JCR Node holding the contents collection. 132 * @param zos the ZIP OutputStream. 133 * @throws IOException if an error occurs while archiving 134 */ 135 public void exportContents(String prefix, TraversableAmetysObject rootContents, ZipOutputStream zos) throws IOException 136 { 137 zos.putNextEntry(new ZipEntry(StringUtils.appendIfMissing(prefix, "/"))); // even if there is no child, at least export the root of contents 138 139 AmetysObjectIterable<Content> contents = rootContents.getChildren(); 140 for (Content content : contents) 141 { 142 _exportContent(prefix, content, zos); 143 } 144 145 // finally process ACL for the contents root 146 try 147 { 148 Node contentNode = ((JCRAmetysObject) rootContents).getNode(); 149 Archivers.exportAcl(contentNode, zos, prefix + __ACL_ZIP_ENTRY_FILENAME); 150 } 151 catch (RepositoryException e) 152 { 153 throw new RuntimeException("Unable to SAX ACL for root contents at '" + rootContents.getPath() + "' for archiving", e); 154 } 155 } 156 157 private void _exportContent(String prefix, Content content, ZipOutputStream zos) throws IOException 158 { 159 List<String> unexistingContentTypesAndMixins = Stream.of(content.getTypes(), content.getMixinTypes()) 160 .flatMap(Stream::of) 161 .filter(Predicate.not(_contentTypeEP::hasExtension)) 162 .collect(Collectors.toList()); 163 if (!unexistingContentTypesAndMixins.isEmpty()) 164 { 165 getLogger().error("Content \"{}\" will not be exported as at least one of its types or mixins does not exist: {}", content, unexistingContentTypesAndMixins); 166 return; 167 } 168 169 // for each content, first an XML file with attributes, comments, tags, ... 170 String name = content.getName(); 171 String path = prefix + Archivers.getHashedPath(name) + "/" + name + "/"; 172 ZipEntry contentEntry = new ZipEntry(path + __CONTENT_ZIP_ENTRY_FILENAME); 173 zos.putNextEntry(contentEntry); 174 175 try 176 { 177 TransformerHandler contentHandler = Archivers.newTransformerHandler(); 178 contentHandler.setResult(new StreamResult(zos)); 179 180 contentHandler.startDocument(); 181 content.toSAX(contentHandler, null, null, true); 182 contentHandler.endDocument(); 183 } 184 catch (SAXException | TransformerConfigurationException e) 185 { 186 throw new RuntimeException("Unable to SAX content '" + content.getPath() + "' for archiving", e); 187 } 188 189 // then all attachments 190 _resourcesArchiverHelper.exportCollection(content.getRootAttachments(), zos, path + "_attachments/"); 191 192 // then all files local to rich texts (images) 193 Archivers.exportRichTexts(content, zos, path); 194 195 // then all binary attributes 196 Archivers.exportBinaries(content, zos, path); 197 198 // then all file attributes 199 Archivers.exportFiles(content, zos, path); 200 201 // then ACL 202 try 203 { 204 Node contentNode = ((JCRAmetysObject) content).getNode(); 205 Archivers.exportAcl(contentNode, zos, path + __ACL_ZIP_ENTRY_FILENAME); 206 } 207 catch (RepositoryException e) 208 { 209 throw new RuntimeException("Unable to SAX ACL for content '" + content.getPath() + "' for archiving", e); 210 } 211 } 212 213 /** 214 * Imports contents from the given ZIP archive and path, under the given root of contents 215 * @param commonPrefix The common prefix in the ZIP archive 216 * @param rootContents the root {@link JCRTraversableAmetysObject} holding the contents collection. 217 * @param zipPath the input zip path 218 * @param merger The {@link Merger} 219 * @param contentFillers The fillers in order to fill additional attributes on imported contents 220 * @return The {@link ImportReport} 221 * @throws IOException if an error occurs while importing archive 222 */ 223 public ImportReport importContents(String commonPrefix, AmetysObjectCollection rootContents, Path zipPath, Merger merger, Collection<ContentFiller> contentFillers) throws IOException 224 { 225 Importer importer; 226 List<DefaultContent> createdContents; 227 try 228 { 229 importer = new Importer(commonPrefix, rootContents, zipPath, merger, contentFillers, getLogger()); 230 createdContents = importer.importRoot(); 231 } 232 catch (ParserConfigurationException e) 233 { 234 throw new IOException(e); 235 } 236 _saveContents(rootContents); 237 _checkoutContents(createdContents); 238 return importer._report; 239 } 240 241 private void _saveContents(AmetysObjectCollection rootContents) 242 { 243 if (rootContents.needsSave()) 244 { 245 getLogger().warn(Archivers.WARN_MESSAGE_ROOT_HAS_PENDING_CHANGES, rootContents); 246 rootContents.saveChanges(); 247 } 248 } 249 250 private void _checkoutContents(List<DefaultContent> createdContents) 251 { 252 for (DefaultContent createdContent : createdContents) 253 { 254 createdContent.checkpoint(); 255 } 256 } 257 258 /** 259 * A filler in order to fill additional attributes on imported contents 260 */ 261 @FunctionalInterface 262 public static interface ContentFiller 263 { 264 /** 265 * Fill the content with additional attributes 266 * @param content The imported content 267 */ 268 void fillContent(DefaultContent content); 269 } 270 271 private class Importer 272 { 273 final ImportReport _report = new ImportReport(); 274 private final String _commonPrefix; 275 private final AmetysObjectCollection _root; 276 private final Path _zipArchivePath; 277 private final Merger _merger; 278 private final Collection<ContentFiller> _contentFillers; 279 private final Logger _logger; 280 private final DocumentBuilder _builder; 281 private final UnitaryContentImporter _unitaryImporter = new UnitaryContentImporter(); 282 283 Importer(String commonPrefix, AmetysObjectCollection root, Path zipArchivePath, Merger merger, Collection<ContentFiller> contentFillers, Logger logger) throws ParserConfigurationException 284 { 285 _commonPrefix = commonPrefix; 286 _root = root; 287 _zipArchivePath = zipArchivePath; 288 _merger = merger; 289 _contentFillers = contentFillers; 290 _logger = logger; 291 _builder = DocumentBuilderFactory.newInstance() 292 .newDocumentBuilder(); 293 } 294 295 private class UnitaryContentImporter implements UnitaryImporter<DefaultContent> 296 { 297 @Override 298 public String objectNameForLogs() 299 { 300 return "Content"; 301 } 302 303 @Override 304 public Document getPropertiesXml(Path zipEntryPath) throws Exception 305 { 306 return _getContentPropertiesXml(zipEntryPath); 307 } 308 309 @Override 310 public String retrieveId(Document propertiesXml) throws Exception 311 { 312 return Archivers.xpathEvalNonEmpty("content/@id", propertiesXml); 313 } 314 315 @Override 316 public DefaultContent create(Path zipEntryPath, String id, Document propertiesXml) throws AmetysObjectNotImportedException, Exception 317 { 318 DefaultContent createdContent = _createContent(zipEntryPath, propertiesXml); 319 Node contentNode = createdContent.getNode(); 320 _createContentAcl(contentNode, zipEntryPath); 321 Archivers.unitarySave(contentNode, _logger); 322 return createdContent; 323 } 324 325 @Override 326 public ImportReport getReport() 327 { 328 return _report; 329 } 330 } 331 332 List<DefaultContent> importRoot() throws IOException 333 { 334 _fillRoot(); 335 336 try (Stream<Path> zippedFiles = _matchingZippedFiles()) 337 { 338 // no stream pipeline here because exception flow is important 339 List<DefaultContent> createdContents = new ArrayList<>(); 340 for (Path zipEntryPath : zippedFiles.toArray(Path[]::new)) 341 { 342 Optional<DefaultContent> createdContent = _importContent(zipEntryPath); 343 createdContent.ifPresent(createdContents::add); 344 } 345 return createdContents; 346 } 347 } 348 349 private void _fillRoot() throws IOException 350 { 351 _createRootContentAcl(); 352 try 353 { 354 Archivers.unitarySave(_root.getNode(), _logger); 355 } 356 catch (AmetysObjectNotImportedException e) 357 { 358 // ACL were not exported => it was already logged in error level, and it does not affect the future import of contents => continue 359 } 360 } 361 362 private void _createRootContentAcl() throws IOException 363 { 364 Node rootNode = _root.getNode(); 365 String zipEntryPath = new StringBuilder() 366 .append(StringUtils.strip(_commonPrefix, "/")) 367 .append("/") 368 .append(__ACL_ZIP_ENTRY_FILENAME) 369 .toString(); 370 _createAcl(rootNode, zipEntryPath); 371 } 372 373 private void _createContentAcl(Node contentNode, Path contentZipEntryPath) throws IOException 374 { 375 String zipEntryPath = contentZipEntryPath 376 .getParent() 377 .resolve(__ACL_ZIP_ENTRY_FILENAME) 378 .toString(); 379 _createAcl(contentNode, zipEntryPath); 380 } 381 382 private void _createAcl(Node node, String zipAclEntryPath) throws IOException 383 { 384 try 385 { 386 _logger.debug("Trying to import ACL node for Content (or root of contents) '{}', from ACL XML file '{}', if it exists", node, zipAclEntryPath); 387 Archivers.importAcl(node, _zipArchivePath, _merger, zipAclEntryPath, _logger); 388 } 389 catch (RepositoryException e) 390 { 391 throw new IOException(e); 392 } 393 } 394 395 private Stream<Path> _matchingZippedFiles() throws IOException 396 { 397 return ZipEntryHelper.zipFileTree( 398 _zipArchivePath, 399 Optional.of(_commonPrefix), 400 (Path p, BasicFileAttributes attrs) -> 401 !attrs.isDirectory() 402 && __CONTENT_ZIP_ENTRY_FILENAME.equals(p.getFileName().toString())); 403 } 404 405 private Optional<DefaultContent> _importContent(Path zipEntryPath) throws ImportGlobalFailException 406 { 407 return _unitaryImporter.unitaryImport(_zipArchivePath, zipEntryPath, _merger, _logger); 408 } 409 410 private Document _getContentPropertiesXml(Path zipEntryPath) throws SAXException, IOException 411 { 412 URI zipEntryUri = zipEntryPath.toUri(); 413 return _builder.parse(zipEntryUri.toString()); 414 } 415 416 private DefaultContent _createContent(Path contentZipEntry, Document propertiesXml) throws AmetysObjectNotImportedException, Exception 417 { 418 // At first, check the content types and mixins exist in the current application, otherwise do not import the content ASAP 419 String[] contentTypes = _retrieveContentTypes(contentZipEntry, propertiesXml, "content/contentTypes/contentType"); 420 String[] mixins = _retrieveContentTypes(contentZipEntry, propertiesXml, "content/mixins/mixin"); 421 422 // Create the JCR Node 423 String uuid = Archivers.xpathEvalNonEmpty("content/@uuid", propertiesXml); 424 String contentDesiredName = Archivers.xpathEvalNonEmpty("content/@name", propertiesXml); 425 String type = Archivers.xpathEvalNonEmpty("content/@primaryType", propertiesXml); 426 _logger.info("Creating a Content object for '{}' file (uuid={}, type={}, desiredName={})", contentZipEntry, uuid, type, contentDesiredName); 427 428 DefaultContent createdContent = _createChild(uuid, contentDesiredName, type); 429 430 // Set mandatory properties 431 _setContentMandatoryProperties(createdContent, contentTypes, mixins, propertiesXml); 432 // Set other properties 433 _fillContentNode(createdContent, propertiesXml, contentZipEntry); 434 // Set content attachments 435 ImportReport importAttachmentReport = _fillContentAttachments(createdContent, contentZipEntry); 436 _report.addFrom(importAttachmentReport); 437 // Fill other attributes 438 _fillAdditionalContentAttributes(createdContent); 439 // Outgoing references 440 _setOutgoingReferences(createdContent); 441 442 // Initialize workflow 443 if (createdContent instanceof WorkflowAwareContent) 444 { 445 String workflowName = Archivers.xpathEvalNonEmpty("content/workflow-step/@workflowName", propertiesXml); 446 _handleWorkflow(workflowName, (WorkflowAwareContent) createdContent); 447 } 448 449 return createdContent; 450 } 451 452 private String[] _retrieveContentTypes(Path contentZipEntry, Document propertiesXml, String xPath) throws TransformerException, AmetysObjectNotImportedException 453 { 454 String[] contentTypes = DomNodeHelper.stringValues(propertiesXml, xPath); 455 List<String> unexistingTypes = Stream.of(contentTypes) 456 .filter(Predicate.not(_contentTypeEP::hasExtension)) 457 .collect(Collectors.toList()); 458 if (!unexistingTypes.isEmpty()) 459 { 460 String message = String.format("Content defined in '%s' has at least one of its types or mixins which does not exist: %s", contentZipEntry, unexistingTypes); 461 throw new AmetysObjectNotImportedException(message); 462 } 463 return contentTypes; 464 } 465 466 private DefaultContent _createChild(String uuid, String contentDesiredName, String type) throws AccessDeniedException, ItemNotFoundException, RepositoryException 467 { 468 // Create a content with AmetysObjectCollection.createChild 469 String unusedContentName = _getUnusedContentName(contentDesiredName); 470 JCRAmetysObject srcContent = (JCRAmetysObject) _root.createChild(unusedContentName, type); 471 Node srcNode = srcContent.getNode(); 472 // But then call 'replaceNodeWithDesiredUuid' to have it with the desired UUID (srcNode will be removed) 473 Node nodeWithDesiredUuid = Archivers.replaceNodeWithDesiredUuid(srcNode, uuid); 474 475 // Then resolve and return a Content 476 String parentPath = _root.getPath(); 477 DefaultContent createdContent = _resolver.resolve(parentPath, nodeWithDesiredUuid, null, false); 478 return createdContent; 479 } 480 481 // ~ same algorithm than org.ametys.cms.workflow.CreateContentFunction._createContent 482 // no use of org.ametys.cms.FilterNameHelper.filterName because it was already filtered during the export (taken from the existing content name) 483 private String _getUnusedContentName(String desiredName) 484 { 485 String contentName = desiredName; 486 for (int errorCount = 0; true; errorCount++) 487 { 488 if (errorCount != 0) 489 { 490 _logger.debug("Name '{}' from Content is already used. Trying another one...", contentName); 491 contentName = desiredName + "-" + (errorCount + 1); 492 } 493 if (!_root.hasChild(contentName)) 494 { 495 _logger.debug("Content will be created with unused name '{}'. The desired name was '{}'", contentName, desiredName); 496 return contentName; 497 } 498 } 499 } 500 501 private void _setContentMandatoryProperties(DefaultContent content, String[] contentTypes, String[] mixins, Document propertiesXml) throws TransformerException, AmetysObjectNotImportedException 502 { 503 _modifiableContentHelper.setTypes(content, contentTypes); 504 _modifiableContentHelper.setMixinTypes(content, mixins); 505 506 Date creationDate = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "content/@createdAt")); 507 _modifiableContentHelper.setCreationDate(content, creationDate); 508 509 String creator = Archivers.xpathEvalNonEmpty("content/@creator", propertiesXml); 510 _modifiableContentHelper.setCreator(content, UserIdentity.stringToUserIdentity(creator)); 511 512 Date lastModifiedAt = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "content/@lastModifiedAt")); 513 _modifiableContentHelper.setLastModified(content, lastModifiedAt); 514 515 String lastContributor = Archivers.xpathEvalNonEmpty("content/@lastContributor", propertiesXml); 516 _modifiableContentHelper.setLastContributor(content, UserIdentity.stringToUserIdentity(lastContributor)); 517 } 518 519 private void _fillContentNode(DefaultContent content, Document propertiesXml, Path contentZipEntry) throws TransformerException, Exception 520 { 521 String language = DomNodeHelper.nullableStringValue(propertiesXml, "content/@language"); 522 if (language != null) 523 { 524 _modifiableContentHelper.setLanguage(content, language); 525 } 526 527 Date lastValidatedAt = DomNodeHelper.nullableDatetimeValue(propertiesXml, "content/@lastValidatedAt"); 528 if (lastValidatedAt != null) 529 { 530 _modifiableContentHelper.setLastValidationDate(content, lastValidatedAt); 531 } 532 533 if (content instanceof ModifiableContent) 534 { 535 Path contentPath = contentZipEntry.getParent(); 536 _fillContent((ModifiableContent) content, propertiesXml, contentPath); 537 } 538 } 539 540 private void _fillContent(ModifiableContent content, Document propertiesXml, Path contentZipEntry) throws Exception 541 { 542 XMLValuesExtractorAdditionalDataGetter additionalDataGetter = new ResourcesAdditionalDataGetter(_zipArchivePath, contentZipEntry); 543 _contentSaxer.fillContent(content, propertiesXml, additionalDataGetter); 544 } 545 546 private ImportReport _fillContentAttachments(DefaultContent createdContent, Path contentZipEntry) throws IOException, RepositoryException 547 { 548 Node contentNode = createdContent.getNode(); 549 550 // ametys-internal:attachments is created automatically 551 if (contentNode.hasNode(DefaultContent.ATTACHMENTS_NODE_NAME)) 552 { 553 contentNode.getNode(DefaultContent.ATTACHMENTS_NODE_NAME).remove(); 554 } 555 556 Path contentAttachmentsZipEntryFolder = contentZipEntry.resolveSibling("_attachments/"); 557 String commonPrefix = StringUtils.appendIfMissing(contentAttachmentsZipEntryFolder.toString(), "/"); 558 return _resourcesArchiverHelper.importCollection(commonPrefix, contentNode, _zipArchivePath, _merger); 559 } 560 561 private void _fillAdditionalContentAttributes(DefaultContent content) 562 { 563 for (ContentFiller contentFiller : _contentFillers) 564 { 565 contentFiller.fillContent(content); 566 } 567 } 568 569 private void _setOutgoingReferences(DefaultContent content) 570 { 571 if (content instanceof ModifiableContent) 572 { 573 Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content); 574 ((ModifiableContent) content).setOutgoingReferences(outgoingReferencesByPath); 575 } 576 } 577 578 private void _handleWorkflow(String workflowName, WorkflowAwareContent createdContent) throws WorkflowException 579 { 580 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(createdContent); 581 582 int initialAction = 0; 583 Map<String, Object> inputs = new HashMap<>(Map.of()); 584 long workflowId = workflow.initialize(workflowName, initialAction, inputs); 585 WorkflowAwareContentHelper.setWorkflowId(createdContent, workflowId); 586 587 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next(); 588 createdContent.setCurrentStepId(currentStep.getStepId()); 589 } 590 } 591}