001/*
002 *  Copyright 2013 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.cms.content;
017
018import java.io.InputStream;
019import java.io.OutputStream;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Properties;
031import java.util.Set;
032
033import javax.xml.transform.OutputKeys;
034import javax.xml.transform.TransformerFactory;
035import javax.xml.transform.sax.SAXTransformerFactory;
036import javax.xml.transform.sax.TransformerHandler;
037import javax.xml.transform.stream.StreamResult;
038
039import org.apache.avalon.framework.component.Component;
040import org.apache.avalon.framework.service.ServiceException;
041import org.apache.avalon.framework.service.ServiceManager;
042import org.apache.avalon.framework.service.Serviceable;
043import org.apache.cocoon.ProcessingException;
044import org.apache.commons.lang3.BooleanUtils;
045import org.apache.commons.lang3.StringUtils;
046import org.apache.excalibur.xml.sax.ContentHandlerProxy;
047import org.apache.excalibur.xml.sax.SAXParser;
048import org.apache.xml.serializer.OutputPropertiesFactory;
049import org.slf4j.Logger;
050import org.xml.sax.Attributes;
051import org.xml.sax.ContentHandler;
052import org.xml.sax.InputSource;
053import org.xml.sax.SAXException;
054
055import org.ametys.cms.content.CopyReport.CopyMode;
056import org.ametys.cms.content.CopyReport.CopyState;
057import org.ametys.cms.content.references.OutgoingReferences;
058import org.ametys.cms.content.references.OutgoingReferencesExtractor;
059import org.ametys.cms.contenttype.AbstractMetadataSetElement;
060import org.ametys.cms.contenttype.ContentType;
061import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
062import org.ametys.cms.contenttype.ContentTypesHelper;
063import org.ametys.cms.contenttype.MetadataDefinition;
064import org.ametys.cms.contenttype.MetadataDefinitionReference;
065import org.ametys.cms.contenttype.MetadataManager;
066import org.ametys.cms.contenttype.MetadataSet;
067import org.ametys.cms.contenttype.MetadataType;
068import org.ametys.cms.contenttype.RepeaterDefinition;
069import org.ametys.cms.contenttype.RichTextUpdater;
070import org.ametys.cms.repository.Content;
071import org.ametys.cms.repository.DefaultContent;
072import org.ametys.cms.repository.ModifiableContent;
073import org.ametys.cms.repository.WorkflowAwareContent;
074import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
075import org.ametys.cms.workflow.CreateContentFunction;
076import org.ametys.cms.workflow.EditContentFunction;
077import org.ametys.cms.workflow.copy.CreateContentByCopyFunction;
078import org.ametys.cms.workflow.copy.EditContentByCopyFunction;
079import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
080import org.ametys.plugins.explorer.resources.ResourceCollection;
081import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
082import org.ametys.plugins.repository.AmetysObject;
083import org.ametys.plugins.repository.AmetysObjectIterable;
084import org.ametys.plugins.repository.AmetysObjectResolver;
085import org.ametys.plugins.repository.AmetysRepositoryException;
086import org.ametys.plugins.repository.CopiableAmetysObject;
087import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
088import org.ametys.plugins.repository.TraversableAmetysObject;
089import org.ametys.plugins.repository.metadata.BinaryMetadata;
090import org.ametys.plugins.repository.metadata.CompositeMetadata;
091import org.ametys.plugins.repository.metadata.File;
092import org.ametys.plugins.repository.metadata.Folder;
093import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata;
094import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
095import org.ametys.plugins.repository.metadata.ModifiableFile;
096import org.ametys.plugins.repository.metadata.ModifiableFolder;
097import org.ametys.plugins.repository.metadata.ModifiableResource;
098import org.ametys.plugins.repository.metadata.ModifiableRichText;
099import org.ametys.plugins.repository.metadata.MultilingualString;
100import org.ametys.plugins.repository.metadata.Resource;
101import org.ametys.plugins.repository.metadata.RichText;
102import org.ametys.plugins.workflow.AbstractWorkflowComponent;
103import org.ametys.plugins.workflow.component.CheckRightsCondition;
104import org.ametys.plugins.workflow.support.WorkflowProvider;
105import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
106import org.ametys.runtime.i18n.I18nizableText;
107import org.ametys.runtime.plugin.component.AbstractLogEnabled;
108
109/**
110 * <p>
111 * This component is used to copy a content (either totally or partially).
112 * </p><p>
113 * In this whole file a Map named <em>copyMap</em> is regularly used. This map
114 * provide the name of the metadata to copy as well as some optional parameters.
115 * It has the following form (JSON) :
116 * </p>
117 * <pre>
118 * {
119 *   "$param1": value,
120 *   "metadataA": null,
121 *   "metadataB": {
122 *     "subMetadataB1": null,
123 *     "subMetadataB2": {
124 *       "$param1": value,
125 *       "$param2": value,
126 *       "subSubMetadataB21": {...}
127 *     },
128 *     ...
129 *   }
130 * }
131 * </pre>
132 * <p>
133 * Each metadata that should be copied must be present as a key in the map.
134 * Composite metadata can contains child metadata but as seen on the example the
135 * map must be well structured, it is not a flat map. Parameters in the map must
136 * always start with the reserved character '$', in order to be differentiated
137 * from metadata name.
138 * </p><p>
139 * There are two main entry points for this helper component:
140 * </p>
141 * <ul>
142 * <li>copyContent and editContent are methods that run a dedicated workflow
143 * function (createByCopy or editByCopy) that will later call the
144 * copyMetadataMap function (see below).</li>
145 * <li>copyMetadataMap is the method that is responsible for the recursive copy
146 * of the metadata (by following the copyMap structure). During this process,
147 * underlying content creation can be requested in which case the copyContent
148 * method will be called.</li>
149 * </ul>
150 */
151public class CopyContentMetadataComponent extends AbstractLogEnabled implements Serviceable, Component
152{
153    /** Avalon ROLE. */
154    public static final String ROLE = CopyContentMetadataComponent.class.getName();
155    
156    /** Workflow provider. */
157    protected WorkflowProvider _workflowProvider;
158    
159    /** Ametys object resolver available to subclasses. */
160    protected AmetysObjectResolver _resolver;
161    
162    /** Content type extension point. */
163    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
164    
165    /** Helper for content types */
166    protected ContentTypesHelper _contentTypesHelper;
167    
168    /** The outgoing references extractor */
169    protected OutgoingReferencesExtractor _outgoingReferencesExtractor;
170
171    /** The content helper */
172    protected ContentHelper _contentHelper;
173
174    /** Avalon service manager */
175    protected ServiceManager _manager;
176    
177    @Override
178    public void service(ServiceManager manager) throws ServiceException
179    {
180        _manager = manager;
181        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
182        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
183        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
184        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
185        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
186        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) manager.lookup(OutgoingReferencesExtractor.ROLE);
187    }
188    
189    /**
190     * Copy a content by creating a new content and copying the value of the
191     * metadata of a source content into the new one.
192     * The title of the new content will the one from the base content.
193     * 
194     * @param baseContentId The identifier of the base content
195     * @param copyMap The map of properties as described in
196     *            {@link CopyContentMetadataComponent}. Can be null in which
197     *            case the map will be constructed from a metadataSet.
198     * @param metadataSetName The name of the metadata set to be used to
199     *            construct to copyMap if not provided. This will also be the
200     *            default name for possible inner copies (if not provided as a
201     *            copyMap parameter).
202     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
203     * @param metadataSetType The type of the metadata set to be used to
204     *            construct to copyMap if not provided. This will also be the
205     *            default type for possible inner copies.
206     *            
207     * @param initActionId The workflow action id to use to create the new content 
208     * @param editActionId The workflow action id to use to edit the newly created content
209     *  
210     * @return The copy report containing valuable information about the copy
211     *         and the possible encountered errors.
212     */
213    public CopyReport copyContent(String baseContentId, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editActionId)
214    {
215        return copyContent(baseContentId, null, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, initActionId, editActionId);
216    }
217    
218    /**
219     * Copy a content by creating a new content and copying the value of the
220     * metadata of a source content into the new one.
221     * 
222     * @param baseContentId The identifier of the base content
223     * @param title Desired title for the new content.
224     * @param copyMap The map of properties as described in
225     *            {@link CopyContentMetadataComponent}. Can be null in which
226     *            case the map will be constructed from a metadataSet.
227     * @param metadataSetName The name of the metadata set to be used to
228     *            construct to copyMap if not provided. This will also be the
229     *            default name for possible inner copies (if not provided as a
230     *            copyMap parameter).
231     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
232     * @param metadataSetType The type of the metadata set to be used to
233     *            construct to copyMap if not provided. This will also be the
234     *            default type for possible inner copies.
235     * @param initActionId The init workflow action id for main content only
236     * @param editActionId The workflow action for editing main content only
237     * @return The copy report containing valuable information about the copy
238     *         and the possible encountered errors.
239     */
240    public CopyReport copyContent(String baseContentId, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editActionId)
241    {
242        return copyContent(baseContentId, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, null, initActionId, editActionId);
243    }
244    
245    /**
246     * Create a new content by copy of another. The type of created content can be different of the source content. 
247     * The source and target contents must have a common content type ancestor.
248     * @param baseContentId The id of content to copy
249     * @param title The title of the new created content
250     * @param copyMap The map of metadata to copy. Can be null to copy the whole metadata set.
251     * @param metadataSetName The name of metadata set to copy
252     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
253     * @param metadataSetType The type of metadata set to copy
254     * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content.
255     * @param initActionId The init workflow action id for main content only
256     * @param editActionId The workflow action for editing main content only
257     * @return The copy report
258     */
259    public CopyReport copyContent(String baseContentId, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, int initActionId, int editActionId)
260    {
261        try
262        {
263            Content baseContent = _resolver.resolveById(baseContentId);
264            return copyContent(baseContent, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, targetContentType, initActionId, editActionId);
265        }
266        catch (AmetysRepositoryException e)
267        {
268            getLogger().error("An error has been encountered during the content copy, or the copy is not allowed (base content identifier : " + baseContentId + ").", e);
269            
270            boolean isSimple = true;
271            
272            CopyReport report = new CopyReport(baseContentId, isSimple, metadataSetName, fallbackMetadataSetName, metadataSetType, CopyMode.CREATION);
273            report.notifyContentCopyError();
274            
275            return report;
276        }
277    }
278    
279    /**
280     * Copy a content by creating a new content and copying the value of the
281     * metadata of a source content into the new one.
282     * 
283     * @param baseContent The base content.
284     * @param copyMap The map of properties as described in
285     *            {@link CopyContentMetadataComponent}. Can be null in which
286     *            case the map will be constructed from a metadataSet.
287     * @param metadataSetName The name of the metadata set to be used to
288     *            construct to copyMap if not provided. This will also be the
289     *            default name for possible inner copies (if not provided as a
290     *            copyMap parameter).
291     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
292     * @param metadataSetType The type of the metadata set to be used to
293     *            construct to copyMap if not provided. This will also be the
294     *            default type for possible inner copies.
295     * @return The copy report containing valuable information about the copy
296     *         and the possible encountered errors.
297     */
298    public CopyReport copyContent(Content baseContent, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType)
299    {
300        return copyContent(baseContent, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, getDefaultInitActionId(), getDefaultActionIdForEditingContentReferences());
301    }
302    
303    /**
304     * Copy a content by creating a new content and copying the value of the
305     * metadata of a source content into the new one.
306     * 
307     * @param baseContent The base content.
308     * @param copyMap The map of properties as described in
309     *            {@link CopyContentMetadataComponent}. Can be null in which
310     *            case the map will be constructed from a metadataSet.
311     * @param metadataSetName The name of the metadata set to be used to
312     *            construct to copyMap if not provided. This will also be the
313     *            default name for possible inner copies (if not provided as a
314     *            copyMap parameter).
315     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
316     * @param metadataSetType The type of the metadata set to be used to
317     *            construct to copyMap if not provided. This will also be the
318     *            default type for possible inner copies.
319     * @param initActionId The init workflow action id for main content only
320     * @param editRefActionId The workflow action for editing references
321     * @return The copy report containing valuable information about the copy
322     *         and the possible encountered errors.
323     */
324    public CopyReport copyContent(Content baseContent, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editRefActionId)
325    {
326        return copyContent(baseContent, null, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, initActionId, editRefActionId);
327    }
328    
329    /**
330     * Copy a content by creating a new content and copying the value of the
331     * metadata of a source content into the new one.
332     * 
333     * @param baseContent The base content.
334     * @param title Desired title for the new content.
335     * @param copyMap The map of properties as described in
336     *            {@link CopyContentMetadataComponent}. Can be null in which
337     *            case the map will be constructed from a metadataSet.
338     * @param metadataSetName The name of the metadata set to be used to
339     *            construct to copyMap if not provided. This will also be the
340     *            default name for possible inner copies (if not provided as a
341     *            copyMap parameter).
342     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
343     * @param metadataSetType The type of the metadata set to be used to
344     *            construct to copyMap if not provided. This will also be the
345     *            default type for possible inner copies.
346     * @param initActionId The init workflow action id for main content only
347     * @param editRefActionId The workflow action for editing references
348     * @return The copy report containing valuable information about the copy
349     *         and the possible encountered errors.
350     */
351    public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int initActionId, int editRefActionId)
352    {
353        return copyContent(baseContent, title, copyMap, metadataSetName, metadataSetName, fallbackMetadataSetName, null, initActionId, editRefActionId);
354    }
355    
356    /**
357     * Copy a content by creating a new content and copying the value of the
358     * metadata of a source content into the new one.
359     * 
360     * @param baseContent The base content.
361     * @param title Desired title for the new content.
362     * @param copyMap The map of properties as described in
363     *            {@link CopyContentMetadataComponent}. Can be null in which
364     *            case the map will be constructed from a metadataSet.
365     * @param metadataSetName The name of the metadata set to be used to
366     *            construct to copyMap if not provided. This will also be the
367     *            default name for possible inner copies (if not provided as a
368     *            copyMap parameter).
369     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
370     * @param metadataSetType The type of the metadata set to be used to
371     *            construct to copyMap if not provided. This will also be the
372     *            default type for possible inner copies.
373     * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content.
374     * @param initActionId The init workflow action id for main content only
375     * @param editRefActionId The workflow action for editing references
376     * @return The copy report containing valuable information about the copy
377     *         and the possible encountered errors.
378     */
379    public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, int initActionId, int editRefActionId)
380    {
381        return copyContent(baseContent, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, targetContentType, null, null, initActionId, editRefActionId);
382    }
383    
384    /**
385     * Copy a content by creating a new content and copying the value of the
386     * metadata of a source content into the new one.
387     * 
388     * @param baseContent The base content.
389     * @param title Desired title for the new content.
390     * @param copyMap The map of properties as described in
391     *            {@link CopyContentMetadataComponent}. Can be null in which
392     *            case the map will be constructed from a metadataSet.
393     * @param metadataSetName The name of the metadata set to be used to
394     *            construct to copyMap if not provided. This will also be the
395     *            default name for possible inner copies (if not provided as a
396     *            copyMap parameter).
397     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
398     * @param metadataSetType The type of the metadata set to be used to
399     *            construct to copyMap if not provided. This will also be the
400     *            default type for possible inner copies.
401     * @param parentContentId The target content ID.
402     * @param parentMetadataPath the parent metadata path, if a sub-content is being created.
403     * @param initActionId The init workflow action id for main content only
404     * @param editActionId The workflow action for editing main content only
405     * @return The copy report containing valuable information about the copy
406     *         and the possible encountered errors.
407     */
408    public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String parentContentId, String parentMetadataPath, int initActionId, int editActionId)
409    {
410        return copyContent(baseContent, title, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, null, parentContentId, parentMetadataPath, initActionId, editActionId);
411    }
412    
413    /**
414     * Copy a content by creating a new content and copying the value of the
415     * metadata of a source content into the new one.
416     * 
417     * @param baseContent The base content.
418     * @param title Desired title for the new content.
419     * @param copyMap The map of properties as described in
420     *            {@link CopyContentMetadataComponent}. Can be null in which
421     *            case the map will be constructed from a metadataSet.
422     * @param metadataSetName The name of the metadata set to be used to
423     *            construct to copyMap if not provided. This will also be the
424     *            default name for possible inner copies (if not provided as a
425     *            copyMap parameter).
426     * @param fallbackMetadataSetName The fallback of metadata set to copy. Use for legacy purpose if 'metadataSetName' does not exist.
427     * @param metadataSetType The type of the metadata set to be used to
428     *            construct to copyMap if not provided. This will also be the
429     *            default type for possible inner copies.
430     * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content.
431     * @param parentContentId The target content ID.
432     * @param parentMetadataPath the parent metadata path, if a sub-content is being created.
433     * @param initActionId The init workflow action id for main content only
434     * @param editRefActionId The workflow action for editing references
435     * @return The copy report containing valuable information about the copy
436     *         and the possible encountered errors.
437     */
438    public CopyReport copyContent(Content baseContent, String title, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, String parentContentId, String parentMetadataPath, int initActionId, int editRefActionId)
439    {
440        String baseContentId = baseContent.getId();
441        String auxMetadataSetName = StringUtils.defaultIfEmpty(metadataSetName, "default-edition");
442        String auxFallbackMetadataSetName = StringUtils.defaultIfEmpty(fallbackMetadataSetName, "main");
443        String auxMetadataSetType = StringUtils.defaultIfEmpty(metadataSetType, "edition");
444        CopyReport report = new CopyReport(baseContentId, _contentHelper.getTitle(baseContent), true, auxMetadataSetName, auxFallbackMetadataSetName, auxMetadataSetType, CopyMode.CREATION);
445        
446        try
447        {
448            report.setReferenceTable(_contentHelper.isReferenceTable(baseContent));
449            
450            Map<String, Object> internalCopyMap = copyMap;
451            if (internalCopyMap == null || BooleanUtils.isTrue((Boolean) internalCopyMap.get("$forceBuild")))
452            {
453                internalCopyMap = _buildCopyMap(baseContent, auxMetadataSetName, fallbackMetadataSetName, auxMetadataSetType, targetContentType, internalCopyMap);
454            }
455            
456            // Title metadata must never be copied in case of a content copy, the title is set during the content creation.
457            internalCopyMap.remove("title");
458            
459            Map<String, Object> inputs = _getInputsForCopy(baseContent, title, internalCopyMap, targetContentType, parentContentId, parentMetadataPath, report);
460            String workflowName = _getWorklowName(baseContent, inputs);
461            
462            // Create the content and copy metadata from the base content.
463            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow();
464            workflow.initialize(workflowName, initActionId, inputs);
465            
466            // Manual call to the edit content function to edit the content
467            // references stored by the copy report through the duplication process
468            _runContentReferencesEdition(baseContent, workflow, editRefActionId, true, report);
469            
470            report.notifyContentCopySuccess();
471        }
472        catch (Exception e)
473        {
474            getLogger().error("An error has been encountered during the content copy, or the copy is not allowed (base content identifier : " + baseContentId + ").", e);
475            
476            report.notifyContentCopyError();
477        }
478        
479        return report;
480    }
481    
482    /**
483     * Retrieve the inputs for the copy workflow function.
484     * @param baseContent The content to copy
485     * @param title The title to set
486     * @param copyMap The map with properties to copy
487     * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content.
488     * @param parentContentId The parent content ID, when copying a sub-content.
489     * @param parentMetadataPath The parent metadata path, when copying a sub-content.
490     * @param copyReport The report of the copy
491     * @return The map of inputs.
492     */
493    @SuppressWarnings("unchecked")
494    protected Map<String, Object> _getInputsForCopy(Content baseContent, String title, Map<String, Object> copyMap, String targetContentType, String parentContentId, String parentMetadataPath, CopyReport copyReport)
495    {
496        Map<String, Object> inputs = new HashMap<>();
497        
498        // Add copy map inputs
499        Map<String, Object> copyMapInputs = (Map<String, Object>) copyMap.get("$inputs");
500        if (copyMapInputs != null)
501        {
502            inputs.putAll(copyMapInputs);
503        }
504        
505        inputs.put(CreateContentByCopyFunction.BASE_CONTENT_KEY, baseContent);
506        inputs.put(CreateContentByCopyFunction.COPY_MAP_KEY, copyMap);
507        inputs.put(CreateContentByCopyFunction.COPY_REPORT_KEY, copyReport);
508        
509        if (StringUtils.isNotBlank(parentContentId) && StringUtils.isNotBlank(parentMetadataPath))
510        {
511            // Provide the parent content ID and metadata path to the CreateContentFunction (removing the leading slash).
512            inputs.put(CreateContentFunction.PARENT_CONTENT_ID_KEY, parentContentId);
513            inputs.put(CreateContentFunction.PARENT_CONTENT_METADATA_PATH_KEY, StringUtils.stripStart(parentMetadataPath, "/"));
514        }
515        
516        if (StringUtils.isNoneBlank(title))
517        {
518            inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, title);
519        }
520
521        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
522        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
523        
524        if (targetContentType != null)
525        {
526            inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {targetContentType});
527        }
528        
529        return inputs;
530    }
531    
532    /**
533     * Retrieve the workflow name of a content.
534     * @param content The content to consider
535     * @param inputs The inputs that will be provided to the workflow function
536     * @return The name of the workflow.
537     * @throws IllegalArgumentException if the content is not workflow aware.
538     */
539    protected String _getWorklowName(Content content, Map<String, Object> inputs) throws IllegalArgumentException
540    {
541        String workflowName = null;
542        
543        if (content instanceof WorkflowAwareContent)
544        {
545            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
546            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
547            workflowName = workflow.getWorkflowName(waContent.getWorkflowId());
548        }
549        
550        if (workflowName == null)
551        {
552            String errorMsg = "Unable to retrieve the workflow name for the content with identifier '" + content.getId() + "'.";
553            
554            getLogger().error(errorMsg);
555            throw new IllegalArgumentException(errorMsg);
556        }
557        
558        return workflowName;
559    }
560    
561    /**
562     * Build the copy map from given content and metadata set.
563     * @param baseContent the content to copy
564     * @param metadataSetName Name of the metadata set.
565     * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found
566     * @param metadataSetType Type of the metadata set.
567     * @return A map with properties to copy.
568     */
569    public Map<String, Object> buildCopyMap (Content baseContent, String metadataSetName, String fallbackMetadataSetName, String metadataSetType)
570    {
571        return _buildCopyMap(baseContent, metadataSetName, fallbackMetadataSetName, metadataSetType, new HashMap<String, Object>());
572    }
573    
574    /**
575     * Build the copy map from a metadata set.
576     * @param baseContent the content to copy
577     * @param metadataSetName Name of the metadata set.
578     * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found
579     * @param metadataSetType Type of the metadata set.
580     * @param baseCopyMap The copy map being constructed
581     * @return The copy map.
582     */
583    protected Map<String, Object> _buildCopyMap(Content baseContent, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, Map<String, Object> baseCopyMap)
584    {
585        return _buildCopyMap(baseContent, metadataSetName, fallbackMetadataSetName, metadataSetType, null, baseCopyMap);
586    }
587    
588    /**
589     * Build the copy map from a metadata set.
590     * @param baseContent The content to be copied
591     * @param metadataSetName Name of the metadata set to be copied.
592     * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found
593     * @param metadataSetType Type of the metadata set to be copied (view or edition).
594     * @param targetContentType The content type of content to create. Can be null.
595     * @param baseCopyMap The copy map being constructed
596     * @return The copy map.
597     */
598    protected Map<String, Object> _buildCopyMap(Content baseContent, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, String targetContentType, Map<String, Object> baseCopyMap)
599    {
600        boolean isEditionMetadataSet = !"view".equals(metadataSetType);
601        
602        MetadataSet metadataSet = null;
603        if (targetContentType != null)
604        {
605            Set<String> types = new HashSet<>();
606            Collections.addAll(types, baseContent.getTypes());
607            types.add(targetContentType);
608            Set<String> commonAncestors = _contentTypesHelper.getCommonAncestors(types);
609            
610            metadataSet = _contentTypesHelper.getMetadataSetWithFallback(metadataSetName, fallbackMetadataSetName, commonAncestors.toArray(new String[commonAncestors.size()]), new String[0], isEditionMetadataSet);
611        }
612        else
613        {
614            metadataSet = _contentTypesHelper.getMetadataSetWithFallback(metadataSetName, fallbackMetadataSetName, baseContent.getTypes(), baseContent.getMixinTypes(), isEditionMetadataSet);
615        }
616        
617        return _buildCopyMap(metadataSet, baseCopyMap);
618    }
619    
620    /**
621     * Recursive auxiliary function used to build the copy map.
622     * @param metadataSetElement The current metadata set element 
623     * @param copyMap The copy map being constructed or null
624     * @return The copy map corresponding to this metadata set element.
625     */
626    protected Map<String, Object> _buildCopyMap(AbstractMetadataSetElement metadataSetElement, Map<String, Object> copyMap)
627    {
628        Map<String, Object> map = copyMap;
629        
630        for (AbstractMetadataSetElement metadataElement : metadataSetElement.getElements())
631        {
632            if (metadataElement instanceof MetadataDefinitionReference)
633            {
634                MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) metadataElement;
635                String name = metadataDefRef.getMetadataName();
636                
637                map = map != null ? map : new HashMap<>();
638                map.put(name, _buildCopyMap(metadataDefRef, null));
639            }
640            else
641            {
642                map = map != null ? map : new HashMap<>();
643                map.putAll(_buildCopyMap(metadataElement, map));
644            }
645        }
646        
647        return map;
648    }
649    
650    /**
651     * Edit a content by copying the value of the
652     * metadata of a source content into a target content.
653     * 
654     * @param baseContentId The identifier of the base content
655     * @param targetContentId The identifier of the target content
656     * @param copyMap The map of properties as described in
657     *            {@link CopyContentMetadataComponent}. Can be null in which
658     *            case the map will be constructed from a metadataSet.
659     * @param metadataSetName The name of the metadata set to be used to
660     *            construct to copyMap if not provided. This will also be the
661     *            default name for possible inner copies (if not provided as a
662     *            copyMap parameter).
663     * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found
664     * @param metadataSetType The type of the metadata set to be used to
665     *            construct to copyMap if not provided. This will also be the
666     *            default type for possible inner copies.
667     * @return The copy report containing valuable information about the copy
668     *         and the possible encountered errors.
669     */
670    public CopyReport editContent(String baseContentId, String targetContentId, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType)
671    {
672        return editContent(baseContentId, targetContentId, copyMap, metadataSetName, fallbackMetadataSetName, metadataSetType, getDefaultActionIdForContentEdition(), getDefaultActionIdForEditingContentReferences());
673    }
674    
675    /**
676     * Edit a content by copying the value of the
677     * metadata of a source content into a target content.
678     * 
679     * @param baseContentId The identifier of the base content
680     * @param targetContentId The identifier of the target content
681     * @param copyMap The map of properties as described in
682     *            {@link CopyContentMetadataComponent}. Can be null in which
683     *            case the map will be constructed from a metadataSet.
684     * @param metadataSetName The name of the metadata set to be used to
685     *            construct to copyMap if not provided. This will also be the
686     *            default name for possible inner copies (if not provided as a
687     *            copyMap parameter).
688     * @param fallbackMetadataSetName The fallback metadata set name if 'metadataSetName' is not found
689     * @param metadataSetType The type of the metadata set to be used to
690     *            construct to copyMap if not provided. This will also be the
691     *            default type for possible inner copies.
692     * @param actionId the edit workflow action id
693     * @param editRefActionId the workflow action id for editing references
694     * @return The copy report containing valuable information about the copy
695     *         and the possible encountered errors.
696     */
697    public CopyReport editContent(String baseContentId, String targetContentId, Map<String, Object> copyMap, String metadataSetName, String fallbackMetadataSetName, String metadataSetType, int actionId, int editRefActionId)
698    {
699        Content baseContent = null;
700        CopyReport report = null;
701        
702        String auxMetadataSetName = StringUtils.defaultIfEmpty(metadataSetName, "default-edition");
703        String auxFallbackMetadataSetName = StringUtils.defaultIfEmpty(fallbackMetadataSetName, "main");
704        String auxMetadataSetType = StringUtils.defaultIfEmpty(metadataSetType, "edition");
705        
706        try
707        {
708            baseContent = _resolver.resolveById(baseContentId);
709            report = new CopyReport(baseContentId, _contentHelper.getTitle(baseContent), _contentHelper.isReferenceTable(baseContent), auxMetadataSetName, fallbackMetadataSetName, auxMetadataSetType, CopyMode.EDITION);
710            
711            Map<String, Object> internalCopyMap = copyMap;
712            if (internalCopyMap == null || BooleanUtils.isTrue((Boolean) internalCopyMap.get("$forceBuild")))
713            {
714                internalCopyMap = _buildCopyMap(baseContent, auxMetadataSetName, auxFallbackMetadataSetName, auxMetadataSetType, internalCopyMap);
715            }
716            
717            WorkflowAwareContent targetContent = _retrieveTargetContent(targetContentId);
718            Map<String, Object> inputs = _getInputsForEdition(baseContent, targetContent, internalCopyMap, report);
719            
720            // Edit the content by copying metadata from the base content.
721            // This is done in a workflow function.
722            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(targetContent);
723            workflow.doAction(targetContent.getWorkflowId(), actionId, inputs);
724            
725            // Manual call to the edit content function to edit the content
726            // references stored by the copy report through the duplication process
727            _runContentReferencesEdition(baseContent, workflow, editRefActionId, false, report);
728            
729            report.notifyContentCopySuccess();
730        }
731        catch (Exception e)
732        {
733            getLogger().error(
734                    "An error has been encountered during the content edition, or the edition is not allowed (base content identifier : " + baseContentId
735                            + ", target content identifier : " + targetContentId + ").", e);
736            
737            if (report != null)
738            {
739                report.notifyContentCopyError();
740            }
741            else
742            {
743                report = new CopyReport(baseContentId, _contentHelper.isReferenceTable(baseContent), auxMetadataSetName, fallbackMetadataSetName, auxMetadataSetType, CopyMode.EDITION);
744                report.notifyContentCopyError();
745            }
746        }
747        
748        return report;
749    }
750    
751    /**
752     * Retrieve the target content from its id.
753     * Also ensure that it is a workflow aware content.
754     * @param targetContentId The target content identifer.
755     * @return the retrieved workflow aware content
756     * @throws IllegalArgumentException if the content is not workflow aware.
757     */
758    protected WorkflowAwareContent _retrieveTargetContent(String targetContentId) throws IllegalArgumentException
759    {
760        Content content = _resolver.resolveById(targetContentId);
761        
762        if (!(content instanceof WorkflowAwareContent))
763        {
764            throw new IllegalArgumentException("Content with identifier '" + targetContentId + "' is not workflow aware.");
765        }
766        
767        return (WorkflowAwareContent) content;
768    }
769    
770    /**
771     * Retrieve the inputs for the edition workflow function.
772     * @param baseContent The content to copy
773     * @param targetContent The target of the copy
774     * @param copyMap The properties to copy
775     * @param copyReport The report of the copy
776     * @return The map of inputs.
777     */
778    protected Map<String, Object> _getInputsForEdition(Content baseContent, WorkflowAwareContent targetContent, Map<String, Object> copyMap, CopyReport copyReport)
779    {
780        Map<String, Object> inputs = new HashMap<>();
781        
782        // Add copy map inputs
783        @SuppressWarnings("unchecked")
784        Map<String, Object> copyMapInputs = (Map<String, Object>) copyMap.get("$inputs");
785        if (copyMapInputs != null)
786        {
787            inputs.putAll(copyMapInputs);
788        }
789        
790        inputs.put(EditContentByCopyFunction.BASE_CONTENT_KEY, baseContent);
791        inputs.put(EditContentByCopyFunction.COPY_MAP_KEY, copyMap);
792        inputs.put(EditContentByCopyFunction.COPY_REPORT_KEY, copyReport);
793        
794        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, targetContent);
795        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
796        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
797        
798        return inputs;
799    }
800
801    
802    /* *******************************
803     *                               *
804     *      Copy of the metadata     *
805     *                               *
806     * ******************************/
807    
808    /**
809     * Copy the specified set of metadata from a base content to a target content by iterating over the copyMap.
810     * 
811     * @param baseContent The original content
812     * @param targetContent The copy content
813     * @param copyMap The properties to copy
814     * @param copyReport The copy report being populated during the copy.
815     */
816    @SuppressWarnings("unchecked")
817    public void copyMetadataMap(Content baseContent, ModifiableContent targetContent, Map<String, Object> copyMap, CopyReport copyReport)
818    {
819        copyReport.notifyContentCreation(targetContent.getId(), _contentHelper.getTitle(targetContent), _contentHelper.isReferenceTable(baseContent));
820        
821        Map<String, Object> innerCopyMapInputs =  copyMap != null ? (Map<String, Object>) copyMap.get("$inputs") : null;
822        _copyMetadataMap(baseContent, baseContent.getMetadataHolder(), targetContent.getMetadataHolder(), "", null, copyMap, innerCopyMapInputs, copyReport);
823        
824        copyReport.setTargetContentTitle(_contentHelper.getTitle(targetContent));
825        
826        _updateRichTextMetadata(baseContent, targetContent, copyReport);
827        _extractOutgoingReferences(targetContent);
828    }
829    
830    /**
831     * Copy the specified set of metadata from a base composite metadata to a target composite metadata by iterating over the copyMap.
832     * @param baseContent The original content
833     * @param baseMetaHolder The metadata holder of the baseContent
834     * @param targetMetaHolder The metadata holder of the target content
835     * @param metaPrefix The metadata prefix.
836     * @param parentMetadataDefinition The parent metadata definition. Can be null
837     * @param copyMap The properties to copy
838     * @param innerCopyMapInputs The properties to copy for sub objects
839     * @param copyReport The copy report being populated during the copy.
840     */
841    @SuppressWarnings("unchecked")
842    protected void _copyMetadataMap(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metaPrefix, MetadataDefinition parentMetadataDefinition, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport)
843    {
844        if (copyMap == null)
845        {
846            return;
847        }
848        
849        for (String metadataName : copyMap.keySet())
850        {
851            // Ignore key starting with the $ (denotes a parameter)
852            if (StringUtils.startsWith(metadataName, "$"))
853            {
854                continue;
855            }
856            
857            MetadataDefinition metadataDefinition = null;
858            try
859            {
860                metadataDefinition = _getMetadataDefinition(baseContent, parentMetadataDefinition, metadataName);
861                if (metadataDefinition != null)
862                {
863                    copyMetadata(baseContent, baseMetaHolder, targetMetaHolder, metadataDefinition, metaPrefix, (Map<String, Object>) copyMap.get(metadataName), innerCopyMapInputs, copyReport);
864                }
865            }
866            catch (Exception e)
867            {
868                _reportMetadataException(baseMetaHolder, targetMetaHolder, metadataName, metadataDefinition, copyReport, e);
869            }
870        }
871    }
872    
873    /**
874     * Retrieves a {@link MetadataDefinition} through its
875     * parent definition or the {@link ContentType} of the
876     * current content if at the root level of the metadataset.
877     * @param content The content
878     * @param metadataDefinition The parent metadata definition. Can be null.
879     * @param metadataName The metadata name
880     * @return The retrieved metadata defintion or null if not found
881     */
882    protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition metadataDefinition, String metadataName)
883    {
884        if (metadataDefinition != null)
885        {
886            return metadataDefinition.getMetadataDefinition(metadataName);
887        }
888        else
889        {
890            return _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
891        }
892    }
893    
894    /**
895     * Add a metadata exception to the report.
896     * @param baseMetaHolder The metadata holder to copy
897     * @param targetMetaHolder The metadata holder where to copy
898     * @param metadataName The metadata to copy
899     * @param metadataDefinition The associated definition
900     * @param copyReport The report of the copy
901     * @param e The raised exception.
902     */
903    protected void _reportMetadataException(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, MetadataDefinition metadataDefinition, CopyReport copyReport, Exception e)
904    {
905        String type = metadataDefinition != null ? metadataDefinition.getType().toString() : null;
906        
907        String errorMsg = "Copy of the metadata '" + metadataName + "' of type '" + (type != null ? type : "unknown type") + "' has failed.";
908        getLogger().error(errorMsg, e);
909        
910        copyReport.notifyMetadataCopyError(metadataDefinition != null ? metadataDefinition.getLabel() : new I18nizableText(metadataName));
911    }
912    
913    /**
914     * Copy the specified metadata from a base composite metadata to a target composite metadata.
915     * @param baseContent The original content
916     * @param baseMetaHolder The metadata holder to copy
917     * @param targetMetaHolder The metadata holder where to copy
918     * @param metadataDefinition The associated definition
919     * @param metaPrefix The metadata prefix.
920     * @param copyMap The properties to copy
921     * @param innerCopyMapInputs The properties to copy for sub objects
922     * @param copyReport The report of the copy
923     */
924    public void copyMetadata(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metaPrefix, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport)
925    {
926        String metadataName = metadataDefinition.getName();
927        String metadataPath = metaPrefix + "/" + metadataName;
928        
929        switch (metadataDefinition.getType())
930        {
931            case GEOCODE:
932                copyGeocodeMetadata(baseMetaHolder, targetMetaHolder, metadataName);
933                break;
934            case USER:
935                copyUserMetadata(baseMetaHolder, targetMetaHolder, metadataName);
936                break;
937            case MULTILINGUAL_STRING:
938                copyMultilingualStringMetadata(baseMetaHolder, targetMetaHolder, metadataName);
939                break;
940            case REFERENCE:
941                copyReferenceMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName);
942                break;
943            case BINARY:
944                copyBinaryMetadata(baseMetaHolder, targetMetaHolder, metadataName);
945                break;
946            case FILE:
947                copyFileMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName);
948                break;
949            case RICH_TEXT:
950                copyRichTextMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName, copyReport);
951                break;
952            case CONTENT:
953                copyContentReferenceMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataPath, copyMap, innerCopyMapInputs, copyReport);
954                break;
955            case SUB_CONTENT:
956                copySubContentMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataPath, copyMap, innerCopyMapInputs, copyReport);
957                break;
958            case COMPOSITE:
959                copyCompositeMetadata(baseContent, baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, innerCopyMapInputs, copyReport);
960                break;
961            default:
962                copyBasicMetadata(baseMetaHolder, targetMetaHolder, metadataDefinition, metadataName);
963                break;
964        }
965    }
966    
967    /**
968     * Copy a 'basic' metadata.
969     * This is used to copy metadata of type :
970     * {@link MetadataType#STRING}, {@link MetadataType#DATE}, {@link MetadataType#DATETIME},
971     * {@link MetadataType#DOUBLE}, {@link MetadataType#LONG}, {@link MetadataType#BOOLEAN},
972     * {@link MetadataType#USER}
973     * 
974     * @param baseMetaHolder The metadata holder to copy
975     * @param targetMetaHolder The metadata holder where to copy
976     * @param metadataDef The metadata definition 
977     * @param metadataName The metadata name 
978     */
979    public void copyBasicMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName)
980    {
981        if (baseMetaHolder.hasMetadata(metadataName))
982        {
983            switch (metadataDef.getType())
984            {
985                case DATE:
986                case DATETIME:
987                    _setDateMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
988                    break;
989                case DOUBLE:
990                    _setDoubleMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
991                    break;
992                case LONG:
993                    _setLongMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
994                    break;
995                case BOOLEAN:
996                    _setBooleanMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
997                    break;
998                case STRING:
999                case USER:
1000                default:
1001                    _setStringMetadata(baseMetaHolder, targetMetaHolder, metadataName, metadataDef.isMultiple());
1002                    break;
1003            }
1004        }
1005        else if (targetMetaHolder.hasMetadata(metadataName))
1006        {
1007            targetMetaHolder.removeMetadata(metadataName);
1008        }
1009    }
1010    
1011    /**
1012     * Set a metadata of type string from a base composite metadata to a target composite metadata.
1013     * @param baseMetaHolder The metadata holder to copy
1014     * @param targetMetaHolder The metadata holder where to copy
1015     * @param metadataName The metadata to consider
1016     * @param isMultiple Is the metadata multiple
1017     */
1018    protected void _setStringMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple)
1019    {
1020        if (isMultiple)
1021        {
1022            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getStringArray(metadataName));
1023        }
1024        else
1025        {
1026            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getString(metadataName));
1027        }
1028    }
1029    
1030    /**
1031     * Set a metadata of type date/datetime from a base composite metadata to a target composite metadata.
1032     * @param baseMetaHolder The metadata holder to copy
1033     * @param targetMetaHolder The metadata holder where to copy
1034     * @param metadataName The metadata to consider
1035     * @param isMultiple Is the metadata multiple
1036     */
1037    protected void _setDateMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple)
1038    {
1039        if (isMultiple)
1040        {
1041            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDateArray(metadataName));
1042        }
1043        else
1044        {
1045            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDate(metadataName));
1046        }
1047    }
1048    
1049    /**
1050     * Set a metadata of type double from a base composite metadata to a target composite metadata.
1051     * @param baseMetaHolder The metadata holder to copy
1052     * @param targetMetaHolder The metadata holder where to copy
1053     * @param metadataName The metadata to consider
1054     * @param isMultiple Is the metadata multiple
1055     */
1056    protected void _setDoubleMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple)
1057    {
1058        if (isMultiple)
1059        {
1060            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDoubleArray(metadataName));
1061        }
1062        else
1063        {
1064            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getDouble(metadataName));
1065        }
1066    }
1067    
1068    /**
1069     * Set a metadata of type long from a base composite metadata to a target composite metadata.
1070     * @param baseMetaHolder The metadata holder to copy
1071     * @param targetMetaHolder The metadata holder where to copy
1072     * @param metadataName The metadata to consider
1073     * @param isMultiple Is the metadata multiple
1074     */
1075    protected void _setLongMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple)
1076    {
1077        if (isMultiple)
1078        {
1079            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getLongArray(metadataName));
1080        }
1081        else
1082        {
1083            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getLong(metadataName));
1084        }
1085    }
1086    
1087    /**
1088     * Set a metadata of type boolean from a base composite metadata to a target composite metadata.
1089     * @param baseMetaHolder The metadata holder to copy
1090     * @param targetMetaHolder The metadata holder where to copy
1091     * @param metadataName The metadata to consider
1092     * @param isMultiple Is the metadata multiple
1093     */
1094    protected void _setBooleanMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, Boolean isMultiple)
1095    {
1096        if (isMultiple)
1097        {
1098            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getBooleanArray(metadataName));
1099        }
1100        else
1101        {
1102            targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getBoolean(metadataName));
1103        }
1104    }
1105
1106    /**
1107     * Duplicate a metadata of type {@link MetadataType#GEOCODE}.
1108     * @param baseMetaHolder The metadata holder to copy
1109     * @param targetMetaHolder The metadata holder where to copy
1110     * @param metadataName The metadata to consider
1111     */
1112    public void copyGeocodeMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName)
1113    {
1114        if (baseMetaHolder.hasMetadata(metadataName))
1115        {
1116            CompositeMetadata baseGeoCode = baseMetaHolder.getCompositeMetadata(metadataName);
1117            ModifiableCompositeMetadata targetGeoCode = targetMetaHolder.getCompositeMetadata(metadataName, true);
1118            
1119            targetGeoCode.setMetadata("longitude", baseGeoCode.getDouble("longitude"));
1120            targetGeoCode.setMetadata("latitude", baseGeoCode.getDouble("latitude"));
1121        }
1122        else if (targetMetaHolder.hasMetadata(metadataName))
1123        {
1124            targetMetaHolder.removeMetadata(metadataName);
1125        }
1126    }
1127    
1128    /**
1129     * Duplicate a metadata of type {@link MetadataType#MULTILINGUAL_STRING}.
1130     * @param baseMetaHolder The metadata holder to copy
1131     * @param targetMetaHolder The metadata holder where to copy
1132     * @param metadataName The metadata to consider
1133     */
1134    public void copyMultilingualStringMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName)
1135    {
1136        if (baseMetaHolder.hasMetadata(metadataName))
1137        {
1138            MultilingualString baseMultilingualString = baseMetaHolder.getMultilingualString(metadataName);
1139            for (Locale locale : baseMultilingualString.getLocales())
1140            {
1141                targetMetaHolder.setMetadata(metadataName, baseMultilingualString.getValue(locale), locale);
1142            }
1143        }
1144        else if (targetMetaHolder.hasMetadata(metadataName))
1145        {
1146            targetMetaHolder.removeMetadata(metadataName);
1147        }
1148    }
1149    
1150    /**
1151     * Duplicate a metadata of type {@link MetadataType#USER}.
1152     * @param baseMetaHolder The metadata holder to copy
1153     * @param targetMetaHolder The metadata holder where to copy
1154     * @param metadataName The metadata to consider
1155     */
1156    public void copyUserMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName)
1157    {
1158        if (baseMetaHolder.hasMetadata(metadataName))
1159        {
1160            CompositeMetadata baseUser = baseMetaHolder.getCompositeMetadata(metadataName);
1161            ModifiableCompositeMetadata targetUser = targetMetaHolder.getCompositeMetadata(metadataName, true);
1162            
1163            targetUser.setMetadata("login", baseUser.getString("login"));
1164            targetUser.setMetadata("population", baseUser.getString("population"));
1165        }
1166        else if (targetMetaHolder.hasMetadata(metadataName))
1167        {
1168            targetMetaHolder.removeMetadata(metadataName);
1169        }
1170    }
1171    
1172    /**
1173     * Duplicate a metadata of type {@link MetadataType#REFERENCE}.
1174     * @param baseMetaHolder The metadata holder to copy
1175     * @param targetMetaHolder The metadata holder where to copy
1176     * @param metadataName The metadata to consider
1177     * @param metadataDefinition The associated definition 
1178     */
1179    public void copyReferenceMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metadataName)
1180    {
1181        if (baseMetaHolder.hasMetadata(metadataName))
1182        {
1183            CompositeMetadata baseReferenceMeta = baseMetaHolder.getCompositeMetadata(metadataName);
1184            ModifiableCompositeMetadata targetReferenceMeta = targetMetaHolder.getCompositeMetadata(metadataName, true);
1185            
1186            targetReferenceMeta.setMetadata("type", baseReferenceMeta.getString("type"));
1187            targetReferenceMeta.setMetadata("value", baseReferenceMeta.getString("value"));
1188        }
1189        else if (targetMetaHolder.hasMetadata(metadataName))
1190        {
1191            targetMetaHolder.removeMetadata(metadataName);
1192        }
1193    }
1194    
1195    /**
1196     * Duplicate a metadata of type {@link MetadataType#BINARY}.
1197     * @param baseMetaHolder The metadata holder to copy
1198     * @param targetMetaHolder The metadata holder where to copy
1199     * @param metadataName The metadata to consider
1200     */
1201    public void copyBinaryMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName)
1202    {
1203        if (baseMetaHolder.hasMetadata(metadataName))
1204        {
1205            BinaryMetadata baseBinaryMetadata = baseMetaHolder.getBinaryMetadata(metadataName);
1206            ModifiableBinaryMetadata targetBinaryMetadata = targetMetaHolder.getBinaryMetadata(metadataName, true);
1207            
1208            targetBinaryMetadata.setFilename(baseBinaryMetadata.getFilename());
1209            targetBinaryMetadata.setMimeType(baseBinaryMetadata.getMimeType());
1210            targetBinaryMetadata.setLastModified(baseBinaryMetadata.getLastModified());
1211            targetBinaryMetadata.setEncoding(baseBinaryMetadata.getEncoding());
1212            targetBinaryMetadata.setInputStream(baseBinaryMetadata.getInputStream());
1213        }
1214        else if (targetMetaHolder.hasMetadata(metadataName))
1215        {
1216            targetMetaHolder.removeMetadata(metadataName);
1217        }
1218    }
1219    
1220    /**
1221     * Copy a file metadata
1222     * @param baseMetaHolder The copied composite metadata
1223     * @param targetMetaHolder The target composite metadata
1224     * @param metadataDef The metadata definition
1225     * @param metadataName The metadata name
1226     */
1227    public void copyFileMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName)
1228    {
1229        if (baseMetaHolder.hasMetadata(metadataName))
1230        {
1231            // Could be a binary or a string.
1232            if (CompositeMetadata.MetadataType.BINARY.equals(baseMetaHolder.getType(metadataName)))
1233            {
1234                copyBinaryMetadata(baseMetaHolder, targetMetaHolder, metadataName);
1235            }
1236            else
1237            {
1238                targetMetaHolder.setMetadata(metadataName, baseMetaHolder.getString(metadataName));
1239                copyBasicMetadata(baseMetaHolder, targetMetaHolder, metadataDef, metadataName);
1240            }
1241        }
1242        else if (targetMetaHolder.hasMetadata(metadataName))
1243        {
1244            targetMetaHolder.removeMetadata(metadataName);
1245        }
1246    }
1247    
1248    /**
1249     * Copy a rich-text metadata
1250     * @param baseMetaHolder The copied composite metadata
1251     * @param targetMetaHolder The target composite metadata
1252     * @param metadataName The metadata name
1253     * @param metadataDef The metadata definition
1254     * @param copyReport The copy report
1255     */
1256    protected void copyRichTextMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDef, String metadataName, CopyReport copyReport)
1257    {
1258        if (baseMetaHolder.hasMetadata(metadataName))
1259        {
1260            RichText baseRichText = baseMetaHolder.getRichText(metadataName);
1261            ModifiableRichText targetRichText = targetMetaHolder.getRichText(metadataName, true);
1262            
1263            // Notify the report that a rich text has been copied.
1264            copyReport.addRichText(targetRichText, metadataDef);
1265            
1266            targetRichText.setEncoding(baseRichText.getEncoding());
1267            targetRichText.setMimeType(baseRichText.getMimeType());
1268            targetRichText.setLastModified(baseRichText.getLastModified());
1269            targetRichText.setInputStream(baseRichText.getInputStream());
1270            
1271            // Copy additional data
1272            _copyFolder(baseRichText.getAdditionalDataFolder(), targetRichText.getAdditionalDataFolder());
1273        }
1274        else if (targetMetaHolder.hasMetadata(metadataName))
1275        {
1276            targetMetaHolder.removeMetadata(metadataName);
1277        }
1278    }
1279    
1280    /**
1281     * Folder copy during the copy of a rich text metadata.
1282     * @param baseFolder The folder to copy
1283     * @param targetFolder The folder where to copy
1284     */
1285    protected void _copyFolder(Folder baseFolder, ModifiableFolder targetFolder)
1286    {
1287        // Files and folders removal
1288        targetFolder.removeAll();
1289        
1290        // Copy folders
1291        Collection< ? extends Folder> baseSubFolders = baseFolder.getFolders();
1292        for (Folder baseSubFolder : baseSubFolders)
1293        {
1294            ModifiableFolder targetSubFolder = targetFolder.addFolder(baseSubFolder.getName());
1295            _copyFolder(baseSubFolder, targetSubFolder);
1296        }
1297        
1298        // Copy files
1299        Collection< ? extends File> baseFiles = baseFolder.getFiles();
1300        for (File baseFile : baseFiles)
1301        {
1302            _copyFile(baseFile, targetFolder);
1303        }
1304    }
1305    
1306    /**
1307     * File copy during the copy of a folder.
1308     * @param baseFile The file to copy
1309     * @param targetFolder The folder where to copy
1310     */
1311    protected void _copyFile(File baseFile, ModifiableFolder targetFolder)
1312    {
1313        ModifiableFile file = targetFolder.addFile(baseFile.getName());
1314        
1315        Resource baseResource = baseFile.getResource();
1316        ModifiableResource targetResource = file.getResource();
1317        
1318        targetResource.setLastModified(baseResource.getLastModified());
1319        targetResource.setMimeType(baseResource.getMimeType());
1320        targetResource.setEncoding(baseResource.getEncoding());
1321        targetResource.setInputStream(baseResource.getInputStream());
1322    }
1323    
1324    /**
1325     * Duplicate a metadata of type {@link MetadataType#CONTENT}.
1326     * If the copy map has a '$mode' parameter set to 'create', a new content will be created for each referenced content in the base metadata.
1327     * The referenced contents are created if needed but content references are not set here.
1328     * It should be done manually as done in {@link #_runContentReferencesEdition}
1329     * @param baseMetaHolder The metadata holder of the content to copy
1330     * @param targetMetaHolder The metadata holder of the target content
1331     * @param metadataDefinition The definition of the metadata
1332     * @param metadataPath The content metadata path.
1333     * @param copyMap The properties to copy
1334     * @param innerCopyMapInputs The properties of sub objects
1335     * @param copyReport The report of the copy
1336     */
1337    public void copyContentReferenceMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, MetadataDefinition metadataDefinition, String metadataPath, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport)
1338    {
1339        String metadataName = metadataDefinition.getName();
1340        
1341        if (baseMetaHolder.hasMetadata(metadataName))
1342        {
1343            String[] baseContentIds = baseMetaHolder.getStringArray(metadataName);
1344            String[] targetContentIds = baseContentIds;
1345            
1346            if (copyMap != null && StringUtils.equals((String) copyMap.get("$mode"), "create"))
1347            {
1348                targetContentIds = _copyReferencedContents(baseContentIds, copyMap, innerCopyMapInputs, copyReport);
1349            }
1350            
1351            if (targetContentIds.length > 0)
1352            {
1353                Object values = metadataDefinition.isMultiple() ? Arrays.asList(targetContentIds) : targetContentIds[0];
1354                copyReport.addContentReferenceValues(metadataPath, values);
1355            }
1356            else if (targetMetaHolder.hasMetadata(metadataName))
1357            {
1358                Object values = metadataDefinition.isMultiple() ? Collections.EMPTY_LIST : null;
1359                copyReport.addContentReferenceValues(metadataPath, values);
1360            }
1361        }
1362        else if (targetMetaHolder.hasMetadata(metadataName))
1363        {
1364            Object values = metadataDefinition.isMultiple() ? Collections.EMPTY_LIST : null;
1365            copyReport.addContentReferenceValues(metadataPath, values);
1366        }
1367    }
1368    
1369    /**
1370     * Duplicate base contents by creating new contents. If a specific metadata
1371     * set must be used, a '$metadataSetName' parameter must be specified. If
1372     * the copyMap for each inner duplication is already present in the current
1373     * copyMap, a '$loaded' parameter must be set to true, if not the copyMap
1374     * will be constructed from the requested metadata set.
1375     * @param baseContentIds The ids of contents to copy
1376     * @param copyMap The properties to copy
1377     * @param innerCopyMapInputs The properties of sub objects
1378     * @param copyReport The report of the copy
1379     * @return The array of identifiers of the new contents.
1380     */
1381    protected String[] _copyReferencedContents(String[] baseContentIds, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport)
1382    {
1383        String defaultMetadataSetName = StringUtils.defaultString((String) copyMap.get("$metadataSetName"), copyReport._metadataSetName);
1384        String defaultFallbackMetadataSetName = StringUtils.defaultString((String) copyMap.get("$fallbackMetadataSetName"), copyReport._fallbackMetadataSetName);
1385        String defaultMetadataSetType = copyReport._metadataSetType;
1386        
1387        Map<String, Object> innerCopyMap = _handleInnerCopyMap(copyMap, innerCopyMapInputs);
1388        List<String> targetContentIds = new ArrayList<>();
1389        
1390        for (String baseContentId : baseContentIds)
1391        {
1392            CopyReport innerReport = copyContent(baseContentId, innerCopyMap, defaultMetadataSetName, defaultFallbackMetadataSetName, defaultMetadataSetType, getDefaultInitActionId(), getDefaultActionIdForEditingContentReferences());
1393            
1394            if (innerReport._state == CopyState.SUCCESS)
1395            {
1396                targetContentIds.add(innerReport._targetContentId);
1397            }
1398            
1399            copyReport.addReport(innerReport);
1400        }
1401        
1402        return targetContentIds.toArray(new String[targetContentIds.size()]);
1403    }
1404    
1405    /**
1406     * Handle the creation of the inner copy map given the current context.
1407     * The inner copy map will act as the copy map for underlying content copies.
1408     * @param copyMap The properties to copy
1409     * @param innerCopyMapInputs The properties of sub objects
1410     * @return The inner copy map
1411     */
1412    protected Map<String, Object> _handleInnerCopyMap(Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs)
1413    {
1414        // If the copy map does not contain the data for the inner duplications,
1415        // a null copy map should be passed to force the copy process to build
1416        // it.
1417        // If inner inputs for the copy map are supplied, they will be added to
1418        // the copy map.
1419        // In case of a null copy map, the copy map will no be null anymore but
1420        // a special parameters $forceBuild is added.
1421        
1422        Map<String, Object> innerCopyMap = null;
1423        if (copyMap != null && BooleanUtils.isTrue((Boolean) copyMap.get("$loaded")))
1424        {
1425            innerCopyMap = copyMap;
1426        }
1427        
1428        if (innerCopyMapInputs != null)
1429        {
1430            if (innerCopyMap == null)
1431            {
1432                innerCopyMap = new HashMap<>();
1433                innerCopyMap.put("$forceBuild", true);
1434            }
1435            
1436            innerCopyMap.put("$inputs", innerCopyMapInputs);
1437        }
1438        
1439        return innerCopyMap;
1440    }
1441
1442    /**
1443     * Duplicate a metadata of type {@link MetadataType#SUB_CONTENT}.
1444     * If the copy map has a '$mode' parameter set to 'create', a new content will be created for each referenced content in the base metadata.
1445     * @param baseMetaHolder The metadata holder of the content to copy
1446     * @param targetMetaHolder The metadata holder of the target content
1447     * @param metadataName The name of the metadata 
1448     * @param metadataPath The content metadata path.
1449     * @param copyMap The properties to copy
1450     * @param innerCopyMapInputs The properties of sub objects
1451     * @param copyReport The report of the copy
1452     */
1453    public void copySubContentMetadata(CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataName, String metadataPath, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport)
1454    {
1455        if (baseMetaHolder.hasMetadata(metadataName))
1456        {
1457            String defaultMetadataSetType = copyReport._metadataSetType;
1458            String defaultMetadataSetName = copyReport._metadataSetName;
1459            String defaultFallbackMetadataSetName = copyReport._fallbackMetadataSetName;
1460            if (copyMap != null && copyMap.containsKey("$metadataSetName"))
1461            {
1462                defaultMetadataSetName = (String) copyMap.get("$metadataSetName");
1463            }
1464            
1465            Map<String, Object> innerCopyMap = _handleInnerCopyMap(copyMap, innerCopyMapInputs);
1466            TraversableAmetysObject objectCollection = baseMetaHolder.getObjectCollection(metadataName);
1467            AmetysObjectIterable<Content> subContents = objectCollection.getChildren();
1468            
1469            for (Content content : subContents)
1470            {
1471                CopyReport innerReport = copyContent(content, null, innerCopyMap, defaultMetadataSetName, defaultFallbackMetadataSetName, defaultMetadataSetType, copyReport.getTargetContentId(), metadataPath, getDefaultInitActionId(), getDefaultActionIdForEditingContentReferences());
1472                
1473                copyReport.addReport(innerReport);
1474            }
1475        }
1476        else if (targetMetaHolder.hasMetadata(metadataName))
1477        {
1478            targetMetaHolder.removeMetadata(metadataName);
1479        }
1480    }
1481    
1482    /**
1483     * Copy composite metadata
1484     * @param baseContent The copied content
1485     * @param baseMetaHolder The original composite metadata
1486     * @param targetMetaHolder The target composite metadata
1487     * @param metadataPath The metadata path
1488     * @param metadataDefinition The metadata definition
1489     * @param copyMap The map for copy
1490     * @param innerCopyMapInputs The properties of sub objects
1491     * @param copyReport The copy report
1492     */
1493    public void copyCompositeMetadata(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport)
1494    {
1495        if (metadataDefinition instanceof RepeaterDefinition)
1496        {
1497            _copyRepeater(baseContent, baseMetaHolder, targetMetaHolder, metadataPath, metadataDefinition, copyMap, innerCopyMapInputs, copyReport);
1498        }
1499        else
1500        {
1501            String metadataName = metadataDefinition.getName();
1502            
1503            if (baseMetaHolder.hasMetadata(metadataName))
1504            {
1505                CompositeMetadata baseCompositeMetadata = baseMetaHolder.getCompositeMetadata(metadataName);
1506                ModifiableCompositeMetadata targetCompositeMetadata = targetMetaHolder.getCompositeMetadata(metadataName, true);
1507                
1508                _copyMetadataMap(baseContent, baseCompositeMetadata, targetCompositeMetadata, metadataPath, metadataDefinition, copyMap, innerCopyMapInputs, copyReport);
1509            }
1510            else if (targetMetaHolder.hasMetadata(metadataName))
1511            {
1512                targetMetaHolder.removeMetadata(metadataName);
1513            }
1514        }
1515    }
1516    
1517    /**
1518     * Copy a repeater
1519     * @param baseContent The copied content
1520     * @param baseMetaHolder The original composite metadata
1521     * @param targetMetaHolder The target composite metadata
1522     * @param metadataPath The metadata path
1523     * @param metadataDefinition The metadata definition
1524     * @param copyMap The map for copy
1525     * @param innerCopyMapInputs The properties of sub objects
1526     * @param copyReport The copy report
1527     */
1528    protected void _copyRepeater(Content baseContent, CompositeMetadata baseMetaHolder, ModifiableCompositeMetadata targetMetaHolder, String metadataPath, MetadataDefinition metadataDefinition, Map<String, Object> copyMap, Map<String, Object> innerCopyMapInputs, CopyReport copyReport)
1529    {
1530        String metadataName = metadataDefinition.getName();
1531        
1532        if (baseMetaHolder.hasMetadata(metadataName))
1533        {
1534            CompositeMetadata baseRepeaterMetadata = baseMetaHolder.getCompositeMetadata(metadataName);
1535            ModifiableCompositeMetadata targetRepeaterMetadata = targetMetaHolder.getCompositeMetadata(metadataName, true);
1536            
1537            // Find the higher index for the entries of the base repeater
1538            String[] baseEntryNames = baseRepeaterMetadata.getMetadataNames();
1539            Arrays.sort(baseEntryNames, MetadataManager.REPEATER_ENTRY_COMPARATOR);
1540            
1541            int maxEntryName = 0;
1542            for (String entryName : baseEntryNames)
1543            {
1544                if (baseRepeaterMetadata.getType(entryName) == CompositeMetadata.MetadataType.COMPOSITE)
1545                {
1546                    maxEntryName = Math.max(maxEntryName, Integer.parseInt(entryName));
1547                }
1548            }
1549            
1550            // Rename every repeater entry to ensure their removal later because index of the entry will be higher that the repeater size.
1551            // Sorted by reverse name order to avoid same name conflicts while renaming entries.
1552            if (maxEntryName > 0)
1553            {
1554                String[] targetEntryNames = targetRepeaterMetadata.getMetadataNames();
1555                Arrays.sort(targetEntryNames, Collections.reverseOrder(MetadataManager.REPEATER_ENTRY_COMPARATOR));
1556                
1557                for (String entryName : targetEntryNames)
1558                {
1559                    if (targetRepeaterMetadata.getType(entryName) == CompositeMetadata.MetadataType.COMPOSITE)
1560                    {
1561                        // Warning! Rename expects the full jcr name ('ametys:name'), not the metadata name as usual.
1562                        // FIXME REPOSITORY-277
1563                        targetRepeaterMetadata.getCompositeMetadata(entryName).rename("ametys:" + String.valueOf(maxEntryName + Integer.parseInt(entryName)));
1564                    }
1565                }
1566            }
1567            
1568            // Duplication base repeater entries
1569            // Fix target entry names if necessary (1,3,4 -> 1,2,3)
1570            int targetEntryName = 1;
1571            for (String entryName : baseEntryNames)
1572            {
1573                if (baseRepeaterMetadata.getType(entryName) == CompositeMetadata.MetadataType.COMPOSITE)
1574                {
1575                    CompositeMetadata baseRepeaterEntry = baseRepeaterMetadata.getCompositeMetadata(entryName);
1576                    ModifiableCompositeMetadata targetRepeaterEntry = targetRepeaterMetadata.getCompositeMetadata(String.valueOf(targetEntryName), true);
1577                    
1578                    String metaPrefix = metadataPath + "/" + targetEntryName;
1579                    
1580                    _copyMetadataMap(baseContent, baseRepeaterEntry, targetRepeaterEntry, metaPrefix, metadataDefinition, copyMap, innerCopyMapInputs, copyReport);
1581                    
1582                    targetEntryName++;
1583                }
1584            }
1585            
1586            // Sync repeater by storing base entry names. Remaining entries of
1587            // the target repeater metadata will be deleted with this (the job
1588            // is done via _runContentReferencesEdition)
1589            copyReport.addRepeaterInfo(metadataPath, baseEntryNames.length);
1590        }
1591        else /* if (targetMetaHolder.hasMetadata(metadataName)) */
1592        {
1593            // Info are mandatory even if repeater does not currently exists. Otherwise, the EditContentFunction called via _runContentReferencesEdition will fails.
1594            copyReport.addRepeaterInfo(metadataPath, 0);
1595        }
1596    }
1597    
1598    /**
1599     * Analyse the content to find outgoing references and store them
1600     * @param content The content to analyze
1601     */
1602    protected void _extractOutgoingReferences(ModifiableContent content)
1603    {
1604        Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content);
1605        content.setOutgoingReferences(outgoingReferencesByPath);
1606    }
1607    
1608
1609    /**
1610     * This method analyzes content rich texts and update their content to
1611     * ensure consistency. (link to image, attachments...)
1612     * @param baseContent The copied content
1613     * @param targetContent The target content
1614     * @param copyReport  The copy report
1615     */
1616    protected void _updateRichTextMetadata(final Content baseContent, final ModifiableContent targetContent, CopyReport copyReport)
1617    {
1618        SAXParser saxParser = null;
1619        try
1620        {
1621            Map<ModifiableRichText, MetadataDefinition> copiedRichTexts = copyReport.getCopiedRichTexts();
1622            saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE);
1623            
1624            for (Entry<ModifiableRichText, MetadataDefinition> entry : copiedRichTexts.entrySet())
1625            {
1626                ModifiableRichText richText = entry.getKey();
1627                MetadataDefinition metadataDef = entry.getValue();
1628                
1629                String referenceContentType = metadataDef.getReferenceContentType();
1630                ContentType contentType = _contentTypeExtensionPoint.getExtension(referenceContentType);
1631                
1632                RichTextUpdater richTextUpdater = contentType.getRichTextUpdater();
1633                if (richTextUpdater != null)
1634                {
1635                    Map<String, Object> params = new HashMap<>();
1636                    params.put("initialContent", baseContent);
1637                    params.put("createdContent", targetContent);
1638                    params.put("initialAO", baseContent);
1639                    params.put("createdAO", targetContent);
1640                    
1641                    // create the transformer instance
1642                    TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
1643                    
1644                    // create the format of result
1645                    Properties format = new Properties();
1646                    format.put(OutputKeys.METHOD, "xml");
1647                    format.put(OutputKeys.INDENT, "yes");
1648                    format.put(OutputKeys.ENCODING, "UTF-8");
1649                    format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
1650                    th.getTransformer().setOutputProperties(format);
1651                    
1652                    // Update rich text contents
1653                    // Copy the needed original attachments.
1654                    try (InputStream is = richText.getInputStream(); OutputStream os = richText.getOutputStream())
1655                    {
1656                        StreamResult result = new StreamResult(os);
1657                        th.setResult(result);
1658                        
1659                        ContentHandler richTextHandler = richTextUpdater.getContentHandler(th, th, params);
1660                        
1661                        // Copy attachments handler.
1662                        ContentHandlerProxy copyAttachmentsHandler = new CopyAttachmentsHandler(richTextHandler, baseContent, targetContent, copyReport, _resolver, getLogger());
1663                        
1664                        // Rich text update.
1665                        saxParser.parse(new InputSource(is), copyAttachmentsHandler);
1666                    }
1667                    finally
1668                    {
1669                        th.getTransformer().reset();
1670                    }
1671                }
1672            }
1673        }
1674        catch (Exception e)
1675        {
1676            getLogger().error(
1677                    "An error occurred while updating rich text metadata for content '" + targetContent.getId() + " after copy from initial content '" + baseContent.getId() + "'",
1678                    e);
1679        }
1680        finally
1681        {
1682            _manager.release(saxParser);
1683        }
1684    }
1685    
1686    /**
1687     * Run the edition of the content references that has been stored during the copy of the metadata.
1688     * It is done via the EditContentFunction at the end of the process for technical purposes.
1689     * @param baseContent The base content to copy
1690     * @param targetContentWorkflow the target content local workflow
1691     * @param editActionId The edit action id to edit references
1692     * @param forceEdition true to force edition regardless user's rights
1693     * @param copyReport the copy report
1694     * @throws Exception If an error occured
1695     */
1696    protected void _runContentReferencesEdition(Content baseContent, AmetysObjectWorkflow targetContentWorkflow, int editActionId, boolean forceEdition, CopyReport copyReport) throws Exception
1697    {
1698        Map<String, Object> editionValues = new HashMap<>();
1699        
1700        Map<String, Object> contentReferenceValues = copyReport.getContentReferenceValuesMap();
1701        for (Entry<String, Object> entry : contentReferenceValues.entrySet())
1702        {
1703            String valueKey = StringUtils.replaceChars(StringUtils.stripStart(entry.getKey(), "/"), '/', '.');
1704            editionValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey, entry.getValue());
1705        }
1706        
1707        Map<String, Integer> repeatersInfo = copyReport.getRepeatersInfo();
1708        for (Entry<String, Integer> entry : repeatersInfo.entrySet())
1709        {
1710            String valueKey = StringUtils.replaceChars(StringUtils.stripStart(entry.getKey(), "/"), '/', '.');
1711            Integer entryCount = entry.getValue();
1712            
1713            editionValues.put(EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey, null);
1714            editionValues.put('_' + EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey + ".size", String.valueOf(entryCount));
1715            
1716            for (int i = 1; i <= entryCount; i++)
1717            {
1718                String pos = String.valueOf(i);
1719                editionValues.put('_' + EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey + "." + i + ".previous-position", pos);
1720                editionValues.put('_' + EditContentFunction.FORM_ELEMENTS_PREFIX + valueKey + "." + i + ".position", pos);
1721            }
1722        }
1723        
1724        if (editionValues.size() > 0)
1725        {
1726            WorkflowAwareContent targetContent = targetContentWorkflow.getAmetysObject();
1727            Map<String, Object> inputs = _getInputsForContentReferencesEdition(targetContent, targetContent, editionValues, copyReport);
1728            if (forceEdition)
1729            {
1730                inputs.put(CheckRightsCondition.FORCE, forceEdition);
1731            }
1732            
1733            try
1734            {
1735                targetContentWorkflow.doAction(targetContent.getWorkflowId(), editActionId, inputs);
1736            }
1737            catch (Exception e)
1738            {
1739                throw new ProcessingException("Error during content references edition. (base content identifier : " + baseContent.getId() 
1740                        + ", target content identifier : " + targetContent.getId() + "). ", e);
1741            }
1742        }
1743    }
1744    
1745    /**
1746     * Retrieve the inputs for the content references edition workflow function.
1747     * @param baseContent The content to copy
1748     * @param targetContent The content where to copy
1749     * @param editionValues The values representing the content references to be edited (also contains special repeater size values)
1750     * @param copyReport The report of the copy
1751     * @return The map of inputs.
1752     */
1753    protected Map<String, Object> _getInputsForContentReferencesEdition(Content baseContent, WorkflowAwareContent targetContent, Map<String, Object> editionValues, CopyReport copyReport)
1754    {
1755        Map<String, Object> inputs = new HashMap<>();
1756        
1757        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, targetContent);
1758        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
1759        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
1760        
1761        Map<String, Object> contextParameters = new HashMap<>();
1762        contextParameters.put(EditContentFunction.QUIT, Boolean.TRUE);
1763        contextParameters.put(EditContentFunction.FORM_RAW_VALUES, editionValues);
1764        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
1765        
1766        return inputs;
1767    }
1768    
1769    /**
1770     * Get the default workflow action id for initialization of main content
1771     * @return the default action id
1772     */
1773    public int getDefaultInitActionId ()
1774    {
1775        return 111;
1776    }
1777    
1778    
1779    /**
1780     * Get the default workflow action id for editing content metadata of main content
1781     * @return the default action id
1782     */
1783    public int getDefaultActionIdForEditingContentReferences ()
1784    {
1785        return 2;
1786    }
1787    
1788    /**
1789     * Get the default workflow action id for editing content by copy
1790     * @return the default action id
1791     */
1792    public int getDefaultActionIdForContentEdition()
1793    {
1794        return 222;
1795    }
1796    
1797    /**
1798     * Retrieve the action id to execute for the content copy.
1799     * @return The action id
1800     */
1801    public int getActionIdForCopy2()
1802    {
1803        return 111;
1804    }
1805    
1806    /**
1807     * A copy attachments content handler.
1808     * To be used to copy the attachments linked in a rich text metadata.
1809     */
1810    protected static class CopyAttachmentsHandler extends ContentHandlerProxy
1811    {
1812        /** base content */
1813        protected Content _baseContent;
1814        /** target content */
1815        protected ModifiableContent _targetContent;
1816        /** copy report */
1817        protected CopyReport _copyReport;
1818        /** Ametys object resolver */
1819        protected AmetysObjectResolver _resolver;
1820        /** logger */
1821        protected Logger _logger;
1822        
1823
1824        /**
1825         * Ctor
1826         * @param contentHandler The content handler to delegate to.
1827         * @param baseContent The content to copy
1828         * @param targetContent The content where to copy
1829         * @param copyReport The report of the copy
1830         * @param resolver The ametys object resolver
1831         * @param logger A logger to log informations
1832         */
1833        protected CopyAttachmentsHandler(ContentHandler contentHandler, Content baseContent, ModifiableContent targetContent, CopyReport copyReport, AmetysObjectResolver resolver, Logger logger)
1834        {
1835            super(contentHandler);
1836            _baseContent = baseContent;
1837            _targetContent = targetContent;
1838            _copyReport = copyReport;
1839            _resolver = resolver;
1840            _logger = logger;
1841        }
1842        
1843        @Override
1844        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException
1845        {
1846            if ("link".equals(loc))
1847            {
1848                // Copy attachment
1849                _copyIfAttachment(attrs.getValue("xlink:href"));
1850            }
1851            
1852            super.startElement(uri, loc, raw, attrs);
1853        }
1854        
1855        /**
1856         * Copy the linked resource to the new content if it is an attachment. 
1857         * @param href link href attribute
1858         */
1859        protected void _copyIfAttachment(String href)
1860        {
1861            try
1862            {
1863                if (_baseContent.getId().equals(href) || _targetContent.getId().equals(href))
1864                {
1865                    // nothing to do
1866                    return;
1867                }
1868                else if (_resolver.hasAmetysObjectForId(href))
1869                {
1870                    AmetysObject ametysObject = _resolver.resolveById(href);
1871                    
1872                    ResourceCollection baseRootAttachments = _baseContent.getRootAttachments();
1873                    if (!(ametysObject instanceof org.ametys.plugins.explorer.resources.Resource) || baseRootAttachments == null)
1874                    {
1875                        // nothing to do
1876                        return;
1877                    }
1878                    
1879                    String baseAttachmentsPath = _baseContent.getRootAttachments().getPath();
1880                    String resourcePath = ametysObject.getPath();
1881                    
1882                    if (resourcePath.startsWith(baseAttachmentsPath + '/'))
1883                    {
1884                        // Is in attachments path
1885                        String relPath = StringUtils.removeStart(resourcePath, baseAttachmentsPath + '/');
1886                        _copyAttachment(ametysObject, relPath);
1887                    }
1888                }
1889            }
1890            catch (AmetysRepositoryException e)
1891            {
1892                // the reference was not <protocol>://<protocol-specific-part> (for example : mailto:mymail@example.com )
1893                _logger.debug("The link '{}' is not recognized as Ametys object. It will be ignored", href);
1894                return; 
1895            }
1896        }
1897
1898        /**
1899         * Copy an attachment
1900         * @param baseResource The resource to copy
1901         * @param relPath The path where to copy
1902         */
1903        protected void _copyAttachment(AmetysObject baseResource, String relPath)
1904        {
1905            boolean success = false;
1906            Exception exception = null;
1907            
1908            try
1909            {
1910                if (_targetContent instanceof ModifiableTraversableAmetysObject)
1911                {
1912                    ModifiableTraversableAmetysObject mtaoTargetContent = (ModifiableTraversableAmetysObject) _targetContent;
1913                    ModifiableResourceCollection targetParentCollection = mtaoTargetContent.getChild(DefaultContent.ATTACHMENTS_NODE_NAME);
1914                    
1915                    String[] parts = StringUtils.split(relPath, '/');
1916                    if (parts.length > 0)
1917                    {
1918                        // Traverse the path and create necessary resources collections
1919                        for (int i = 0; i < parts.length - 1; i++)
1920                        {
1921                            String childName = parts[i];
1922                            if (!targetParentCollection.hasChild(childName))
1923                            {
1924                                targetParentCollection = targetParentCollection.createChild(childName, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE);
1925                            }
1926                            else
1927                            {
1928                                targetParentCollection = targetParentCollection.getChild(childName);
1929                            }
1930                        }
1931                        
1932                        // Copy the attachment resource.
1933                        String resourceName = parts[parts.length - 1];
1934                        if (baseResource instanceof CopiableAmetysObject)
1935                        {
1936                            ((CopiableAmetysObject) baseResource).copyTo(targetParentCollection, resourceName);
1937                            success = true;
1938                            _copyReport.addAttachment(relPath);
1939                        }
1940                    }
1941                }
1942            }
1943            catch (Exception e)
1944            {
1945                exception = e;
1946            }
1947            
1948            if (!success)
1949            {
1950                String warnMsg = "Unable to copy attachment from base path '" + baseResource.getPath() + "' to the content at path : '" + _targetContent.getPath() + "'.";
1951                
1952                if (_logger.isWarnEnabled())
1953                {
1954                    if (exception != null)
1955                    {
1956                        _logger.warn(warnMsg, exception);
1957                    }
1958                    else
1959                    {
1960                        _logger.warn(warnMsg);
1961                    }
1962                }
1963            }
1964        }
1965    }
1966}