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