001/*
002 *  Copyright 2014 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.clientsideelement.relations;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027import java.util.Set;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.commons.collections.CollectionUtils;
033import org.apache.commons.lang.ArrayUtils;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.cms.content.ContentHelper;
037import org.ametys.cms.contenttype.ContentConstants;
038import org.ametys.cms.contenttype.ContentType;
039import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
040import org.ametys.cms.contenttype.ContentTypesHelper;
041import org.ametys.cms.contenttype.MetadataDefinition;
042import org.ametys.cms.contenttype.MetadataType;
043import org.ametys.cms.contenttype.RepeaterDefinition;
044import org.ametys.cms.form.AbstractField.MODE;
045import org.ametys.cms.repository.Content;
046import org.ametys.cms.repository.WorkflowAwareContent;
047import org.ametys.cms.workflow.AllErrors;
048import org.ametys.cms.workflow.ContentWorkflowHelper;
049import org.ametys.cms.workflow.EditContentFunction;
050import org.ametys.cms.workflow.InvalidInputWorkflowException;
051import org.ametys.core.ui.Callable;
052import org.ametys.core.ui.StaticClientSideRelation;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.workflow.AbstractWorkflowComponent;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.parameter.Errors;
057
058/**
059 * Set the metadata of type 'content' of a content, with another content 
060 */
061public class SetContentMetadataClientSideElement extends StaticClientSideRelation implements Component
062{
063    /** The Ametys object resolver */
064    protected AmetysObjectResolver _resolver;
065    /** The content type helper */
066    protected ContentTypesHelper _ctypesHelper;
067    /** The content types extension point */
068    protected ContentTypeExtensionPoint _ctypesEP;
069    /** The content workflow helper */
070    protected ContentWorkflowHelper _contentWorkflowHelper;
071    /** The EP to filter meta */
072    protected FilterCompatibleContentMetadataExtensionPoint _filterCompatibleContentMetadataExtensionPoint;
073    /** The content helper */
074    protected ContentHelper _contentHelper;
075
076    @Override
077    public void service(ServiceManager manager) throws ServiceException
078    {
079        super.service(manager);
080        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
081        _ctypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
082        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
083        _ctypesEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
084        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
085        _filterCompatibleContentMetadataExtensionPoint = (FilterCompatibleContentMetadataExtensionPoint) manager.lookup(FilterCompatibleContentMetadataExtensionPoint.ROLE);
086    }
087    
088    /**
089     * Find a metadata of type content in definition of 'contentIdsToReference' and set the metadata of contents 'contentIdsToEdit' with value 'contentIdsToReference'
090     * @param contentIdsToReference The list of content ids that will be added as values in the content field
091     * @param contentIdsToEdit The list of content ids to edit and that will have a metadata of type content modified
092     * @return A map with key success: true or false. if false, it can be due to errors (list of error messages) or because metadata cannot be determined then 'metadata' is a list of possible metadata
093     */
094    @Callable
095    public Map<String, Object> getCompatibleMetadata(List<String> contentIdsToReference, List<String> contentIdsToEdit)
096    {
097        @SuppressWarnings("unchecked")
098        List<Content> contentToReference = (List<Content>) _resolve(contentIdsToReference);
099        @SuppressWarnings("unchecked")
100        List<Content> contentToEdit = (List<Content>) _resolve(contentIdsToEdit);
101
102        Set<MetadataDefinition> metadatadefs = _findCompatibleMetadata(contentToReference, contentToEdit);
103        
104        Map<String, Object> returnValues = new HashMap<>();
105        returnValues.put("success", false);
106        returnValues.put("metadata", _convert(metadatadefs));
107        return returnValues;
108    }
109    
110    /**
111     * Convert metadata definitions to JSON object
112     * @param metadatadefs The metadata definitions
113     * @return the JSON object
114     */
115    protected List<Map<String, Object>> _convert(Set<MetadataDefinition> metadatadefs)
116    {
117        List<Map<String, Object>> metadatapaths = new ArrayList<>(); 
118        
119        for (MetadataDefinition metadataDef : metadatadefs) 
120        {
121            metadatapaths.add(_convert(metadataDef));
122        }
123        
124        return metadatapaths;
125    }
126
127    /**
128     * Convert a metadata definition to JSON
129     * @param metadatadef the metadata definition
130     * @return the JSON object
131     */
132    protected Map<String, Object> _convert(MetadataDefinition metadatadef)
133    {
134        String metaPath = metadatadef.getId();
135        
136        Map<String, Object> def = new HashMap<>();
137        def.put("id", metaPath);
138        def.put("name", metadatadef.getName());
139        def.put("label", metadatadef.getLabel());
140        def.put("description", metadatadef.getDescription());
141
142        if (metaPath.contains(ContentConstants.METADATA_PATH_SEPARATOR))
143        {
144            String parentPath = StringUtils.substringBeforeLast(metaPath, ContentConstants.METADATA_PATH_SEPARATOR);
145            
146            String cTypeId = metadatadef.getReferenceContentType();
147            ContentType cType = _ctypesEP.getExtension(cTypeId);
148            
149            MetadataDefinition parentMetadatadef = cType.getMetadataDefinitionByPath(parentPath);
150            def.put("parent", _convert(parentMetadatadef));
151        }
152        
153        return def;
154    }
155    
156    /**
157     * Find the list of compatible metadata definition
158     * @param contentToReference the contents to reference
159     * @param contentToEdit the contents to edit
160     * @return the list of compatible metadata definition
161     */
162    protected Set<MetadataDefinition> _findCompatibleMetadata(List<Content> contentToReference, List<Content> contentToEdit)
163    {
164        // First we need to find the type of the target metadata we are looking for
165        Collection<String> contentTypesToReference = new HashSet<>();
166        for (Content content: contentToReference)
167        {
168            Set<String> ancestorsAndMySelf = new HashSet<>();
169            
170            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
171            for (String id : allContentTypes)
172            {
173                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
174                ancestorsAndMySelf.add(id);
175            }
176            
177            if (contentTypesToReference.isEmpty())
178            {
179                contentTypesToReference = ancestorsAndMySelf;
180            }
181            else
182            {
183                contentTypesToReference = CollectionUtils.intersection(contentTypesToReference, ancestorsAndMySelf);
184            }
185        }
186
187        // Second we need to know if this metadata will be mulitple or not
188        boolean requiresMultiple = contentToReference.size() > 1;
189        
190        // Third we need to know the target content type
191        Collection<String> contentTypesToEdit = new ArrayList<>();
192        for (Content content: contentToEdit)
193        {
194            Set<String> ancestorsAndMySelf = new HashSet<>();
195            
196            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
197            for (String id : allContentTypes)
198            {
199                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
200                ancestorsAndMySelf.add(id);
201            }
202            
203            if (contentTypesToEdit.isEmpty())
204            {
205                contentTypesToEdit = ancestorsAndMySelf;
206            }
207            else
208            {
209                contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf);
210            }
211        }
212        
213        // Now lets navigate in the target content type to find a content metadata limited to the metadata content type (or its parent types), that is multiple if necessary
214        Set<MetadataDefinition> metadata = new HashSet<>();
215        
216        for (String targetContentTypeName : contentTypesToEdit)
217        {
218            ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
219            for (String metadataName : targetContentType.getMetadataNames())
220            {
221                MetadataDefinition metaDef = targetContentType.getMetadataDefinition(metadataName);
222                metadata.addAll(_findCompatibleMetadata(contentToReference, contentToEdit, targetContentTypeName, metaDef, false, contentTypesToReference, requiresMultiple));
223            }
224        }
225        
226        return metadata;
227    }
228    
229    private Set<MetadataDefinition> _findCompatibleMetadata(List<Content> contentToReference, List<Content> contentToEdit, String targetContentTypeName, MetadataDefinition metaDef, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple)
230    {
231        Set<MetadataDefinition> metadata = new HashSet<>();
232        
233        if (MetadataType.CONTENT.equals(metaDef.getType()) 
234                && (metaDef.getContentType() == null || compatibleContentTypes.contains(metaDef.getContentType()))
235                && (!requiresMultiple || metaDef.isMultiple() || anyParentIsMultiple)
236                && metaDef.getReferenceContentType().equals(targetContentTypeName)
237                && _filterCompatibleContentMetadataExtensionPoint.filter(targetContentTypeName, metaDef, contentToReference, compatibleContentTypes)
238                && _hasRight(contentToEdit, metaDef))
239        {
240            metadata.add(metaDef);
241        }
242        else if (MetadataType.COMPOSITE.equals(metaDef.getType()))
243        {
244            for (String subMetadataName : metaDef.getMetadataNames())
245            {
246                MetadataDefinition subMetaDef = metaDef.getMetadataDefinition(subMetadataName);
247                metadata.addAll(_findCompatibleMetadata(contentToReference, contentToEdit, targetContentTypeName, subMetaDef, anyParentIsMultiple || metaDef instanceof RepeaterDefinition, compatibleContentTypes, requiresMultiple));
248            }
249        }
250        
251        return metadata;
252    }
253    
254    private boolean _hasRight (List<Content> contentToEdit, MetadataDefinition metaDef)
255    {
256        for (Content content : contentToEdit)
257        {
258            if (!_ctypesHelper.canWrite(content, metaDef))
259            {
260                return false;
261            }
262        }
263        return true;
264    }
265
266    /**
267     * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
268     * @param contentIdsToReference The list of content ids that will be added as values in the content field
269     * @param contentIdsToEdit The map {key: content ids to edit and that will have a metadata of type content modified; value: the new position if metadata is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder}
270     * @param contentsToEditToRemove  The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove"
271     * @param metadatapath The metadata path selected to do modification in the contentIdsToEdit contents
272     * @param workflowActionIds The ids of workflow actions to use to edit the metadata. Actions will be tested in this order and first available action will be used
273     * @return A map with key success: true or false. if false, it can be due to errors (list of error messages)
274     */
275    @SuppressWarnings("unchecked")
276    @Callable
277    public Map<String, Object> setContentMetatada(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String metadatapath, List<String> workflowActionIds)
278    {
279        Map<WorkflowAwareContent, Integer> contentToEdit = (Map<WorkflowAwareContent, Integer>) _resolve(contentIdsToEdit);
280 
281        List<String> errorIds = new ArrayList<>();
282        List<I18nizableText> errorMessages = new ArrayList<>();
283
284        _clean(contentsToEditToRemove, workflowActionIds, errorMessages, errorIds);
285        
286        if (contentIdsToEdit.isEmpty() || contentIdsToReference.isEmpty())
287        {
288            return _returnValue(errorMessages, errorIds);
289        }
290        
291        Collection<String> contentTypesToEdit = new ArrayList<>();
292        for (Content content: contentToEdit.keySet())
293        {
294            Set<String> ancestorsAndMySelf = new HashSet<>();
295            
296            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
297            for (String id : allContentTypes)
298            {
299                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
300                ancestorsAndMySelf.add(id);
301            }
302            
303            if (contentTypesToEdit.isEmpty())
304            {
305                contentTypesToEdit = ancestorsAndMySelf;
306            }
307            else
308            {
309                contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf);
310            }
311        }
312        
313        for (String targetContentTypeName : contentTypesToEdit)
314        {
315            ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
316            MetadataDefinition metadataDef = targetContentType.getMetadataDefinitionByPath(metadatapath);
317            if (metadataDef != null)
318            {
319                _setContentMetatada(contentIdsToReference, contentToEdit, targetContentType, metadatapath, workflowActionIds, errorMessages, errorIds);
320
321                return _returnValue(errorMessages, errorIds);
322            }
323        }
324        
325        throw new IllegalStateException("Unable to find medatata definition to path '" + metadatapath + "'.");
326    }
327    
328    private Map<String, Object> _returnValue(List<I18nizableText> errorMessages, List<String> errorIds)
329    {
330        Map<String, Object> returnValues = new HashMap<>();
331        returnValues.put("success", errorMessages.isEmpty() && errorIds.isEmpty());
332        if (!errorMessages.isEmpty())
333        {
334            returnValues.put("errorMessages", errorMessages);
335        }
336        if (!errorIds.isEmpty())
337        {
338            returnValues.put("errorIds", errorIds);
339        }
340        return returnValues;
341    }
342    
343    private void _clean(List<Map<String, String>> contentsToEditToRemove, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds)
344    {
345        for (Map<String, String> removeObject : contentsToEditToRemove)
346        {
347            String contentIdToEdit = removeObject.get("contentId");
348            String referencingMetadataPath = removeObject.get("referencingMetadataPath");
349            String valueToRemove = removeObject.get("valueToRemove");
350            
351            WorkflowAwareContent content = _resolver.resolveById(contentIdToEdit);
352            
353            Map<String, Object> values = new HashMap<>();
354            values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + referencingMetadataPath + ".mode", MODE.REMOVE.name());
355            
356            MetadataDefinition metadataDef = _ctypesHelper.getMetadataDefinitionByMetadataValuePath(referencingMetadataPath, content);
357            if (metadataDef.isMultiple())
358            {
359                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, Arrays.asList(valueToRemove));
360            }
361            else
362            {
363                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, valueToRemove);
364            }
365            
366            if (getLogger().isDebugEnabled())
367            {
368                getLogger().debug("Content " + contentIdToEdit + " must be edited at " + referencingMetadataPath +  " to remove " + valueToRemove);
369            }
370            
371            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
372        }
373    }
374    
375
376    /**
377     * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
378     * @param contentIdsToReference The list of content ids that will be added as values in the content field
379     * @param contentToEdit The map {key: contents to edit and that will have a metadata of type content modified; value: the new position if metadata is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder}
380     * @param contentType The content type
381     * @param metadataPath The metadata selected to do modification in the contentIdsToEdit contents
382     * @param workflowActionIds The ids of workflow actions to use to edit the metadata. Actions will be tested in this order and first available action will be used
383     * @param errorMessages The list that will be felt with error messages of content that had an issue during the operation 
384     * @param errorIds The list that will be felt with ids of content that had an issue during the operation
385     */
386    protected void _setContentMetatada(List<String> contentIdsToReference, Map<WorkflowAwareContent, Integer> contentToEdit, ContentType contentType, String metadataPath, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds)
387    {
388        // On each content
389        for (WorkflowAwareContent content : contentToEdit.keySet())
390        {
391            Map<String, Object> values = new HashMap<>();
392            
393            String metadataName = "";
394            String[] metadataDefPath = StringUtils.split(metadataPath, '/');
395            
396            // Find repeaters in the path
397            String lastRepeaterPath = null;
398            MetadataDefinition metadataDef = null;
399            for (String element : metadataDefPath)
400            {
401                metadataDef = metadataDef == null ? contentType.getMetadataDefinition(element) : metadataDef.getMetadataDefinition(element);
402                metadataName += ("".equals(metadataName) ? "" : ".") + metadataDef.getName();
403                
404                if (metadataDef instanceof RepeaterDefinition)
405                {
406                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".size", "1");
407                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
408                    lastRepeaterPath = metadataName;
409                    metadataName += ".1";
410                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".position", "0"); // 0 means at the end
411                }
412            }
413
414            if (metadataDef == null)
415            {
416                throw new IllegalStateException("Definition cannot be null");
417            }
418            
419            // The value to set
420            if (metadataDef.isMultiple())
421            {
422                Integer newPosition = contentToEdit.get(content);
423                if (newPosition == null || newPosition < 0)
424                {
425                    // Normal case, it is not a move
426                    values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference);
427                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
428                }
429                else
430                {
431                    // Specific case where there is no new content id to reference, but a reorder in a multiple metadata
432                    List<String> currentMetadataValue = new ArrayList<>(Arrays.asList(content.getMetadataHolder().getStringArray(metadataName)));
433                    List<String> reorderedMetadataValue = _reorder(currentMetadataValue, contentIdsToReference, newPosition);
434                    values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, reorderedMetadataValue);
435                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.REPLACE.name());
436                }
437            }
438            else if (lastRepeaterPath != null) // Special case in there is a repeater in the path and the metadata is single valued.
439            {
440                // create as many repeater entries as there is referenced contents.
441                int nbContentIdsToReference = contentIdsToReference.size();
442                values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + ".size", String.valueOf(nbContentIdsToReference));
443                
444                String inRepeaterPath = StringUtils.removeStart(metadataName, lastRepeaterPath + ".1");
445                
446                for (int i = 1; i <= nbContentIdsToReference; i++)
447                {
448                    values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath, contentIdsToReference.get(i - 1));
449                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath + ".mode", MODE.INSERT.name());
450                    
451                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + ".position", "0"); // 0 means at the end
452                }
453            }
454            else
455            {
456                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference.get(0));
457                values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
458            }
459            
460            // Find the edit action to use
461            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
462        }
463    }
464    
465    private List<String> _reorder(List<String> currentElements, List<String> elementsToReorder, int newPosition)
466    {
467        List<String> reorderedList = new ArrayList<>(currentElements);
468        
469        // 1/ in currentElements, replace the ones to reorder by null, in order to keep all indexes
470        for (int i = 0; i < currentElements.size(); i++)
471        {
472            String element = currentElements.get(i);
473            if (elementsToReorder.contains(element))
474            {
475                reorderedList.set(i, null);
476            }
477        }
478        
479        // 2/ insert the elements to reorder at the new position
480        reorderedList.addAll(newPosition, elementsToReorder);
481        
482        // 3/ remove null elements, corresponding to the old positions of the elements that were reordered
483        reorderedList.removeIf(Objects::isNull);
484        
485        return reorderedList;
486    }
487
488    private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages)
489    {
490        Integer actionId = null; 
491        
492        int[] actionIds = _contentWorkflowHelper.getAvailableActions(content);
493        for (String workflowActionIdToTryAsString : workflowActionIds)
494        {
495            Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString);
496            if (ArrayUtils.contains(actionIds, workflowActionIdToTry))
497            {
498                actionId = workflowActionIdToTry;
499                break;
500            }
501        }
502        
503        if (actionId == null)
504        {
505            List<String> parameters = new ArrayList<>();
506            parameters.add(_contentHelper.getTitle(content));
507            parameters.add(content.getName());
508            parameters.add(content.getId());
509            errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_WORKFLOW", parameters));
510            errorIds.add(content.getId());
511        }
512        else
513        {
514            // edit
515            Map<String, Object> contextParameters = new HashMap<>();
516            contextParameters.put("quit", true);
517            contextParameters.put("values", values);
518            
519            Map<String, Object> inputs = new HashMap<>();
520            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
521            
522            try
523            {
524                _contentWorkflowHelper.doAction(content, actionId, inputs);
525            }
526            catch (Exception e)
527            {
528                getLogger().error("Content '" + _contentHelper.getTitle(content) + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e);
529                
530                Map<String, I18nizableText> parameters = new HashMap<>();
531                parameters.put("0", new I18nizableText(_contentHelper.getTitle(content)));
532                parameters.put("1", new I18nizableText(content.getName()));
533                parameters.put("2", new I18nizableText(content.getId()));
534                
535                if (e instanceof InvalidInputWorkflowException)
536                {
537                    I18nizableText rootError = null;
538                    
539                    AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors();
540                    Map<String, Errors> allErrorsMap = allErrors.getAllErrors();
541                    for (String errorMetadataPath : allErrorsMap.keySet())
542                    {
543                        Errors errors = allErrorsMap.get(errorMetadataPath);
544                        
545                        I18nizableText insideError = null;
546                        
547                        List<I18nizableText> errorsAsList = errors.getErrors();
548                        for (I18nizableText error : errorsAsList)
549                        {
550                            Map<String, I18nizableText> i18nparameters = new HashMap<>();
551                            i18nparameters.put("0", error);
552                            
553                            I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA_CHAIN", i18nparameters);
554                            
555                            if (insideError == null)
556                            {
557                                insideError = localError;
558                            }
559                            else
560                            {
561                                insideError.getParameterMap().put("1", localError);
562                            }
563                        }
564
565                        Map<String, I18nizableText> i18ngeneralparameters = new HashMap<>();
566                        
567                        String i18ngeneralkey = null;
568                        if (EditContentFunction.GLOBAL_ERROR_KEY.equals(errorMetadataPath))
569                        {
570                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_GLOBAL_VALIDATION";
571                            i18ngeneralparameters.put("1", insideError);
572                        }
573                        else
574                        {
575                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA";
576                            i18ngeneralparameters.put("0", new I18nizableText(errorMetadataPath));
577                            i18ngeneralparameters.put("1", insideError);
578                        }
579
580                        I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters);
581                        if (rootError == null)
582                        {
583                            rootError = generalError;
584                        }
585                        else
586                        {
587                            rootError.getParameterMap().put("2", generalError);
588                        }
589                    }
590                    
591                    parameters.put("3", rootError);
592                }
593                else
594                {
595                    if (e.getMessage() != null)
596                    {
597                        parameters.put("3", new I18nizableText(e.getMessage()));
598                    }
599                    else
600                    {
601                        parameters.put("3", new I18nizableText(e.getClass().getName()));
602                    }
603                }
604                errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_EDIT", parameters));
605                errorIds.add(content.getId());
606            }
607        }
608    }
609    
610    /**
611     * Resolve content by their ids
612     * @param contentIds The id of contents to resolve
613     * @return the contents
614     */
615    protected List<? extends Content> _resolve(List<String> contentIds)
616    {
617        List<Content> contents = new ArrayList<>();
618
619        for (String contentId: contentIds)
620        {
621            Content content = _resolver.resolveById(contentId);
622            contents.add(content);
623        }
624        
625        return contents;
626    }
627    
628    /**
629     * Resolve content by their ids
630     * @param contentIds The id of contents to resolve
631     * @return the contents
632     */
633    protected Map<? extends Content, Integer> _resolve(Map<String, Integer> contentIds)
634    {
635        Map<Content, Integer> contents = new LinkedHashMap<>();
636        
637        for (Map.Entry<String, Integer> entry: contentIds.entrySet())
638        {
639            Content content = _resolver.resolveById(entry.getKey());
640            contents.put(content, entry.getValue());
641        }
642        
643        return contents;
644    }
645}