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