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