001/*
002 *  Copyright 2011 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.web.repository.page;
017
018import java.io.InputStream;
019import java.io.OutputStream;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Properties;
027import java.util.Set;
028import java.util.stream.Stream;
029
030import javax.jcr.Node;
031import javax.jcr.RepositoryException;
032import javax.xml.transform.OutputKeys;
033import javax.xml.transform.TransformerFactory;
034import javax.xml.transform.sax.SAXTransformerFactory;
035import javax.xml.transform.sax.TransformerHandler;
036import javax.xml.transform.stream.StreamResult;
037
038import org.apache.avalon.framework.component.Component;
039import org.apache.avalon.framework.logger.AbstractLogEnabled;
040import org.apache.avalon.framework.service.ServiceException;
041import org.apache.avalon.framework.service.ServiceManager;
042import org.apache.avalon.framework.service.Serviceable;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.excalibur.xml.sax.SAXParser;
045import org.apache.xml.serializer.OutputPropertiesFactory;
046import org.xml.sax.ContentHandler;
047import org.xml.sax.InputSource;
048
049import org.ametys.cms.CmsConstants;
050import org.ametys.cms.content.references.OutgoingReferences;
051import org.ametys.cms.content.references.OutgoingReferencesExtractor;
052import org.ametys.cms.contenttype.RichTextUpdater;
053import org.ametys.cms.data.ContentDataHelper;
054import org.ametys.cms.data.ExplorerFile;
055import org.ametys.cms.data.File;
056import org.ametys.cms.data.RichText;
057import org.ametys.cms.data.type.RichTextElementType;
058import org.ametys.cms.repository.Content;
059import org.ametys.cms.repository.ModifiableContent;
060import org.ametys.cms.repository.WorkflowAwareContent;
061import org.ametys.cms.repository.WorkflowAwareContentHelper;
062import org.ametys.plugins.repository.AmetysObject;
063import org.ametys.plugins.repository.AmetysObjectIterable;
064import org.ametys.plugins.repository.AmetysObjectResolver;
065import org.ametys.plugins.repository.AmetysRepositoryException;
066import org.ametys.plugins.repository.ModifiableAmetysObject;
067import org.ametys.plugins.repository.RepositoryConstants;
068import org.ametys.plugins.repository.TraversableAmetysObject;
069import org.ametys.plugins.repository.UnknownAmetysObjectException;
070import org.ametys.plugins.repository.data.UnknownDataException;
071import org.ametys.plugins.repository.data.holder.DataHolder;
072import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
073import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
074import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
075import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder;
076import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
077import org.ametys.plugins.repository.data.holder.group.Repeater;
078import org.ametys.plugins.repository.data.holder.group.RepeaterEntry;
079import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
080import org.ametys.plugins.repository.data.type.ModelItemTypeConstants;
081import org.ametys.plugins.repository.version.VersionableAmetysObject;
082import org.ametys.plugins.workflow.support.WorkflowProvider;
083import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
084import org.ametys.runtime.model.ElementDefinition;
085import org.ametys.runtime.model.ModelItem;
086import org.ametys.runtime.model.exception.BadItemTypeException;
087import org.ametys.runtime.model.exception.NotUniqueTypeException;
088import org.ametys.runtime.model.exception.UndefinedItemPathException;
089import org.ametys.runtime.model.exception.UnknownTypeException;
090import org.ametys.web.repository.ModifiableSiteAwareAmetysObject;
091import org.ametys.web.repository.content.WebContent;
092import org.ametys.web.repository.content.jcr.DefaultSharedContent;
093import org.ametys.web.repository.content.shared.SharedContentManager;
094import org.ametys.web.repository.page.ZoneItem.ZoneType;
095import org.ametys.web.repository.site.Site;
096import org.ametys.web.repository.sitemap.Sitemap;
097import org.ametys.web.site.CopyUpdaterExtensionPoint;
098
099import com.opensymphony.workflow.spi.Step;
100
101/**
102 * Component for copying site or pages
103 *
104 */
105public class CopySiteComponent extends AbstractLogEnabled implements Component, Serviceable
106{
107    /** Avalon Role */
108    public static final String ROLE = CopySiteComponent.class.getName();
109    
110    /** The service manager. */
111    protected ServiceManager _manager;
112    
113    private AmetysObjectResolver _resolver;
114    private WorkflowProvider _workflowProvider;
115    
116    private CopyUpdaterExtensionPoint _updaterEP;
117    private OutgoingReferencesExtractor _outgoingReferencesExtractor;
118    private SharedContentManager _sharedContentManager;
119    
120    @Override
121    public void service(ServiceManager serviceManager) throws ServiceException
122    {
123        _manager = serviceManager;
124        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
125        _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE);
126        _updaterEP = (CopyUpdaterExtensionPoint) serviceManager.lookup(CopyUpdaterExtensionPoint.ROLE);
127        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) serviceManager.lookup(OutgoingReferencesExtractor.ROLE);
128        _sharedContentManager = (SharedContentManager) serviceManager.lookup(SharedContentManager.ROLE);
129    }
130
131    /**
132     * This methods must be used after calling <code>copyTo</code> on a Page.
133     * Its updates references to ametys objects for metadata of new created pages and contents
134     * @param originalPage the original page
135     * @param createdPage the created page after a copy
136     * @throws AmetysRepositoryException if an error occurs
137     */
138    public void updateReferencesAfterCopy (Page originalPage, Page createdPage) throws AmetysRepositoryException
139    {
140        // Update references to ametys object on metadata
141        _updateReferencesToAmetysObjects (createdPage, originalPage, createdPage);
142        
143        for (Zone zone : createdPage.getZones())
144        {
145            _updateReferencesToAmetysObjects (zone, originalPage, createdPage);
146            
147            try (AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems())
148            {
149                for (ZoneItem zoneItem : zoneItems)
150                {
151                    _updateReferencesToAmetysObjects (zoneItem, originalPage, createdPage);
152                    
153                    if (zoneItem.getType().equals(ZoneType.SERVICE))
154                    {
155                        _updateReferencesToAmetysObjects (zoneItem, originalPage, createdPage);
156                    }
157                    else
158                    {
159                        Content content = zoneItem.getContent();
160                        _updateReferencesToAmetysObjects(content, originalPage, createdPage);
161                    }
162                }
163            }
164        }
165        
166        // Browse child pages
167        for (Page childPage : createdPage.getChildrenPages())
168        {
169            updateReferencesAfterCopy ((Page) originalPage.getChild(childPage.getName()), childPage);
170        }
171    }
172    
173    /**
174     * This method must be used after calling <code>copyTo</code> on a Site.
175     * Its updates contents and pages after a site copy
176     * @param originalSite the original site
177     * @param createdSite the created site after copy
178     * @throws AmetysRepositoryException if an error occurs
179     */
180    public void updateSiteAfterCopy (Site originalSite, Site createdSite) throws AmetysRepositoryException
181    {
182        updateContentsAfterCopy (originalSite, createdSite);
183        updatePagesAfterCopy (originalSite, createdSite);
184        
185        Set<String> ids = _updaterEP.getExtensionsIds();
186        for (String id : ids)
187        {
188            _updaterEP.getExtension(id).updateSite(originalSite, createdSite);
189        }
190    }
191    
192    /**
193     * This method re-initializes workflow, updates the site name for web content and updates references to ametys objects on metadata after a site copy
194     * @param initialSite the original site
195     * @param createdSite the created site after copy
196     * @throws AmetysRepositoryException if an error occurs 
197     */
198    public void updateContentsAfterCopy (Site initialSite, Site createdSite) throws AmetysRepositoryException
199    {
200        AmetysObjectIterable<Content> contents = createdSite.getContents();
201        for (Content content : contents)
202        {
203            String relPath = content.getPath().substring(createdSite.getPath().length() + 1);
204            WebContent initialContent = initialSite.getChild(relPath);
205            
206            try
207            {
208                // Re-init workflow
209                if (content instanceof WorkflowAwareContent)
210                {
211                    _reinitWorkflow ((WorkflowAwareContent) content);
212                }
213                
214                // Update site name
215                if (content instanceof ModifiableSiteAwareAmetysObject)
216                {
217                    ((ModifiableSiteAwareAmetysObject) content).setSiteName(createdSite.getName());
218                }
219                
220                if (content instanceof ModifiableContent)
221                {
222                    // Update references to ametys object on attributes
223                    _updateReferencesToAmetysObjects(content, initialSite, createdSite);
224                    
225                    // Update links in RichText
226                    updateLinksInRichText (initialSite, createdSite, initialContent, content);
227                }
228                
229                // Updaters
230                Set<String> ids = _updaterEP.getExtensionsIds();
231                for (String id : ids)
232                {
233                    _updaterEP.getExtension(id).updateContent(initialSite, createdSite, initialContent, content);
234                }
235                
236                boolean needsSaveAndCheckpoint = true;
237                
238                // Validate the shared content if the original content is validated.
239                if (content instanceof DefaultSharedContent sharedContent)
240                {
241                    Content referenceContent = sharedContent.getInitialContent();
242                    if (referenceContent != null && referenceContent instanceof VersionableAmetysObject vaContent)
243                    {
244                        if (Arrays.asList(vaContent.getAllLabels()).contains(CmsConstants.LIVE_LABEL))
245                        {
246                            _sharedContentManager.validateContent(sharedContent);
247                            needsSaveAndCheckpoint = false; // validateContent already saved and checkpointed the content
248                        }
249                    }
250                }
251
252                if (needsSaveAndCheckpoint)
253                {
254                    // save
255                    if (content instanceof ModifiableAmetysObject)
256                    {
257                        ((ModifiableAmetysObject) content).saveChanges();
258                    }
259                    
260                    // Creates the first version
261                    if (content instanceof VersionableAmetysObject)
262                    {
263                        ((VersionableAmetysObject) content).checkpoint();
264                    }
265                }
266            }
267            catch (Exception e)
268            {
269                // Do not make the copy fail.
270                getLogger().warn("[Site copy] An error occured while updating content '" + content.getId() + " after copy from initial content '" + initialContent.getId() + "'", e);
271            }
272        }
273        
274        if (createdSite.needsSave())
275        {
276            createdSite.saveChanges();
277        }
278    }
279
280    /**
281     * Updates references all references in a content to another one.
282     * @param initialContent the initial content.
283     * @param destContent the destination content.
284     */
285    public void updateSharedContent(WebContent initialContent, WebContent destContent)
286    {
287        updateSharedContent(initialContent, destContent, true);
288    }
289    
290    /**
291     * Updates references all references in a content to another one.
292     * @param initialContent the initial content.
293     * @param destContent the destination content.
294     * @param reinitWorkflow set to 'true' to reinitialize the workflow
295     */
296    public void updateSharedContent(WebContent initialContent, WebContent destContent, boolean reinitWorkflow)
297    {
298        Site initialSite = initialContent.getSite();
299        Site createdSite = destContent.getSite();
300        
301        // Re-init workflow
302        if (reinitWorkflow && destContent instanceof WorkflowAwareContent)
303        {
304            _reinitWorkflow((WorkflowAwareContent) destContent);
305        }
306        
307        // Update references to ametys object on attributes
308        _updateReferencesToAmetysObjects(destContent, initialContent, destContent);
309        
310        // Update links in RichText
311        updateLinksInRichText(initialContent, destContent, initialContent, destContent);
312        
313        // Updaters
314        Set<String> ids = _updaterEP.getExtensionsIds();
315        for (String id : ids)
316        {
317            _updaterEP.getExtension(id).updateContent(initialSite, createdSite, initialContent, destContent);
318        }
319    }
320    
321    /**
322     * This method analyzes content rich texts and update links if necessary
323     * @param initialAO The initial object copied
324     * @param createdAO The target object 
325     * @param initialContent The initial content
326     * @param createdContent The created content after copy to update
327     * @throws AmetysRepositoryException if an error occurs
328     */
329    public void updateLinksInRichText (TraversableAmetysObject initialAO, TraversableAmetysObject createdAO, Content initialContent, Content createdContent) throws AmetysRepositoryException
330    {
331        SAXParser saxParser = null;
332        try
333        {
334            if (createdContent instanceof ModifiableContent)
335            {
336                saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE);
337                
338                Map<String, Object> params = new HashMap<>();
339                params.put("initialContent", initialContent);
340                params.put("createdContent", createdContent);
341                params.put("initialAO", initialAO);
342                params.put("createdAO", createdAO);
343                
344                Map<String, Object> richTexts = DataHolderHelper.findEditableItemsByType(createdContent, org.ametys.cms.data.type.ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID);
345                for (Map.Entry<String, Object> entry : richTexts.entrySet())
346                {
347                    Object value = entry.getValue();
348                    if (value != null)
349                    {
350                        String attributePath = entry.getKey();
351                        ModelItem attributeDefinition = createdContent.getDefinition(attributePath);
352                        RichTextElementType type = (RichTextElementType) attributeDefinition.getType();
353                        RichTextUpdater richTextUpdater = type.getRichTextUpdater();
354                        
355                        if (value instanceof RichText)
356                        {
357                            _updateRichText((RichText) value, richTextUpdater, params, saxParser);
358                        }
359                        else if (value instanceof RichText[])
360                        {
361                            for (RichText richText : (RichText[]) value)
362                            {
363                                _updateRichText(richText, richTextUpdater, params, saxParser);
364                            }
365                        }
366                        
367                        ((ModifiableContent) createdContent).setValue(attributePath, value);
368                    }
369                }
370            
371                // Outgoing references
372                Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(createdContent);
373                ((ModifiableContent) createdContent).setOutgoingReferences(outgoingReferencesByPath);
374            }
375        }
376        catch (Exception e)
377        {
378            // Do not failed the copy
379            getLogger().warn("An error occured while updating links in RichText for content '" + createdContent.getId() + " after copy from initial content '" + initialContent.getId() + "'", e);
380        }
381        finally
382        {
383            _manager.release(saxParser);
384        }
385    }
386
387    private void _updateRichText(RichText richText, RichTextUpdater richTextUpdater, Map<String, Object> params, SAXParser saxParser) throws Exception
388    {
389        try (InputStream is = richText.getInputStream(); OutputStream os = richText.getOutputStream())
390        {
391            // create a transformer for saving sax into a file
392            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
393
394            // create the result where to write
395            StreamResult result = new StreamResult(os);
396            th.setResult(result);
397
398            // create the format of result
399            Properties format = new Properties();
400            format.put(OutputKeys.METHOD, "xml");
401            format.put(OutputKeys.INDENT, "yes");
402            format.put(OutputKeys.ENCODING, "UTF-8");
403            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
404            th.getTransformer().setOutputProperties(format);
405
406            ContentHandler richTextHandler = richTextUpdater.getContentHandler(th, th, params);
407            saxParser.parse(new InputSource(is), richTextHandler);
408        }
409    }
410    
411    /**
412     * This method updates the site name of pages and updates references to ametys objects on page's metadata after a site copy
413     * @param originalSite the original site
414     * @param createdSite the created site after copy
415     * @throws AmetysRepositoryException if an error occurs 
416     */
417    public void updatePagesAfterCopy (Site originalSite, Site createdSite) throws AmetysRepositoryException
418    {
419        AmetysObjectIterable<Sitemap> sitemaps = createdSite.getSitemaps();
420        
421        for (Sitemap sitemap : sitemaps)
422        {
423            for (Page page : sitemap.getChildrenPages())
424            {
425                _updatePageAfterCopy (originalSite, createdSite, page);
426            }
427        }
428    }
429    
430    private void _updatePageAfterCopy (Site originalSite, Site createdSite, Page page) throws AmetysRepositoryException
431    {
432        try
433        {
434            // Update site name
435            if (page instanceof ModifiablePage)
436            {
437                ((ModifiablePage) page).setSiteName(createdSite.getName());
438            }
439            
440            // Update references to ametys object on metadata
441            _updateReferencesToAmetysObjects (page, originalSite, createdSite);
442            
443            for (Zone zone : page.getZones())
444            {
445                _updateReferencesToAmetysObjects (zone, originalSite, createdSite);
446                
447                try (AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems())
448                {
449                    for (ZoneItem zoneItem : zoneItems)
450                    {
451                        try
452                        {
453                            _updateZoneItemAfterCopy(originalSite, createdSite, zoneItem);
454                        }
455                        catch (Exception e)
456                        {
457                            getLogger().warn("An error occured while updating zone item '" + zoneItem.getId() + "' (" + zoneItem.getPath() + ") after copy", e);
458                        }
459                    }
460                }
461            }
462        }
463        catch (AmetysRepositoryException e)
464        {
465            // Do not failed the copy
466            getLogger().warn("An error occured while updating page '" + page.getId() + "' (" + page.getPathInSitemap() + ") after copy", e);
467        }
468        
469        // Browse child pages
470        for (Page childPage : page.getChildrenPages())
471        {
472            _updatePageAfterCopy (originalSite, createdSite, childPage);
473        }
474        
475        // Updaters
476        Set<String> ids = _updaterEP.getExtensionsIds();
477        for (String id : ids)
478        {
479            _updaterEP.getExtension(id).updatePage(originalSite, createdSite, page);
480        }
481    }
482
483    private void _updateZoneItemAfterCopy(Site originalSite, Site createdSite, ZoneItem zoneItem)
484    {
485        _updateReferencesToAmetysObjects (zoneItem, originalSite, createdSite);
486        
487        if (zoneItem.getType().equals(ZoneType.SERVICE))
488        {
489            _updateReferencesToAmetysObjects (zoneItem.getServiceParameters(), originalSite, createdSite);
490        }
491        else if (zoneItem.getType().equals(ZoneType.CONTENT) && zoneItem instanceof ModifiableZoneItem)
492        {
493            Content content = zoneItem.getContent();
494            String path = content.getPath();
495            
496            String originalPath = originalSite.getPath();
497            if (path.startsWith(originalPath))
498            {
499                String relPath = path.substring(originalPath.length() + 1);
500                try
501                {
502                    // Find symmetric object on copied sub-tree
503                    Content child = createdSite.getChild(relPath);
504                    ((ModifiableZoneItem) zoneItem).setContent(child);
505                }
506                catch (UnknownAmetysObjectException e)
507                {
508                    // Nothing
509                }
510            }
511        }
512    }
513    
514    
515    
516    private void _updateReferencesToAmetysObjects (DataHolder dataHolder, TraversableAmetysObject originalAO, TraversableAmetysObject createdAO)
517    {
518        try
519        {
520            for (String dataName : dataHolder.getDataNames())
521            {
522                if (ModelItemTypeConstants.COMPOSITE_TYPE_ID.equals(_getDataTypeID(dataHolder, dataName)))
523                {
524                    _updateReferencesToAmetysObjects(dataHolder.getComposite(dataName), originalAO, createdAO);
525                }
526                else if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(_getDataTypeID(dataHolder, dataName)) && dataHolder instanceof ModelAwareDataHolder)
527                {
528                    // Repeaters are only available on model aware data holders
529                    Repeater repeater = ((ModelAwareDataHolder) dataHolder).getRepeater(dataName);
530                    for (RepeaterEntry entry : repeater.getEntries())
531                    {
532                        _updateReferencesToAmetysObjects(entry, originalAO, createdAO);
533                    }
534                }
535                else if (_isAmetysObject(dataHolder, dataName))
536                {
537                    _updateReferenceToAmetysObject(dataHolder, dataName, originalAO, createdAO);
538                }
539            }
540        }
541        catch (UndefinedItemPathException | UnknownDataException | UnknownTypeException | NotUniqueTypeException e)
542        {
543            // The type of the data has not been determined so there is no way to determine if it is a reference to an ametys object
544        }
545        
546    }
547    
548    private void _updateReferenceToAmetysObject (DataHolder dataHolder, String dataName, TraversableAmetysObject originalAO, TraversableAmetysObject createdAO) throws AmetysRepositoryException
549    {
550        boolean isDataModifiable = dataHolder instanceof ModifiableDataHolder;
551        if (isDataModifiable && dataHolder instanceof ModelAwareDataHolder modelAwareDataHolder)
552        {
553            ElementDefinition definition = (ElementDefinition) modelAwareDataHolder.getDefinition(dataName); // definition can be casted hare. No way we can have groups here.
554            isDataModifiable = definition.isEditable();
555        }
556        
557        if (isDataModifiable)
558        {
559            if (_isDataMultiple(dataHolder, dataName))
560            {
561                String[] ids = _getValue(dataHolder, dataName);
562                List<String> newReferences = new ArrayList<>();
563                boolean hasNewReference = false;
564                for (String id : ids)
565                {
566                    String newReference = _getNewReferenceToAmetysObject(originalAO, createdAO, id).orElse(id);
567                    newReferences.add(newReference);
568                    hasNewReference |= !newReference.equals(id);
569                }
570                
571                if (hasNewReference)
572                {
573                    _setValue((ModifiableDataHolder) dataHolder, dataName, newReferences.toArray(new String[newReferences.size()]));
574                }
575            }
576            else
577            {
578                String id = _getValue(dataHolder, dataName);
579                _getNewReferenceToAmetysObject(originalAO, createdAO, id)
580                    .ifPresent(newReference -> _setValue((ModifiableDataHolder) dataHolder, dataName, newReference));
581            }
582        }
583    }
584    
585    /**
586     * Retrieves the updated reference to an ametys object.
587     * If the object has been copied into the created site, the new reference is the symmetric object on created site
588     * Otherwise, the reference does not change, there is no new reference to retrieve
589     * @param originalAO the original ametys object containing the reference
590     * @param createdAO the created ametys object where to update the reference
591     * @param id the id of the referenced ametys object to potentially update
592     * @return the new reference to the ametys object, an empty {@link Optional} if the reference does not to change
593     */
594    private Optional<String> _getNewReferenceToAmetysObject(TraversableAmetysObject originalAO, TraversableAmetysObject createdAO, String id)
595    {
596        AmetysObject ametysObject = _resolver.resolveById(id);
597        String path = ametysObject.getPath();
598        
599        String originalPath = originalAO.getPath();
600        if (path.startsWith(originalPath + "/"))
601        {
602            String relPath = path.substring(originalPath.length() + 1);
603            try
604            {
605                // Find symmetric object on copied sub-tree
606                AmetysObject child = createdAO.getChild(relPath);
607                return Optional.of(child.getId());
608            }
609            catch (UnknownAmetysObjectException e)
610            {
611                getLogger().warn("Object of path " + relPath + " was not found on copied sub-tree " + createdAO.getPath(), e);
612                return Optional.empty();
613            }
614            catch (AmetysRepositoryException e)
615            {
616                getLogger().error("Unable to retrieve object of path " + relPath + " on copied sub-tree " + createdAO.getPath(), e);
617                return Optional.empty();
618            }
619        }
620        else
621        {
622            return Optional.empty();
623        }
624    }
625    
626    private boolean _isAmetysObject (DataHolder dataHolder, String dataName)
627    {
628        try
629        {
630            String typeId = _getDataTypeID(dataHolder, dataName);
631            switch (typeId)
632            {
633                case org.ametys.cms.data.type.ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID:
634                    return dataHolder.hasValue(dataName);
635                case org.ametys.runtime.model.type.ModelItemTypeConstants.STRING_TYPE_ID:
636                case org.ametys.cms.data.type.ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID:
637                    if (dataHolder.hasValue(dataName))
638                    {
639                        if (_isDataMultiple(dataHolder, dataName))
640                        {
641                            String[] values = _getValue(dataHolder, dataName);
642                            for (String value : values)
643                            {
644                                if (_resolver.hasAmetysObjectForId(value))
645                                {
646                                    return true;
647                                }
648                            }
649                            
650                            // None of the value of the multiple data is a reference
651                            return false;
652                        }
653                        else
654                        {
655                            String value = _getValue(dataHolder, dataName);
656                            return _resolver.hasAmetysObjectForId(value);
657                        }
658                    }
659                    else
660                    {
661                        return false;
662                    }
663                default:
664                    return false;
665            }
666        }
667        catch (AmetysRepositoryException | UndefinedItemPathException | UnknownTypeException | NotUniqueTypeException e)
668        {
669            return false;
670        }
671    }
672    
673    private String _getDataTypeID(DataHolder dataHolder, String dataName) throws UndefinedItemPathException, UnknownDataException, UnknownTypeException, NotUniqueTypeException
674    {
675        return dataHolder instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) dataHolder).getType(dataName).getId() : ((ModelLessDataHolder) dataHolder).getType(dataName).getId();
676    }
677    
678    private boolean _isDataMultiple(DataHolder dataHolder, String dataName)
679    {
680        return dataHolder instanceof ModelAwareDataHolder ? ((ModelAwareDataHolder) dataHolder).isMultiple(dataName) : ((ModelLessDataHolder) dataHolder).isMultiple(dataName);
681    }
682    
683    @SuppressWarnings("unchecked")
684    private <T> T _getValue(DataHolder dataHolder, String dataName)
685    {
686        if (dataHolder instanceof ModelAwareDataHolder)
687        {
688            ModelAwareDataHolder modelAwareDataHolder = (ModelAwareDataHolder) dataHolder;
689            String typeId = _getDataTypeID(dataHolder, dataName);
690            if (org.ametys.cms.data.type.ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID.equals(typeId))
691            {
692                return _isDataMultiple(dataHolder, dataName)
693                        ? (T) ContentDataHelper.getContentIdsArrayFromMultipleContentData(modelAwareDataHolder, dataName)
694                        : (T) ContentDataHelper.getContentIdFromContentData(modelAwareDataHolder, dataName);
695            }
696            else if (org.ametys.cms.data.type.ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID.equals(typeId))
697            {
698                return _isDataMultiple(dataHolder, dataName)
699                        ? (T) _getResourceIdsArrayFromMultipleFileData(modelAwareDataHolder, dataName)
700                        : (T) _getResourceIdFromFileData(modelAwareDataHolder, dataName);
701            }
702            else
703            {
704                return modelAwareDataHolder.getValue(dataName);
705            }
706        }
707        else
708        {
709            return ((ModelLessDataHolder) dataHolder).getValue(dataName);
710        }
711    }
712    
713    private String _getResourceIdFromFileData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException
714    {
715        File value = dataHolder.getValue(dataPath);
716        return Optional.ofNullable(value)
717                       .filter(ExplorerFile.class::isInstance)
718                       .map(ExplorerFile.class::cast)
719                       .map(ExplorerFile::getResourceId)
720                       .orElse(StringUtils.EMPTY);
721    }
722    
723    private String[] _getResourceIdsArrayFromMultipleFileData(ModelAwareDataHolder dataHolder, String dataPath) throws BadItemTypeException
724    {
725        File[] value = dataHolder.getValue(dataPath);
726        return Optional.ofNullable(value)
727                .map(v -> Arrays.stream(v))
728                .orElse(Stream.empty())
729                .filter(ExplorerFile.class::isInstance)
730                .map(ExplorerFile.class::cast)
731                .map(ExplorerFile::getResourceId)
732                .toArray(String[]::new);
733    }
734    
735    private void _setValue(ModifiableDataHolder dataHolder, String dataName, Object value)
736    {
737        if (dataHolder instanceof ModifiableModelAwareDataHolder)
738        {
739            ((ModifiableModelAwareDataHolder) dataHolder).setValue(dataName, value);
740        }
741        else
742        {
743            ((ModifiableModelLessDataHolder) dataHolder).setValue(dataName, value);
744        }
745    }
746    
747    private void _reinitWorkflow (WorkflowAwareContent content) throws AmetysRepositoryException
748    {
749        try
750        {
751            long wId = content.getWorkflowId();
752            
753            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
754            String workflowName = workflow.getWorkflowName(wId);
755            
756            // 1 - Delete the cloned workflow
757            WorkflowAwareContentHelper.removeWorkflowId(content);
758            
759            // For legacy purpose, delete the workflow reference property if exists (only for contents created on 3.x versions)
760            Node node = content.getNode();
761            try
762            {
763                if (node.hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowRef"))
764                {
765                    node.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowRef").remove();
766                }
767            }
768            catch (RepositoryException e)
769            {
770                throw new AmetysRepositoryException("Unable to remove workflowId property", e);
771            }
772            
773            workflow.removeWorkflow(wId);
774            
775            // 2 - Initialize new workflow instance
776            HashMap<String, Object> inputs = new HashMap<>();
777            long workflowId = workflow.initialize(workflowName, 0, inputs);
778            content.setWorkflowId(workflowId);
779            
780            // Update current step property
781            Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next();
782            content.setCurrentStepId(currentStep.getStepId());
783        }
784        catch (Exception e)
785        {
786            throw new AmetysRepositoryException("Unable to initialize workflow for content " + content.getId(), e);
787        }
788        
789    }
790}