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    @Callable
276    public Map<String, Object> setContentMetatada(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String metadatapath, List<String> workflowActionIds)
277    {
278        return setContentMetatada(contentIdsToReference, contentIdsToEdit, contentsToEditToRemove, metadatapath, workflowActionIds, new HashMap<>());
279    }
280    
281    /**
282     * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
283     * @param contentIdsToReference The list of content ids that will be added as values in the content field
284     * @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}
285     * @param contentsToEditToRemove  The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove"
286     * @param metadatapath The metadata path selected to do modification in the contentIdsToEdit contents
287     * @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
288     * @param additionalParams the map of additional parameters
289     * @return A map with key success: true or false. if false, it can be due to errors (list of error messages)
290     */
291    @SuppressWarnings("unchecked")
292    @Callable
293    public Map<String, Object> setContentMetatada(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String metadatapath, List<String> workflowActionIds, Map<String, Object> additionalParams)
294    {
295        Map<WorkflowAwareContent, Integer> contentToEdit = (Map<WorkflowAwareContent, Integer>) _resolve(contentIdsToEdit);
296 
297        List<String> errorIds = new ArrayList<>();
298        List<I18nizableText> errorMessages = new ArrayList<>();
299
300        _clean(contentsToEditToRemove, workflowActionIds, errorMessages, errorIds);
301        
302        if (contentIdsToEdit.isEmpty() || contentIdsToReference.isEmpty())
303        {
304            return _returnValue(errorMessages, errorIds);
305        }
306        
307        Collection<String> contentTypesToEdit = new ArrayList<>();
308        for (Content content: contentToEdit.keySet())
309        {
310            Set<String> ancestorsAndMySelf = new HashSet<>();
311            
312            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
313            for (String id : allContentTypes)
314            {
315                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
316                ancestorsAndMySelf.add(id);
317            }
318            
319            if (contentTypesToEdit.isEmpty())
320            {
321                contentTypesToEdit = ancestorsAndMySelf;
322            }
323            else
324            {
325                contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf);
326            }
327        }
328        
329        for (String targetContentTypeName : contentTypesToEdit)
330        {
331            ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
332            MetadataDefinition metadataDef = targetContentType.getMetadataDefinitionByPath(metadatapath);
333            if (metadataDef != null)
334            {
335                _setContentMetatada(contentIdsToReference, contentToEdit, targetContentType, metadatapath, workflowActionIds, errorMessages, errorIds, additionalParams);
336
337                return _returnValue(errorMessages, errorIds);
338            }
339        }
340        
341        throw new IllegalStateException("Unable to find medatata definition to path '" + metadatapath + "'.");
342    }
343    
344    private Map<String, Object> _returnValue(List<I18nizableText> errorMessages, List<String> errorIds)
345    {
346        Map<String, Object> returnValues = new HashMap<>();
347        returnValues.put("success", errorMessages.isEmpty() && errorIds.isEmpty());
348        if (!errorMessages.isEmpty())
349        {
350            returnValues.put("errorMessages", errorMessages);
351        }
352        if (!errorIds.isEmpty())
353        {
354            returnValues.put("errorIds", errorIds);
355        }
356        return returnValues;
357    }
358    
359    private void _clean(List<Map<String, String>> contentsToEditToRemove, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds)
360    {
361        for (Map<String, String> removeObject : contentsToEditToRemove)
362        {
363            String contentIdToEdit = removeObject.get("contentId");
364            String referencingMetadataPath = removeObject.get("referencingMetadataPath");
365            String valueToRemove = removeObject.get("valueToRemove");
366            
367            WorkflowAwareContent content = _resolver.resolveById(contentIdToEdit);
368            
369            Map<String, Object> values = new HashMap<>();
370            values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + referencingMetadataPath + ".mode", MODE.REMOVE.name());
371            
372            MetadataDefinition metadataDef = _ctypesHelper.getMetadataDefinitionByMetadataValuePath(referencingMetadataPath, content);
373            if (metadataDef.isMultiple())
374            {
375                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, Arrays.asList(valueToRemove));
376            }
377            else
378            {
379                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, valueToRemove);
380            }
381            
382            if (getLogger().isDebugEnabled())
383            {
384                getLogger().debug("Content " + contentIdToEdit + " must be edited at " + referencingMetadataPath +  " to remove " + valueToRemove);
385            }
386            
387            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
388        }
389    }
390    
391
392    /**
393     * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
394     * @param contentIdsToReference The list of content ids that will be added as values in the content field
395     * @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}
396     * @param contentType The content type
397     * @param metadataPath The metadata selected to do modification in the contentIdsToEdit contents
398     * @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
399     * @param errorMessages The list that will be felt with error messages of content that had an issue during the operation 
400     * @param errorIds The list that will be felt with ids of content that had an issue during the operation
401     * @param additionalParams the map of additional parameters
402     */
403    protected void _setContentMetatada(List<String> contentIdsToReference, Map<WorkflowAwareContent, Integer> contentToEdit, ContentType contentType, String metadataPath, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds, Map<String, Object> additionalParams)
404    {
405        // On each content
406        for (WorkflowAwareContent content : contentToEdit.keySet())
407        {
408            Map<String, Object> values = new HashMap<>();
409            
410            String metadataName = "";
411            String[] metadataDefPath = StringUtils.split(metadataPath, '/');
412            
413            // Find repeaters in the path
414            String lastRepeaterPath = null;
415            MetadataDefinition metadataDef = null;
416            for (String element : metadataDefPath)
417            {
418                metadataDef = metadataDef == null ? contentType.getMetadataDefinition(element) : metadataDef.getMetadataDefinition(element);
419                metadataName += ("".equals(metadataName) ? "" : ".") + metadataDef.getName();
420                
421                if (metadataDef instanceof RepeaterDefinition)
422                {
423                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".size", "1");
424                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
425                    lastRepeaterPath = metadataName;
426                    metadataName += ".1";
427                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".position", "0"); // 0 means at the end
428                }
429            }
430
431            if (metadataDef == null)
432            {
433                throw new IllegalStateException("Definition cannot be null");
434            }
435            
436            // The value to set
437            if (metadataDef.isMultiple())
438            {
439                Integer newPosition = contentToEdit.get(content);
440                if (newPosition == null || newPosition < 0)
441                {
442                    // Normal case, it is not a move
443                    values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference);
444                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
445                }
446                else
447                {
448                    // Specific case where there is no new content id to reference, but a reorder in a multiple metadata
449                    List<String> currentMetadataValue = new ArrayList<>(Arrays.asList(content.getMetadataHolder().getStringArray(metadataName)));
450                    List<String> reorderedMetadataValue = _reorder(currentMetadataValue, contentIdsToReference, newPosition);
451                    values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, reorderedMetadataValue);
452                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.REPLACE.name());
453                }
454            }
455            else if (lastRepeaterPath != null) // Special case in there is a repeater in the path and the metadata is single valued.
456            {
457                // create as many repeater entries as there is referenced contents.
458                int nbContentIdsToReference = contentIdsToReference.size();
459                values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + ".size", String.valueOf(nbContentIdsToReference));
460                
461                String inRepeaterPath = StringUtils.removeStart(metadataName, lastRepeaterPath + ".1");
462                
463                for (int i = 1; i <= nbContentIdsToReference; i++)
464                {
465                    values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath, contentIdsToReference.get(i - 1));
466                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath + ".mode", MODE.INSERT.name());
467                    
468                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + ".position", "0"); // 0 means at the end
469                }
470            }
471            else
472            {
473                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference.get(0));
474                values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
475            }
476            
477            // Find the edit action to use
478            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
479        }
480    }
481    
482    private List<String> _reorder(List<String> currentElements, List<String> elementsToReorder, int newPosition)
483    {
484        List<String> reorderedList = new ArrayList<>(currentElements);
485        
486        // 1/ in currentElements, replace the ones to reorder by null, in order to keep all indexes
487        for (int i = 0; i < currentElements.size(); i++)
488        {
489            String element = currentElements.get(i);
490            if (elementsToReorder.contains(element))
491            {
492                reorderedList.set(i, null);
493            }
494        }
495        
496        // 2/ insert the elements to reorder at the new position
497        reorderedList.addAll(newPosition, elementsToReorder);
498        
499        // 3/ remove null elements, corresponding to the old positions of the elements that were reordered
500        reorderedList.removeIf(Objects::isNull);
501        
502        return reorderedList;
503    }
504
505    private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages)
506    {
507        Integer actionId = null; 
508        
509        int[] actionIds = _contentWorkflowHelper.getAvailableActions(content);
510        for (String workflowActionIdToTryAsString : workflowActionIds)
511        {
512            Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString);
513            if (ArrayUtils.contains(actionIds, workflowActionIdToTry))
514            {
515                actionId = workflowActionIdToTry;
516                break;
517            }
518        }
519        
520        if (actionId == null)
521        {
522            List<String> parameters = new ArrayList<>();
523            parameters.add(_contentHelper.getTitle(content));
524            parameters.add(content.getName());
525            parameters.add(content.getId());
526            errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_WORKFLOW", parameters));
527            errorIds.add(content.getId());
528        }
529        else
530        {
531            // edit
532            Map<String, Object> contextParameters = new HashMap<>();
533            contextParameters.put("quit", true);
534            contextParameters.put("values", values);
535            
536            Map<String, Object> inputs = new HashMap<>();
537            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
538            
539            try
540            {
541                _contentWorkflowHelper.doAction(content, actionId, inputs);
542            }
543            catch (Exception e)
544            {
545                getLogger().error("Content '" + _contentHelper.getTitle(content) + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e);
546                
547                Map<String, I18nizableText> parameters = new HashMap<>();
548                parameters.put("0", new I18nizableText(_contentHelper.getTitle(content)));
549                parameters.put("1", new I18nizableText(content.getName()));
550                parameters.put("2", new I18nizableText(content.getId()));
551                
552                if (e instanceof InvalidInputWorkflowException)
553                {
554                    I18nizableText rootError = null;
555                    
556                    AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors();
557                    Map<String, Errors> allErrorsMap = allErrors.getAllErrors();
558                    for (String errorMetadataPath : allErrorsMap.keySet())
559                    {
560                        Errors errors = allErrorsMap.get(errorMetadataPath);
561                        
562                        I18nizableText insideError = null;
563                        
564                        List<I18nizableText> errorsAsList = errors.getErrors();
565                        for (I18nizableText error : errorsAsList)
566                        {
567                            Map<String, I18nizableText> i18nparameters = new HashMap<>();
568                            i18nparameters.put("0", error);
569                            
570                            I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA_CHAIN", i18nparameters);
571                            
572                            if (insideError == null)
573                            {
574                                insideError = localError;
575                            }
576                            else
577                            {
578                                insideError.getParameterMap().put("1", localError);
579                            }
580                        }
581
582                        Map<String, I18nizableText> i18ngeneralparameters = new HashMap<>();
583                        
584                        String i18ngeneralkey = null;
585                        if (EditContentFunction.GLOBAL_ERROR_KEY.equals(errorMetadataPath))
586                        {
587                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_GLOBAL_VALIDATION";
588                            i18ngeneralparameters.put("1", insideError);
589                        }
590                        else
591                        {
592                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA";
593                            i18ngeneralparameters.put("0", new I18nizableText(errorMetadataPath));
594                            i18ngeneralparameters.put("1", insideError);
595                        }
596
597                        I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters);
598                        if (rootError == null)
599                        {
600                            rootError = generalError;
601                        }
602                        else
603                        {
604                            rootError.getParameterMap().put("2", generalError);
605                        }
606                    }
607                    
608                    parameters.put("3", rootError);
609                }
610                else
611                {
612                    if (e.getMessage() != null)
613                    {
614                        parameters.put("3", new I18nizableText(e.getMessage()));
615                    }
616                    else
617                    {
618                        parameters.put("3", new I18nizableText(e.getClass().getName()));
619                    }
620                }
621                errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_EDIT", parameters));
622                errorIds.add(content.getId());
623            }
624        }
625    }
626    
627    /**
628     * Resolve content by their ids
629     * @param contentIds The id of contents to resolve
630     * @return the contents
631     */
632    protected List<? extends Content> _resolve(List<String> contentIds)
633    {
634        List<Content> contents = new ArrayList<>();
635
636        for (String contentId: contentIds)
637        {
638            Content content = _resolver.resolveById(contentId);
639            contents.add(content);
640        }
641        
642        return contents;
643    }
644    
645    /**
646     * Resolve content by their ids
647     * @param contentIds The id of contents to resolve
648     * @return the contents
649     */
650    protected Map<? extends Content, Integer> _resolve(Map<String, Integer> contentIds)
651    {
652        Map<Content, Integer> contents = new LinkedHashMap<>();
653        
654        for (Map.Entry<String, Integer> entry: contentIds.entrySet())
655        {
656            Content content = _resolver.resolveById(entry.getKey());
657            contents.put(content, entry.getValue());
658        }
659        
660        return contents;
661    }
662}