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