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