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}