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.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Optional;
028import java.util.Properties;
029import java.util.Set;
030
031import javax.xml.transform.OutputKeys;
032import javax.xml.transform.TransformerFactory;
033import javax.xml.transform.sax.SAXTransformerFactory;
034import javax.xml.transform.sax.TransformerHandler;
035import javax.xml.transform.stream.StreamResult;
036
037import org.apache.avalon.framework.component.Component;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.cocoon.ProcessingException;
042import org.apache.commons.lang3.StringUtils;
043import org.apache.commons.lang3.tuple.Pair;
044import org.apache.excalibur.xml.sax.ContentHandlerProxy;
045import org.apache.excalibur.xml.sax.SAXParser;
046import org.apache.xml.serializer.OutputPropertiesFactory;
047import org.slf4j.Logger;
048import org.xml.sax.Attributes;
049import org.xml.sax.ContentHandler;
050import org.xml.sax.InputSource;
051import org.xml.sax.SAXException;
052
053import org.ametys.cms.content.CopyReport.CopyMode;
054import org.ametys.cms.content.CopyReport.CopyState;
055import org.ametys.cms.contenttype.ContentType;
056import org.ametys.cms.contenttype.ContentTypesHelper;
057import org.ametys.cms.contenttype.RichTextUpdater;
058import org.ametys.cms.data.ContentValue;
059import org.ametys.cms.data.RichText;
060import org.ametys.cms.data.type.ModelItemTypeConstants;
061import org.ametys.cms.repository.Content;
062import org.ametys.cms.repository.DefaultContent;
063import org.ametys.cms.repository.ModifiableContent;
064import org.ametys.cms.repository.WorkflowAwareContent;
065import org.ametys.cms.workflow.AllErrors;
066import org.ametys.cms.workflow.ContentWorkflowHelper;
067import org.ametys.cms.workflow.CreateContentFunction;
068import org.ametys.cms.workflow.EditContentFunction;
069import org.ametys.cms.workflow.InvalidInputWorkflowException;
070import org.ametys.cms.workflow.copy.CreateContentByCopyFunction;
071import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
072import org.ametys.plugins.explorer.resources.ResourceCollection;
073import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
074import org.ametys.plugins.repository.AmetysObject;
075import org.ametys.plugins.repository.AmetysObjectResolver;
076import org.ametys.plugins.repository.AmetysRepositoryException;
077import org.ametys.plugins.repository.CopiableAmetysObject;
078import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
079import org.ametys.plugins.repository.model.ViewHelper;
080import org.ametys.plugins.workflow.AbstractWorkflowComponent;
081import org.ametys.plugins.workflow.support.WorkflowProvider;
082import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
083import org.ametys.runtime.i18n.I18nizableText;
084import org.ametys.runtime.i18n.I18nizableTextParameter;
085import org.ametys.runtime.model.ElementDefinition;
086import org.ametys.runtime.model.View;
087import org.ametys.runtime.model.ViewItemContainer;
088import org.ametys.runtime.parameter.Errors;
089import org.ametys.runtime.plugin.component.AbstractLogEnabled;
090
091/**
092 * <p>
093 * This component is used to copy a content (either totally or partially).
094 * </p><p>
095 * In this whole file a Map named <em>copyMap</em> is regularly used. This map
096 * provide the name of the attribute to copy as well as some optional parameters.
097 * It has the following form (JSON) :
098 * </p>
099 * <pre>
100 * {
101 *   "$param1": value,
102 *   "attributeA": null,
103 *   "attributeB": {
104 *     "subattributeB1": null,
105 *     "subattributeB2": {
106 *       "$param1": value,
107 *       "$param2": value,
108 *       "subSubattributeB21": {...}
109 *     },
110 *     ...
111 *   }
112 * }
113 * </pre>
114 * <p>
115 * Each attribute that should be copied must be present as a key in the map.
116 * Composite attribute can contains child attributes but as seen on the example the
117 * map must be well structured, it is not a flat map. Parameters in the map must
118 * always start with the reserved character '$', in order to be differentiated
119 * from attribute name.
120 * </p><p>
121 * The entry points are the copyContent and editContent methods, which run a dedicated workflow
122 * function (createByCopy or edit).<br>
123 * Actual write of values is made through the EditContentFunction, with the values computed by this component.
124 */
125public class CopyContentComponent extends AbstractLogEnabled implements Serviceable, Component
126{
127    /** Avalon ROLE. */
128    public static final String ROLE = CopyContentComponent.class.getName();
129    
130    /** Workflow provider. */
131    protected WorkflowProvider _workflowProvider;
132    
133    /** Ametys object resolver available to subclasses. */
134    protected AmetysObjectResolver _resolver;
135    
136    /** Helper for content types */
137    protected ContentTypesHelper _contentTypesHelper;
138    
139    /** The content helper */
140    protected ContentHelper _contentHelper;
141    
142    /** The content workflow helper */
143    protected ContentWorkflowHelper _contentWorkflowHelper;
144    
145    /** Avalon service manager */
146    protected ServiceManager _manager;
147    
148    @Override
149    public void service(ServiceManager manager) throws ServiceException
150    {
151        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
152        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
153        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
154        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
155        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
156        _manager = manager;
157    }
158    
159    /**
160     * Copy a content by creating a new content and copying the attributes value a source content into the new one.
161     * @param contentId The source content id
162     * @param title Desired title for the new content or null if computed from the source's title
163     * @param copyMap The map of properties as described in {@link CopyContentComponent}. 
164     *                Can be null in which case the map will be constructed from the provided view.
165     * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the
166     *                 default name for possible inner copies (if not provided as a copyMap parameter).
167     * @param fallbackViewName The fallback view name if 'viewName' is not found
168     * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content.
169     * @param initActionId The init workflow action id for main content only
170     * @return The copy report containing valuable information about the copy and the possible encountered errors.
171     */
172    public CopyReport copyContent(String contentId, String title, Map<String, Object> copyMap, String viewName, String fallbackViewName, String targetContentType, int initActionId)
173    {
174        Content content = _resolver.resolveById(contentId);
175        CopyReport report = new CopyReport(contentId, _contentHelper.getTitle(content), _contentHelper.isReferenceTable(content), viewName, fallbackViewName, CopyMode.CREATION);
176        
177        try
178        {
179            Map<String, Object> inputs = getInputsForCopy(content, title, copyMap, targetContentType, report);
180            String workflowName = getWorkflowName(content, inputs);
181            
182            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow();
183            workflow.initialize(workflowName, initActionId, inputs);
184            
185            ModifiableContent targetContent = workflow.getAmetysObject();
186            
187            report.notifyContentCreation(targetContent.getId(), _contentHelper.getTitle(targetContent), _contentHelper.isReferenceTable(content));
188            report.notifyContentCopySuccess();
189        }
190        catch (Exception e)
191        {
192            getLogger().error("An error has been encountered during the content copy, or the copy is not allowed (base content identifier : {}).", contentId, e);
193            
194            if (e instanceof InvalidInputWorkflowException)
195            {
196                I18nizableText rootError = null;
197                
198                AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors();
199                Map<String, Errors> allErrorsMap = allErrors.getAllErrors();
200                for (String errorMetadataPath : allErrorsMap.keySet())
201                {
202                    Errors errors = allErrorsMap.get(errorMetadataPath);
203                    
204                    I18nizableText insideError = null;
205                    
206                    List<I18nizableText> errorsAsList = errors.getErrors();
207                    for (I18nizableText error : errorsAsList)
208                    {
209                        Map<String, I18nizableTextParameter> i18nparameters = new HashMap<>();
210                        i18nparameters.put("0", error);
211                        
212                        I18nizableText localError = new I18nizableText("plugin.cms", "CONTENT_COPY_ACTIONS_REPORT_ERROR_VALIDATION_ATTRIBUTE_CHAIN", i18nparameters);
213                        
214                        if (insideError == null)
215                        {
216                            insideError = localError;
217                        }
218                        else
219                        {
220                            insideError.getParameterMap().put("1", localError);
221                        }
222                    }
223
224                    Map<String, I18nizableTextParameter> i18ngeneralparameters = new HashMap<>();
225                    
226                    String i18ngeneralkey = null;
227                    if (EditContentFunction.GLOBAL_ERROR_KEY.equals(errorMetadataPath))
228                    {
229                        i18ngeneralkey = "CONTENT_COPY_ACTIONS_REPORT_ERROR_GLOBAL_VALIDATION";
230                        i18ngeneralparameters.put("error", insideError);
231                    }
232                    else
233                    {
234                        i18ngeneralkey = "CONTENT_COPY_ACTIONS_REPORT_ERROR_VALIDATION_ATTRIBUTE";
235                        i18ngeneralparameters.put("path", new I18nizableText(errorMetadataPath));
236                        i18ngeneralparameters.put("error", insideError);
237                    }
238
239                    I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters);
240                    if (rootError == null)
241                    {
242                        rootError = generalError;
243                    }
244                    else
245                    {
246                        rootError.getParameterMap().put("2", generalError);
247                    }
248                }
249                
250                Map<String, I18nizableTextParameter> parameters = Map.of("title", new I18nizableText(_contentHelper.getTitle(content)), "error", rootError);
251                I18nizableText errorMsg = new I18nizableText("plugin.cms", "CONTENT_COPY_ACTIONS_REPORT_ERROR_COPY", parameters);
252                report.setErrorMessage(errorMsg);
253            }
254            report.notifyContentCopyError();
255        }
256        
257        return report;
258    }
259    
260    /**
261     * Retrieve the inputs for the copy workflow function.
262     * @param baseContent The content to copy
263     * @param title The title to set
264     * @param copyMap The map with properties to copy
265     * @param targetContentType The type of content to create. If null the type(s) of created content will be those of base content.
266     * @param copyReport The report of the copy
267     * @return The map of inputs.
268     */
269    protected Map<String, Object> getInputsForCopy(Content baseContent, String title, Map<String, Object> copyMap, String targetContentType, CopyReport copyReport)
270    {
271        Map<String, Object> inputs = new HashMap<>();
272        
273        inputs.put(CreateContentByCopyFunction.BASE_CONTENT_KEY, baseContent);
274        inputs.put(CreateContentByCopyFunction.COPY_MAP_KEY, copyMap);
275        inputs.put(CreateContentByCopyFunction.COPY_REPORT_KEY, copyReport);
276        inputs.put(CreateContentByCopyFunction.COPY_VIEW_NAME, copyReport.getViewName());
277        inputs.put(CreateContentByCopyFunction.COPY_FALLBACK_VIEW_NAME, copyReport.getFallbackViewName());
278        
279        if (StringUtils.isNoneBlank(title))
280        {
281            inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, title);
282        }
283
284        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, new HashMap<String, Object>());
285        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
286        
287        if (targetContentType != null)
288        {
289            inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, new String[] {targetContentType});
290        }
291        
292        return inputs;
293    }
294    
295    /**
296     * Retrieve the workflow name of a content.
297     * @param content The content to consider
298     * @param inputs The inputs that will be provided to the workflow function
299     * @return The name of the workflow.
300     * @throws IllegalArgumentException if the content is not workflow aware.
301     */
302    protected String getWorkflowName(Content content, Map<String, Object> inputs) throws IllegalArgumentException
303    {
304        String workflowName = null;
305        
306        if (content instanceof WorkflowAwareContent)
307        {
308            WorkflowAwareContent waContent = (WorkflowAwareContent) content;
309            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent);
310            workflowName = workflow.getWorkflowName(waContent.getWorkflowId());
311        }
312        
313        if (workflowName == null)
314        {
315            String errorMsg = String.format("Unable to retrieve the workflow name for the content with identifier '%s'.", content.getId());
316            
317            getLogger().error(errorMsg);
318            throw new IllegalArgumentException(errorMsg);
319        }
320        
321        return workflowName;
322    }
323    
324    /**
325     * Edit a content by copying attribute values a source content into a target content.
326     * @param contentId The identifier of the source content
327     * @param targetContentId The identifier of the target content
328     * @param copyMap The map of properties as described in {@link CopyContentComponent}. 
329     *                Can be null in which case the map will be constructed from the provided view.
330     * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the
331     *                 default name for possible inner copies (if not provided as a copyMap parameter).
332     * @param fallbackViewName The fallback view name if 'viewName' is not found
333     * @return The copy report containing valuable information about the copy and the possible encountered errors.
334     */
335    public CopyReport editContent(String contentId, String targetContentId, Map<String, Object> copyMap, String viewName, String fallbackViewName)
336    {
337        return editContent(contentId, targetContentId, copyMap, viewName, fallbackViewName, getDefaultActionIdForContentEdition());
338    }
339
340    /**
341     * Edit a content by copying attribute values a source content into a target content.
342     * @param contentId The identifier of the source content
343     * @param targetContentId The identifier of the target content
344     * @param copyMap The map of properties as described in {@link CopyContentComponent}. 
345     *                Can be null in which case the map will be constructed from the provided view.
346     * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the
347     *                 default name for possible inner copies (if not provided as a copyMap parameter).
348     * @param fallbackViewName The fallback view name if 'viewName' is not found
349     * @param actionId the edit workflow action id
350     * @return The copy report containing valuable information about the copy and the possible encountered errors.
351     */
352    public CopyReport editContent(String contentId, String targetContentId, Map<String, Object> copyMap, String viewName, String fallbackViewName, int actionId)
353    {
354        Content content = _resolver.resolveById(contentId);
355        
356        String auxViewName = StringUtils.defaultIfEmpty(viewName, "default-edition");
357        String auxFallbackViewName = StringUtils.defaultIfEmpty(fallbackViewName, "main");
358        
359        CopyReport report = new CopyReport(contentId, _contentHelper.getTitle(content), _contentHelper.isReferenceTable(content), auxViewName, auxFallbackViewName, CopyMode.EDITION);
360        try
361        {
362            WorkflowAwareContent targetContent = _resolver.resolveById(targetContentId);
363            
364            Map<String, Object> values = computeValues(content, (ModifiableContent) targetContent, copyMap, null, auxViewName, auxFallbackViewName, report);
365            
366            _contentWorkflowHelper.editContent(targetContent, values, actionId);
367            
368            report.notifyContentCreation(targetContent.getId(), _contentHelper.getTitle(targetContent), _contentHelper.isReferenceTable(content));
369            report.notifyContentCopySuccess();
370        }
371        catch (Exception e)
372        {
373            getLogger().error("An error has been encountered during the content edition, or the edition is not allowed (base content identifier : {}, target content identifier : {}).",
374                    contentId, targetContentId, e);
375            
376            report.notifyContentCopyError();
377        }
378        
379        return report;
380    }
381    
382    /**
383     * Extract values to copy from the given parameters.
384     * @param content the source content
385     * @param targetContent the target content
386     * @param copyMap the map of properties as described in {@link CopyContentComponent}. 
387     * @param additionalCopyMap an additional map of properties if needed. Can be null. Often used in case of recursive copies.
388     * @param viewName The name of the view to be used to construct to copyMap if not provided. This will also be the
389     *                 default name for possible inner copies (if not provided as a copyMap parameter).
390     * @param fallbackViewName The fallback view name if 'viewName' is not found
391     * @param copyReport The copy report containing valuable information about the copy and the possible encountered errors.
392     * @return the computed values, ready to be synchronized to the target content.
393     */
394    public Map<String, Object> computeValues(Content content, ModifiableContent targetContent, Map<String, Object> copyMap, Map<String, Object> additionalCopyMap, String viewName, String fallbackViewName, CopyReport copyReport)
395    {
396        Pair<View, Map<String, Object>> viewAndValues = _getViewAndValues(content, copyMap, additionalCopyMap, viewName, fallbackViewName, copyReport);
397        View view = viewAndValues.getLeft();
398        Map<String, Object> values = viewAndValues.getRight();
399
400        _updateRichTexts(content, targetContent, view, values, copyReport);
401        
402        return values;
403    }
404    
405    private Pair<View, Map<String, Object>> _getViewAndValues(Content content, Map<String, Object> copyMap, Map<String, Object> additionalCopyMap, String viewName, String fallbackViewName, CopyReport copyReport)
406    {
407        Set<String> viewItems;
408        Map<String, Map<String, Object>> contentToCopy = new HashMap<>();
409        
410        Map<String, Object> finalCopyMap = null;
411        if (copyMap != null)
412        {
413            finalCopyMap = new HashMap<>();
414            for (Entry<String, Object> entry : copyMap.entrySet())
415            {
416                if (!entry.getKey().startsWith("$"))
417                {
418                    // cannot use stream here as entry values are often null and Collectors.toMap don't like that...
419                    finalCopyMap.put(entry.getKey(), entry.getValue());
420                }
421            }
422        }
423        
424        if (finalCopyMap != null && !finalCopyMap.isEmpty())
425        {
426            viewItems = new HashSet<>();
427            _fillViewItemsFromCopyMap(content, viewItems, contentToCopy, finalCopyMap, "");
428        }
429        else
430        {
431            View view = _contentTypesHelper.getViewWithFallback(viewName, fallbackViewName, content.getTypes(), content.getMixinTypes());
432            viewItems = new HashSet<>(org.ametys.runtime.model.ViewHelper.getModelItemsPathsFromView(view));
433        }
434        
435        if (additionalCopyMap != null)
436        {
437            _fillViewItemsFromCopyMap(content, viewItems, contentToCopy, additionalCopyMap, "");
438        }
439        
440        View view = View.of(content.getModel(), viewItems.toArray(String[]::new));
441        Map<String, Object> values = content.dataToMap(view);
442
443        _processLinkedContents(content, view, values, contentToCopy, copyReport);
444
445        return Pair.of(view, values);
446    }
447    
448    @SuppressWarnings("unchecked")
449    private void _fillViewItemsFromCopyMap(Content content, Set<String> items, Map<String, Map<String, Object>> contentToCopy, Map<String, Object> copyMap, String prefix)
450    {
451        for (String name : copyMap.keySet())
452        {
453            if (!name.startsWith("$"))
454            {
455                Object value = copyMap.get(name);
456                if (value == null)
457                {
458                    items.add(prefix + name);
459                }
460                else
461                {
462                    Map<String, Object> subCopyMap = (Map<String, Object>) value;
463                    if (subCopyMap.containsKey("$mode"))
464                    {
465                        // content attribute
466                        items.add(prefix + name);
467                        contentToCopy.put(prefix + name, subCopyMap);
468                    }
469                    else
470                    {
471                        // composite or repeater
472                        _fillViewItemsFromCopyMap(content, items, contentToCopy, subCopyMap, prefix + name + "/");
473                    }
474                }
475            }
476        }
477    }
478    
479    @SuppressWarnings("unchecked")
480    private void _processLinkedContents(Content content, ViewItemContainer viewItemContainer, Map<String, Object> values, Map<String, Map<String, Object>> contentToCopy, CopyReport copyReport)
481    {
482        ViewHelper.visitView(viewItemContainer, 
483            (element, definition) -> {
484                // simple element
485                String name = definition.getName();
486                String path = definition.getPath();
487                Object value = values.get(name);
488                if (value != null && definition.getType().getId().equals(ModelItemTypeConstants.CONTENT_ELEMENT_TYPE_ID))
489                {
490                    // create the content and replace the old value by the new one
491                    Map<String, Object> copyMap = contentToCopy.get(path);
492                    boolean referenceMode = copyMap == null || !"create".equals(copyMap.get("$mode"));
493                    
494                    if (definition.isMultiple())
495                    {
496                        ContentValue[] contentValues = (ContentValue[]) value;
497                        List<ContentValue> targets = new ArrayList<>();
498                        
499                        Arrays.stream(contentValues)
500                              .map(contentValue -> contentValue.getContentIfExists())
501                              .flatMap(Optional::stream)
502                              .forEach(subContent -> {
503                                  ContentValue contentValue = handleLinkedContent(definition, subContent, referenceMode, copyMap, copyReport);
504                                  if (contentValue != null)
505                                  {
506                                      targets.add(contentValue);
507                                  }
508                              });
509                        
510                        values.put(name, targets.toArray(ContentValue[]::new));
511                    }
512                    else
513                    {
514                        ContentValue contentValue = (ContentValue) value;
515                        ModifiableContent subContent = contentValue.getContentIfExists().orElse(null);
516                        if (subContent != null)
517                        {
518                            ContentValue linkedValue = handleLinkedContent(definition, subContent, referenceMode, copyMap, copyReport);
519                            values.put(name, linkedValue);
520                        }
521                    }
522                }
523            }, 
524            (group, definition) -> {
525                // composite
526                String name = definition.getName();
527                Map<String, Object> composite = (Map<String, Object>) values.get(name);
528                if (composite != null)
529                {
530                    _processLinkedContents(content, group, composite, contentToCopy, copyReport);
531                }
532            }, 
533            (group, definition) -> {
534                // repeater
535                String name = definition.getName();
536                List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name);
537                if (entries != null)
538                {
539                    for (Map<String, Object> entry : entries)
540                    {
541                        _processLinkedContents(content, group, entry, contentToCopy, copyReport);
542                    }
543                }
544            }, 
545            group -> _processLinkedContents(content, group, values, contentToCopy, copyReport));
546    }
547    
548    /**
549     * Handle a single value of a content attribute
550     * @param definition the attribute definition
551     * @param value the linked value on the source content
552     * @param referenceMode true if a reference was initially requested, false if it was a copy
553     * @param copyMap the current copy map
554     * @param copyReport the copy report
555     * @return the {@link ContentValue} (copied or not) to insert in the current Content. 
556     */
557    protected ContentValue handleLinkedContent(ElementDefinition definition, ModifiableContent value, boolean referenceMode, Map<String, Object> copyMap, CopyReport copyReport)
558    {
559        if (!referenceMode)
560        {
561            String targetContentId = copyLinkedContent(value, copyMap, copyReport);
562            if (targetContentId != null)
563            {
564                return new ContentValue(_resolver, targetContentId);
565            }
566        }
567        else
568        {
569            return new ContentValue(value);
570        }
571        
572        return null;
573    }
574    
575    /**
576     * Copy a value of a content attribute.
577     * @param content the initial content value.
578     * @param copyMap the current copy map
579     * @param copyReport the current copy report
580     * @return the id of the copied Content.
581     */
582    protected String copyLinkedContent(Content content, Map<String, Object> copyMap, CopyReport copyReport)
583    {
584        String defaultViewName = copyMap != null ? (String) copyMap.getOrDefault("$viewName", copyReport.getViewName()) : copyReport.getViewName();
585        String defaultFallbackViewName = copyMap != null ? (String) copyMap.getOrDefault("$fallbackViewName", copyReport.getFallbackViewName()) : copyReport.getFallbackViewName();
586        
587        CopyReport innerReport = copyContent(content.getId(), content.getTitle(), copyMap, defaultViewName, defaultFallbackViewName, null, getDefaultInitActionId());
588        
589        String targetContentId = null;
590        if (innerReport.getStatus() == CopyState.SUCCESS)
591        {
592            targetContentId = innerReport.getTargetContentId();
593        }
594        
595        copyReport.addReport(innerReport);
596        
597        return targetContentId;
598    }
599    
600    @SuppressWarnings("unchecked")
601    private void _updateRichTexts(Content content, ModifiableContent targetContent, ViewItemContainer viewItemContainer, Map<String, Object> values, CopyReport copyReport)
602    {
603        ViewHelper.visitView(viewItemContainer, 
604            (element, definition) -> {
605                // simple element
606                String name = definition.getName();
607                Object value = values.get(name);
608                if (value != null && definition.getType().getId().equals(ModelItemTypeConstants.RICH_TEXT_ELEMENT_TYPE_ID))
609                {
610                    // update richTexts
611                    RichTextUpdater richTextUpdater = ((ContentType) definition.getModel()).getRichTextUpdater();
612                    if (richTextUpdater != null)
613                    {
614                        if (definition.isMultiple())
615                        {
616                            RichText[] richTexts = (RichText[]) value;
617                            for (RichText richText : richTexts)
618                            {
619                                _updateRichText(richText, richTextUpdater, content, targetContent, copyReport);
620                            }
621                        }
622                        else
623                        {
624                            RichText richText = (RichText) value;
625                            _updateRichText(richText, richTextUpdater, content, targetContent, copyReport);
626                        }
627                    }
628                }
629            }, 
630            (group, definition) -> {
631                // composite
632                String name = definition.getName();
633                Map<String, Object> composite = (Map<String, Object>) values.get(name);
634                if (composite != null)
635                {
636                    _updateRichTexts(content, targetContent, group, composite, copyReport);
637                }
638            }, 
639            (group, definition) -> {
640                // repeater
641                String name = definition.getName();
642                List<Map<String, Object>> entries = (List<Map<String, Object>>) values.get(name);
643                if (entries != null)
644                {
645                    for (Map<String, Object> entry : entries)
646                    {
647                        _updateRichTexts(content, targetContent, group, entry, copyReport);
648                    }
649                }
650            }, 
651            group -> _updateRichTexts(content, targetContent, group, values, copyReport));
652    }
653    
654    private void _updateRichText(RichText richText, RichTextUpdater richTextUpdater, Content initialContent, ModifiableContent targetContent, CopyReport copyReport)
655    {
656        try
657        {
658            Map<String, Object> params = new HashMap<>();
659            params.put("initialContent", initialContent);
660            params.put("createdContent", targetContent);
661            params.put("initialAO", initialContent);
662            params.put("createdAO", targetContent);
663            
664            // create the transformer instance
665            TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
666            
667            // create the format of result
668            Properties format = new Properties();
669            format.put(OutputKeys.METHOD, "xml");
670            format.put(OutputKeys.INDENT, "yes");
671            format.put(OutputKeys.ENCODING, "UTF-8");
672            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2");
673            th.getTransformer().setOutputProperties(format);
674            
675            // Update rich text contents
676            // Copy the needed original attachments.
677            try (InputStream is = richText.getInputStream(); OutputStream os = richText.getOutputStream())
678            {
679                StreamResult result = new StreamResult(os);
680                th.setResult(result);
681                
682                ContentHandler richTextHandler = richTextUpdater.getContentHandler(th, th, params);
683                
684                // Copy attachments handler.
685                ContentHandlerProxy copyAttachmentsHandler = new CopyAttachmentsHandler(richTextHandler, initialContent, targetContent, copyReport, _resolver, getLogger());
686                
687                // Rich text update.
688                SAXParser saxParser = null;
689                try
690                {
691                    saxParser = (SAXParser) _manager.lookup(SAXParser.ROLE);
692                    saxParser.parse(new InputSource(is), copyAttachmentsHandler);
693                }
694                catch (ServiceException e)
695                {
696                    throw new ProcessingException("Unable to get a SAX parser", e);
697                }
698                finally
699                {
700                    _manager.release(saxParser);
701                }
702            }
703        }
704        catch (Exception e)
705        {
706            getLogger().error("An error occurred while updating rich text attribute for content '{}' after copy from initial content '{}'", targetContent.getId(), initialContent.getId(), e);
707        }
708    }
709    
710    /**
711     * Get the default workflow action id for initialization of main content
712     * @return the default action id
713     */
714    public int getDefaultInitActionId ()
715    {
716        return 111;
717    }
718    
719    /**
720     * Get the default workflow action id for editing content by copy
721     * @return the default action id
722     */
723    public int getDefaultActionIdForContentEdition()
724    {
725        return 222;
726    }
727    
728    /**
729     * A copy attachments content handler.
730     * To be used to copy the attachments linked in a rich text attribute.
731     */
732    protected static class CopyAttachmentsHandler extends ContentHandlerProxy
733    {
734        /** base content */
735        protected Content _baseContent;
736        /** target content */
737        protected ModifiableContent _targetContent;
738        /** copy report */
739        protected CopyReport _copyReport;
740        /** Ametys object resolver */
741        protected AmetysObjectResolver _resolver;
742        /** logger */
743        protected Logger _logger;
744        
745        /**
746         * Ctor
747         * @param contentHandler The content handler to delegate to.
748         * @param baseContent The content to copy
749         * @param targetContent The content where to copy
750         * @param copyReport The report of the copy
751         * @param resolver The ametys object resolver
752         * @param logger A logger to log informations
753         */
754        protected CopyAttachmentsHandler(ContentHandler contentHandler, Content baseContent, ModifiableContent targetContent, CopyReport copyReport, AmetysObjectResolver resolver, Logger logger)
755        {
756            super(contentHandler);
757            _baseContent = baseContent;
758            _targetContent = targetContent;
759            _copyReport = copyReport;
760            _resolver = resolver;
761            _logger = logger;
762        }
763        
764        @Override
765        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException
766        {
767            if ("link".equals(loc))
768            {
769                // Copy attachment
770                _copyIfAttachment(attrs.getValue("xlink:href"));
771            }
772            
773            super.startElement(uri, loc, raw, attrs);
774        }
775        
776        /**
777         * Copy the linked resource to the new content if it is an attachment. 
778         * @param href link href attribute
779         */
780        protected void _copyIfAttachment(String href)
781        {
782            try
783            {
784                if (_baseContent.getId().equals(href) || _targetContent.getId().equals(href))
785                {
786                    // nothing to do
787                    return;
788                }
789                else if (_resolver.hasAmetysObjectForId(href))
790                {
791                    AmetysObject ametysObject = _resolver.resolveById(href);
792                    
793                    ResourceCollection baseRootAttachments = _baseContent.getRootAttachments();
794                    if (!(ametysObject instanceof org.ametys.plugins.explorer.resources.Resource) || baseRootAttachments == null)
795                    {
796                        // nothing to do
797                        return;
798                    }
799                    
800                    String baseAttachmentsPath = _baseContent.getRootAttachments().getPath();
801                    String resourcePath = ametysObject.getPath();
802                    
803                    if (resourcePath.startsWith(baseAttachmentsPath + '/'))
804                    {
805                        // Is in attachments path
806                        String relPath = StringUtils.removeStart(resourcePath, baseAttachmentsPath + '/');
807                        _copyAttachment(ametysObject, relPath);
808                    }
809                }
810            }
811            catch (AmetysRepositoryException e)
812            {
813                // the reference was not <protocol>://<protocol-specific-part> (for example : mailto:mymail@example.com )
814                _logger.debug("The link '{}' is not recognized as Ametys object. It will be ignored", href);
815                return; 
816            }
817        }
818
819        /**
820         * Copy an attachment
821         * @param baseResource The resource to copy
822         * @param relPath The path where to copy
823         */
824        protected void _copyAttachment(AmetysObject baseResource, String relPath)
825        {
826            boolean success = false;
827            Exception exception = null;
828            
829            try
830            {
831                if (_targetContent instanceof ModifiableTraversableAmetysObject)
832                {
833                    ModifiableTraversableAmetysObject mtaoTargetContent = (ModifiableTraversableAmetysObject) _targetContent;
834                    ModifiableResourceCollection targetParentCollection = mtaoTargetContent.getChild(DefaultContent.ATTACHMENTS_NODE_NAME);
835                    
836                    String[] parts = StringUtils.split(relPath, '/');
837                    if (parts.length > 0)
838                    {
839                        // Traverse the path and create necessary resources collections
840                        for (int i = 0; i < parts.length - 1; i++)
841                        {
842                            String childName = parts[i];
843                            if (!targetParentCollection.hasChild(childName))
844                            {
845                                targetParentCollection = targetParentCollection.createChild(childName, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE);
846                            }
847                            else
848                            {
849                                targetParentCollection = targetParentCollection.getChild(childName);
850                            }
851                        }
852                        
853                        // Copy the attachment resource.
854                        String resourceName = parts[parts.length - 1];
855                        if (baseResource instanceof CopiableAmetysObject)
856                        {
857                            ((CopiableAmetysObject) baseResource).copyTo(targetParentCollection, resourceName);
858                            success = true;
859                            _copyReport.addAttachment(relPath);
860                        }
861                    }
862                }
863            }
864            catch (Exception e)
865            {
866                exception = e;
867            }
868            
869            if (!success)
870            {
871                String warnMsg = "Unable to copy attachment from base path '" + baseResource.getPath() + "' to the content at path : '" + _targetContent.getPath() + "'.";
872                
873                if (_logger.isWarnEnabled())
874                {
875                    if (exception != null)
876                    {
877                        _logger.warn(warnMsg, exception);
878                    }
879                    else
880                    {
881                        _logger.warn(warnMsg);
882                    }
883                }
884            }
885        }
886    }
887}