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;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.commons.collections.CollectionUtils;
034import org.apache.commons.lang.ArrayUtils;
035
036import org.ametys.cms.content.ContentHelper;
037import org.ametys.cms.contenttype.ContentAttributeDefinition;
038import org.ametys.cms.contenttype.ContentType;
039import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
040import org.ametys.cms.contenttype.ContentTypesHelper;
041import org.ametys.cms.data.ContentValue;
042import org.ametys.cms.model.restrictions.RestrictedModelItem;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.repository.WorkflowAwareContent;
045import org.ametys.cms.workflow.AllErrors;
046import org.ametys.cms.workflow.ContentWorkflowHelper;
047import org.ametys.cms.workflow.EditContentFunction;
048import org.ametys.cms.workflow.InvalidInputWorkflowException;
049import org.ametys.core.ui.Callable;
050import org.ametys.core.ui.StaticClientSideRelation;
051import org.ametys.plugins.repository.AmetysObjectResolver;
052import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
053import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
054import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode;
055import org.ametys.plugins.repository.model.RepeaterDefinition;
056import org.ametys.plugins.workflow.AbstractWorkflowComponent;
057import org.ametys.runtime.i18n.I18nizableTextParameter;
058import org.ametys.runtime.i18n.I18nizableText;
059import org.ametys.runtime.model.ElementDefinition;
060import org.ametys.runtime.model.ModelHelper;
061import org.ametys.runtime.model.ModelItem;
062import org.ametys.runtime.model.ModelItemContainer;
063import org.ametys.runtime.model.exception.UndefinedItemPathException;
064import org.ametys.runtime.parameter.Errors;
065
066/**
067 * Set the attribute of type 'content' of a content, with another content 
068 */
069public class SetContentAttributeClientSideElement extends StaticClientSideRelation implements Component
070{
071    /** The Ametys object resolver */
072    protected AmetysObjectResolver _resolver;
073    /** The content type helper */
074    protected ContentTypesHelper _contentTypesHelper;
075    /** The content types extension point */
076    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
077    /** The content workflow helper */
078    protected ContentWorkflowHelper _contentWorkflowHelper;
079    /** The content helper */
080    protected ContentHelper _contentHelper;
081
082    @Override
083    public void service(ServiceManager manager) throws ServiceException
084    {
085        super.service(manager);
086        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
087        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
088        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
089        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
090        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
091    }
092    
093    /**
094     * Find an attribute of type content in definition of 'contentIdsToReference' and set the attribute of contents 'contentIdsToEdit' with value 'contentIdsToReference'
095     * @param contentIdsToReference The list of content identifiers that will be added as values in the content field
096     * @param contentIdsToEdit The list of content identifiers to edit and that will have an attribute of type content modified
097     * @return the list of all compatible attribute definitions
098     */
099    @Callable
100    public List<Map<String, Object>> getCompatibleAttributes(List<String> contentIdsToReference, List<String> contentIdsToEdit)
101    {
102        List<? extends Content> contentsToReference = _resolve(contentIdsToReference);
103        List<? extends Content> contentsToEdit = _resolve(contentIdsToEdit);
104
105        Set<ModelItem> compatibleAtributes = _findCompatibleAttributes(contentsToReference, contentsToEdit);
106        
107        return _convert(compatibleAtributes);
108    }
109    
110    /**
111     * Convert attribute definitions to JSON object
112     * @param attributeDefinitions The attribute definitions
113     * @return the JSON object
114     */
115    protected List<Map<String, Object>> _convert(Set<ModelItem> attributeDefinitions)
116    {
117        List<Map<String, Object>> attributeInfo = new ArrayList<>(); 
118        
119        for (ModelItem attributeDefinition : attributeDefinitions) 
120        {
121            attributeInfo.add(_convert(attributeDefinition));
122        }
123        
124        return attributeInfo;
125    }
126
127    /**
128     * Convert an attribute definition to JSON
129     * @param attributeDefinition the attribute definition
130     * @return the JSON object
131     */
132    protected Map<String, Object> _convert(ModelItem attributeDefinition)
133    {
134        String attributePath = attributeDefinition.getPath();
135        
136        Map<String, Object> definition = new HashMap<>();
137        definition.put("path", attributePath);
138        definition.put("name", attributeDefinition.getName());
139        definition.put("label", attributeDefinition.getLabel());
140        definition.put("description", attributeDefinition.getDescription());
141
142        if (attributePath.contains(ModelItem.ITEM_PATH_SEPARATOR))
143        {
144            ModelItem parentMetadatadef = attributeDefinition.getParent();
145            definition.put("parent", _convert(parentMetadatadef));
146        }
147        
148        return definition;
149    }
150    
151    /**
152     * Find the list of compatible attribute definitions
153     * @param contentsToReference the contents to reference
154     * @param contentsToEdit the contents to edit
155     * @return the list of compatible attribute definitions
156     */
157    protected Set<ModelItem> _findCompatibleAttributes(List<? extends Content> contentsToReference, List<? extends Content> contentsToEdit)
158    {
159        // First we need to find the type of the target attribute we are looking for
160        Collection<String> contentTypesToReference = _getContentTypesIntersection(contentsToReference);
161
162        // Second we need to know if this attribute will be multiple or not
163        boolean requiresMultiple = contentsToReference.size() > 1;
164        
165        // Third we need to know the target content type
166        Collection<String> contentTypesToEdit = _getContentTypesIntersection(contentsToEdit);
167        
168        // Now lets navigate in the target content types to find an attribute of type content limited to the references content types (or its parent types), that is multiple if necessary
169        Set<ModelItem> compatibleAttributes = new HashSet<>();
170        
171        for (String targetContentTypeId : contentTypesToEdit)
172        {
173            ContentType targetContentType = _contentTypeExtensionPoint.getExtension(targetContentTypeId);
174            for (ModelItem modelItem : targetContentType.getModelItems())
175            {
176                compatibleAttributes.addAll(_findCompatibleAttributes(contentsToReference, contentsToEdit, targetContentTypeId, modelItem, false, contentTypesToReference, requiresMultiple));
177            }
178        }
179        
180        return compatibleAttributes;
181    }
182    
183    private Set<ModelItem> _findCompatibleAttributes(List<? extends Content> contentsToReference, List<? extends Content> contentsToEdit, String targetContentTypeId, ModelItem modelItem, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple)
184    {
185        Set<ModelItem> compatibleAttributes = new HashSet<>();
186        
187        if (modelItem instanceof ContentAttributeDefinition) 
188        {
189            if (_isAttributeCompatible(contentsToEdit, targetContentTypeId, (ContentAttributeDefinition) modelItem, anyParentIsMultiple, compatibleContentTypes, requiresMultiple))
190            {
191                compatibleAttributes.add(modelItem);
192            }
193        }
194        else if (modelItem instanceof ModelItemContainer)
195        {
196            for (ModelItem child : ((ModelItemContainer) modelItem).getModelItems())
197            {
198                compatibleAttributes.addAll(_findCompatibleAttributes(contentsToReference, contentsToEdit, targetContentTypeId, child, anyParentIsMultiple || modelItem instanceof RepeaterDefinition, compatibleContentTypes, requiresMultiple));
199            }
200        }
201        
202        return compatibleAttributes;
203    }
204    
205    private boolean _isAttributeCompatible(List<? extends Content> contentsToEdit, String targetContentTypeId, ContentAttributeDefinition attributeDefinition, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple)
206    {
207        String contentTypeId = attributeDefinition.getContentTypeId();
208        
209        return (contentTypeId == null || compatibleContentTypes.contains(contentTypeId))
210                && (!requiresMultiple || attributeDefinition.isMultiple() || anyParentIsMultiple)
211                && attributeDefinition.getModel().getId().equals(targetContentTypeId)
212                && _hasRight(contentsToEdit, attributeDefinition);
213    }
214    
215    private boolean _hasRight (List<? extends Content> contentsToEdit, RestrictedModelItem<Content> modelItem)
216    {
217        for (Content content : contentsToEdit)
218        {
219            if (!modelItem.canWrite(content))
220            {
221                return false;
222            }
223        }
224        return true;
225    }
226
227    /**
228     * Set the attribute at path 'attributePath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
229     * @param contentIdsToReference The list of content identifiers that will be added as values in the content field
230     * @param contentIdsToEdit The map {key: content identifiers to edit and that will have an attribute of type content modified; value: the new position if attribute is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder}
231     * @param contentsToEditToRemove  The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove"
232     * @param attributePath The attribute path selected to do modification in the contents to edit
233     * @param workflowActionIds The identifiers of workflow actions to use to edit the attribute. Actions will be tested in this order and first available action will be used
234     * @return A map with key success: true or false. if false, it can be due to errors (list of error messages)
235     */
236    @Callable
237    public Map<String, Object> setContentAttribute(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String attributePath, List<String> workflowActionIds)
238    {
239        return setContentAttribute(contentIdsToReference, contentIdsToEdit, contentsToEditToRemove, attributePath, workflowActionIds, new HashMap<>());
240    }
241    
242    /**
243     * Set the attribute at path 'attributePath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
244     * @param contentIdsToReference The list of content identifiers that will be added as values in the content field
245     * @param contentIdsToEdit The map {key: content identifiers to edit and that will have an attribute of type content modified; value: the new position if attribute is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder}
246     * @param contentsToEditToRemove  The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove"
247     * @param attributePath The attribute path selected to do modification in the contents to edit
248     * @param workflowActionIds The identifiers of workflow actions to use to edit the attribute. Actions will be tested in this order and first available action will be used
249     * @param additionalParams the map of additional parameters
250     * @return A map with key success: true or false. if false, it can be due to errors (list of error messages)
251     */
252    @SuppressWarnings("unchecked")
253    @Callable
254    public Map<String, Object> setContentAttribute(List<String> contentIdsToReference, Map<String, Integer> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String attributePath, List<String> workflowActionIds, Map<String, Object> additionalParams)
255    {
256        Map<WorkflowAwareContent, Integer> contentsToEdit = (Map<WorkflowAwareContent, Integer>) _resolve(contentIdsToEdit);
257 
258        List<String> errorIds = new ArrayList<>();
259        List<I18nizableText> errorMessages = new ArrayList<>();
260
261        if (!contentsToEditToRemove.isEmpty())
262        {
263            // There are some relations to remove, so we consider the content has been moving
264            additionalParams.put("mode", "move");
265        }
266        
267        // We filter contents to edit. If some error occurs, the relation is not cleaned and we return the errors
268        // Default implementation returns the same map of contents to edit with no errors
269        Map<WorkflowAwareContent, Integer> filteredContentsToEdit = _filterContentsToEdit(contentsToEdit, contentIdsToReference, errorMessages, errorIds, additionalParams);
270        if (!errorIds.isEmpty() || !errorMessages.isEmpty())
271        {
272            // If some error occurs, return the errors
273            return _returnValue(errorMessages, errorIds);
274        }
275        
276        // Clean the old relations
277        _clean(contentsToEditToRemove, workflowActionIds, errorMessages, errorIds);
278        if (filteredContentsToEdit.isEmpty() || contentIdsToReference.isEmpty())
279        {
280            return _returnValue(errorMessages, errorIds);
281        }
282        
283        // Get the content types of target contents
284        Collection<String> contentTypeIdsToEdit = _getContentTypesIntersection(filteredContentsToEdit.keySet());
285        List<ContentType> contentTypesToEdit = contentTypeIdsToEdit.stream()
286            .map(id -> _contentTypeExtensionPoint.getExtension(id))
287            .collect(Collectors.toList());
288        
289        try
290        {
291            ModelItem attributeDefinition = ModelHelper.getModelItem(attributePath, contentTypesToEdit);
292            // Get the content type holding this attribute
293            ContentType targetContentType = _contentTypeExtensionPoint.getExtension(attributeDefinition.getModel().getId());
294            _setContentAttribute(contentIdsToReference, filteredContentsToEdit, targetContentType, attributePath, workflowActionIds, errorMessages, errorIds, additionalParams);
295            return _returnValue(errorMessages, errorIds);
296        }
297        catch (UndefinedItemPathException e)
298        {
299            throw new IllegalStateException("Unable to set attribute at path '" + attributePath + "'.", e);
300        }
301    }
302    
303    /**
304     * Filter the list of contents to edit
305     * @param contentsToEdit the map of contents to edit
306     * @param contentIdsToReference The list of content ids that will be added as values in the content field
307     * @param errorMessages the error messages
308     * @param errorIds the error content ids
309     * @param additionalParams the map of additional parameters
310     * @return the list of filtered contents
311     */
312    protected Map<WorkflowAwareContent, Integer> _filterContentsToEdit(Map<WorkflowAwareContent, Integer> contentsToEdit, List<String> contentIdsToReference, List<I18nizableText> errorMessages, List<String> errorIds, Map<String, Object> additionalParams)
313    {
314        // Default implementation
315        return contentsToEdit;
316    }
317
318    private Map<String, Object> _returnValue(List<I18nizableText> errorMessages, List<String> errorIds)
319    {
320        Map<String, Object> returnValues = new HashMap<>();
321        returnValues.put("success", errorMessages.isEmpty() && errorIds.isEmpty());
322        if (!errorMessages.isEmpty())
323        {
324            returnValues.put("errorMessages", errorMessages);
325        }
326        if (!errorIds.isEmpty())
327        {
328            returnValues.put("errorIds", errorIds);
329        }
330        return returnValues;
331    }
332    
333    private void _clean(List<Map<String, String>> contentsToEditToRemove, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds)
334    {
335        for (Map<String, String> removeObject : contentsToEditToRemove)
336        {
337            String contentIdToEdit = removeObject.get("contentId");
338            String referencingAttributePath = removeObject.get("referencingAttributePath");
339            String valueToRemove = removeObject.get("valueToRemove");
340            
341            WorkflowAwareContent content = _resolver.resolveById(contentIdToEdit);
342            
343            List<ModelItem> items = ModelHelper.getAllModelItemsInPath(referencingAttributePath, content.getModel());
344            
345            Object valuesToRemove;
346            ModelItem attributeDefinition = items.get(items.size() - 1);
347            ContentValue contentValueToRemove = new ContentValue(_resolver, valueToRemove);
348            if (attributeDefinition instanceof ElementDefinition && ((ElementDefinition) attributeDefinition).isMultiple())
349            {
350                valuesToRemove = new ContentValue[] {contentValueToRemove};
351            }
352            else
353            {
354                valuesToRemove = contentValueToRemove;
355            }
356            
357            SynchronizableValue value = new SynchronizableValue(valuesToRemove);
358            value.setMode(Mode.REMOVE);
359            
360            Map<String, Object> values = Map.of(attributeDefinition.getName(), value);
361            for (int i = items.size() - 2; i >= 0; i--)
362            {
363                ModelItem item = items.get(i);
364                if (item instanceof RepeaterDefinition)
365                {
366                    throw new IllegalArgumentException("SetContentAttributeClientSideElement does not support a path containing repeater for removing references");
367                }
368                
369                values = Map.of(item.getName(), values);
370            }
371            
372            if (getLogger().isDebugEnabled())
373            {
374                getLogger().debug("Content " + contentIdToEdit + " must be edited at " + referencingAttributePath +  " to remove " + valueToRemove);
375            }
376            
377            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
378        }
379    }
380    
381
382    /**
383     * Set the attribute at path 'attributePath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
384     * @param contentIdsToReference The list of content identifiers that will be added as values in the content field
385     * @param contentsToEdit The map {key: contents to edit and that will have an attribute of type content modified; value: the new position if attribute is multiple and it is a reorder of values. May be null or equals to -1 if it is not a reorder}
386     * @param contentType The content type
387     * @param attributePath The attribute path selected to do modification in the contents to edit
388     * @param workflowActionIds The identifiers of workflow actions to use to edit the attribute. Actions will be tested in this order and first available action will be used
389     * @param errorMessages The list that will be felt with error messages of content that had an issue during the operation 
390     * @param errorIds The list that will be felt with ids of content that had an issue during the operation
391     * @param additionalParams the map of additional parameters
392     */
393    protected void _setContentAttribute(List<String> contentIdsToReference, Map<WorkflowAwareContent, Integer> contentsToEdit, ContentType contentType, String attributePath, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds, Map<String, Object> additionalParams)
394    {
395        // On each content
396        for (WorkflowAwareContent content : contentsToEdit.keySet())
397        {
398            List<ModelItem> items = ModelHelper.getAllModelItemsInPath(attributePath, content.getModel());
399            int size = items.size();
400
401            ModelItem attributeDefinition = items.get(size - 1);
402            if (!(attributeDefinition instanceof ContentAttributeDefinition))
403            {
404                throw new IllegalStateException("No definition of type content found for path '" + attributePath + "' in the content type '" + contentType.getId() + "'.");
405            }
406            
407            // find the last repeater in path
408            int index = size > 1 ? size - 2 : -1;
409            RepeaterDefinition lastRepeaterDefinition = null;
410            int lastRepeaterIndex = -1;
411            while (index >= 0 && lastRepeaterDefinition == null)
412            {
413                ModelItem item = items.get(index);
414                if (item instanceof RepeaterDefinition)
415                {
416                    lastRepeaterDefinition = (RepeaterDefinition) item;
417                    lastRepeaterIndex = index;
418                }
419                
420                index--;
421            }
422            
423            Object value;
424            int lastHandledIndex = size;
425            ModelItem lastHandledItem = attributeDefinition;
426            if (((ContentAttributeDefinition) attributeDefinition).isMultiple())
427            {
428                Integer newPosition = contentsToEdit.get(content);
429                if (lastRepeaterDefinition != null)
430                {
431                    // if there is a repeater we can't merge values, they will be put in a single multiple value
432                    value = _getContentValues(contentIdsToReference);
433                }
434                else if (newPosition == null || newPosition < 0)
435                {
436                    // Normal case, it is not a move
437                    SynchronizableValue syncValue = new SynchronizableValue(_getContentValues(contentIdsToReference));
438                    syncValue.setMode(Mode.APPEND);
439                    value = syncValue;
440                }
441                else
442                {
443                    // Specific case where there is no new content id to reference, but a reorder in a multiple attribute
444                    ContentValue[] contentValues = content.getValue(attributePath);
445                    List<String> currentAttributeValue = Arrays.stream(contentValues)
446                            .map(ContentValue::getContentId)
447                            .collect(Collectors.toList());
448                    List<String> reorderedAttributeValue = _reorder(currentAttributeValue, contentIdsToReference, newPosition);
449                    
450                    value = _getContentValues(reorderedAttributeValue);
451                }
452            }
453            else if (lastRepeaterDefinition != null)
454            {
455                // Special case if there is a repeater in the path and the attribute is single valued.
456                // Create as many repeater entries as there are referenced contents.
457                List<Map<String, Object>> entries = new ArrayList<>();
458                for (int i = 0; i < contentIdsToReference.size(); i++)
459                {
460                    Map<String, Object> currentValue =  Map.of(attributeDefinition.getName(), contentIdsToReference.get(i));
461                    for (int j = items.size() - 2; j > lastRepeaterIndex; j--)
462                    {
463                        ModelItem item = items.get(j);
464                        currentValue = Map.of(item.getName(), currentValue);
465                    }
466                    
467                    entries.add(currentValue);
468                }
469                
470                lastHandledIndex = lastRepeaterIndex + 1;
471                lastHandledItem = lastRepeaterDefinition;
472                value = SynchronizableRepeater.appendOrRemove(entries, Set.of());
473            }
474            else
475            {
476                value = contentIdsToReference.get(0);
477            }
478            
479            Map<String, Object> values = Map.of(lastHandledItem.getName(), value);
480            for (int i = lastHandledIndex - 2; i >= 0; i--)
481            {
482                ModelItem item = items.get(i);
483                if (item instanceof RepeaterDefinition)
484                {
485                    values = Map.of(item.getName(), SynchronizableRepeater.appendOrRemove(List.of(values), Set.of()));
486                }
487                else
488                {
489                    values = Map.of(item.getName(), values);
490                }
491            }
492            
493            // Find the edit action to use
494            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
495        }
496    }
497    
498    private ContentValue[] _getContentValues(List<String> ids)
499    {
500        return ids.stream().map(s -> new ContentValue(_resolver, s)).toArray(ContentValue[]::new);
501    }
502    
503    private List<String> _reorder(List<String> currentElements, List<String> elementsToReorder, int newPosition)
504    {
505        List<String> reorderedList = new ArrayList<>(currentElements);
506        
507        // 1/ in currentElements, replace the ones to reorder by null, in order to keep all indexes
508        for (int i = 0; i < currentElements.size(); i++)
509        {
510            String element = currentElements.get(i);
511            if (elementsToReorder.contains(element))
512            {
513                reorderedList.set(i, null);
514            }
515        }
516        
517        // 2/ insert the elements to reorder at the new position
518        reorderedList.addAll(newPosition, elementsToReorder);
519        
520        // 3/ remove null elements, corresponding to the old positions of the elements that were reordered
521        reorderedList.removeIf(Objects::isNull);
522        
523        return reorderedList;
524    }
525
526    private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages)
527    {
528        Integer actionId = null; 
529        
530        int[] actionIds = _contentWorkflowHelper.getAvailableActions(content);
531        for (String workflowActionIdToTryAsString : workflowActionIds)
532        {
533            Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString);
534            if (ArrayUtils.contains(actionIds, workflowActionIdToTry))
535            {
536                actionId = workflowActionIdToTry;
537                break;
538            }
539        }
540        
541        if (actionId == null)
542        {
543            List<String> parameters = new ArrayList<>();
544            parameters.add(_contentHelper.getTitle(content));
545            parameters.add(content.getName());
546            parameters.add(content.getId());
547            errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_WORKFLOW", parameters));
548            errorIds.add(content.getId());
549        }
550        else
551        {
552            // edit
553            Map<String, Object> contextParameters = new HashMap<>();
554            contextParameters.put(EditContentFunction.QUIT, true);
555            contextParameters.put(EditContentFunction.VALUES_KEY, values);
556            
557            Map<String, Object> inputs = new HashMap<>();
558            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
559            
560            try
561            {
562                _contentWorkflowHelper.doAction(content, actionId, inputs);
563            }
564            catch (Exception e)
565            {
566                getLogger().error("Content '" + _contentHelper.getTitle(content) + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e);
567                
568                Map<String, I18nizableTextParameter> parameters = new HashMap<>();
569                parameters.put("0", new I18nizableText(_contentHelper.getTitle(content)));
570                parameters.put("1", new I18nizableText(content.getName()));
571                parameters.put("2", new I18nizableText(content.getId()));
572                
573                if (e instanceof InvalidInputWorkflowException)
574                {
575                    I18nizableText rootError = null;
576                    
577                    AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors();
578                    Map<String, Errors> allErrorsMap = allErrors.getAllErrors();
579                    for (String errorMetadataPath : allErrorsMap.keySet())
580                    {
581                        Errors errors = allErrorsMap.get(errorMetadataPath);
582                        
583                        I18nizableText insideError = null;
584                        
585                        List<I18nizableText> errorsAsList = errors.getErrors();
586                        for (I18nizableText error : errorsAsList)
587                        {
588                            Map<String, I18nizableTextParameter> i18nparameters = new HashMap<>();
589                            i18nparameters.put("0", error);
590                            
591                            I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE_CHAIN", i18nparameters);
592                            
593                            if (insideError == null)
594                            {
595                                insideError = localError;
596                            }
597                            else
598                            {
599                                insideError.getParameterMap().put("1", localError);
600                            }
601                        }
602
603                        Map<String, I18nizableTextParameter> i18ngeneralparameters = new HashMap<>();
604                        
605                        String i18ngeneralkey = null;
606                        if (EditContentFunction.GLOBAL_ERROR_KEY.equals(errorMetadataPath))
607                        {
608                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_GLOBAL_VALIDATION";
609                            i18ngeneralparameters.put("1", insideError);
610                        }
611                        else
612                        {
613                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE";
614                            i18ngeneralparameters.put("0", new I18nizableText(errorMetadataPath));
615                            i18ngeneralparameters.put("1", insideError);
616                        }
617
618                        I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters);
619                        if (rootError == null)
620                        {
621                            rootError = generalError;
622                        }
623                        else
624                        {
625                            rootError.getParameterMap().put("2", generalError);
626                        }
627                    }
628                    
629                    parameters.put("3", rootError);
630                }
631                else
632                {
633                    if (e.getMessage() != null)
634                    {
635                        parameters.put("3", new I18nizableText(e.getMessage()));
636                    }
637                    else
638                    {
639                        parameters.put("3", new I18nizableText(e.getClass().getName()));
640                    }
641                }
642                errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_EDIT", parameters));
643                errorIds.add(content.getId());
644            }
645        }
646    }
647    
648    /**
649     * Resolve content by their identifiers
650     * @param contentIds The id of contents to resolve
651     * @return the contents
652     */
653    protected List<? extends Content> _resolve(List<String> contentIds)
654    {
655        List<Content> contents = new ArrayList<>();
656
657        for (String contentId: contentIds)
658        {
659            Content content = _resolver.resolveById(contentId);
660            contents.add(content);
661        }
662        
663        return contents;
664    }
665    
666    /**
667     * Resolve content by their identifiers
668     * @param contentIds The id of contents to resolve
669     * @return the contents
670     */
671    protected Map<? extends Content, Integer> _resolve(Map<String, Integer> contentIds)
672    {
673        Map<Content, Integer> contents = new LinkedHashMap<>();
674        
675        for (Map.Entry<String, Integer> entry: contentIds.entrySet())
676        {
677            Content content = _resolver.resolveById(entry.getKey());
678            contents.put(content, entry.getValue());
679        }
680        
681        return contents;
682    }
683    
684    private Collection<String> _getContentTypesIntersection(Collection<? extends Content> contents)
685    {
686        Collection<String> contentTypes = new ArrayList<>();
687        for (Content content: contents)
688        {
689            Set<String> ancestorsAndMySelf = new HashSet<>();
690            
691            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
692            for (String id : allContentTypes)
693            {
694                ancestorsAndMySelf.addAll(_contentTypesHelper.getAncestors(id));
695                ancestorsAndMySelf.add(id);
696            }
697            
698            if (contentTypes.isEmpty())
699            {
700                contentTypes = ancestorsAndMySelf;
701            }
702            else
703            {
704                contentTypes = CollectionUtils.intersection(contentTypes, ancestorsAndMySelf);
705            }
706        }
707        return contentTypes;
708    }
709}