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