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.tuple.ImmutablePair;
036import org.apache.commons.lang3.tuple.Pair;
037
038import org.ametys.cms.content.ContentHelper;
039import org.ametys.cms.contenttype.ContentAttributeDefinition;
040import org.ametys.cms.contenttype.ContentType;
041import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
042import org.ametys.cms.contenttype.ContentTypesHelper;
043import org.ametys.cms.data.ContentValue;
044import org.ametys.cms.model.restrictions.RestrictedModelItem;
045import org.ametys.cms.repository.Content;
046import org.ametys.cms.repository.WorkflowAwareContent;
047import org.ametys.cms.workflow.ContentWorkflowHelper;
048import org.ametys.cms.workflow.EditContentFunction;
049import org.ametys.cms.workflow.InvalidInputWorkflowException;
050import org.ametys.core.ui.Callable;
051import org.ametys.core.ui.StaticClientSideRelation;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
054import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
055import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode;
056import org.ametys.plugins.repository.model.RepeaterDefinition;
057import org.ametys.plugins.workflow.AbstractWorkflowComponent;
058import org.ametys.runtime.i18n.I18nizableText;
059import org.ametys.runtime.i18n.I18nizableTextParameter;
060import org.ametys.runtime.model.ElementDefinition;
061import org.ametys.runtime.model.ModelHelper;
062import org.ametys.runtime.model.ModelItem;
063import org.ametys.runtime.model.ModelItemContainer;
064import org.ametys.runtime.model.exception.UndefinedItemPathException;
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 'contentsToEdit' 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            Pair<RepeaterDefinition, Integer> lastRepeaterDefinitionAndIndex = _getLastRepeaterDefinitionAndIndex(items);
409            RepeaterDefinition lastRepeaterDefinition = lastRepeaterDefinitionAndIndex.getLeft();
410            int lastRepeaterIndex = lastRepeaterDefinitionAndIndex.getRight();
411            
412            Object value;
413            int lastHandledIndex = size;
414            ModelItem lastHandledItem = attributeDefinition;
415            if (((ContentAttributeDefinition) attributeDefinition).isMultiple())
416            {
417                Integer newPosition = contentsToEdit.get(content);
418                if (lastRepeaterDefinition != null)
419                {
420                    // if there is a repeater we can't merge values, they will be put in a single multiple value
421                    value = _getContentValues(contentIdsToReference);
422                }
423                else if (newPosition == null || newPosition < 0)
424                {
425                    // Normal case, it is not a move
426                    SynchronizableValue syncValue = new SynchronizableValue(_getContentValues(contentIdsToReference));
427                    syncValue.setMode(Mode.APPEND);
428                    value = syncValue;
429                }
430                else
431                {
432                    // Specific case where there is no new content id to reference, but a reorder in a multiple attribute
433                    ContentValue[] contentValues = content.getValue(attributePath);
434                    List<String> currentAttributeValue = Arrays.stream(contentValues)
435                            .map(ContentValue::getContentId)
436                            .collect(Collectors.toList());
437                    List<String> reorderedAttributeValue = _reorder(currentAttributeValue, contentIdsToReference, newPosition);
438                    
439                    value = _getContentValues(reorderedAttributeValue);
440                }
441            }
442            else if (lastRepeaterDefinition != null)
443            {
444                // Special case if there is a repeater in the path and the attribute is single valued.
445                // Create as many repeater entries as there are referenced contents.
446                List<Map<String, Object>> entries = new ArrayList<>();
447                for (int i = 0; i < contentIdsToReference.size(); i++)
448                {
449                    Map<String, Object> currentValue =  Map.of(attributeDefinition.getName(), contentIdsToReference.get(i));
450                    for (int j = items.size() - 2; j > lastRepeaterIndex; j--)
451                    {
452                        ModelItem item = items.get(j);
453                        currentValue = Map.of(item.getName(), currentValue);
454                    }
455                    
456                    entries.add(currentValue);
457                }
458                
459                lastHandledIndex = lastRepeaterIndex + 1;
460                lastHandledItem = lastRepeaterDefinition;
461                value = SynchronizableRepeater.appendOrRemove(entries, Set.of());
462            }
463            else
464            {
465                value = contentIdsToReference.get(0);
466            }
467            
468            Map<String, Object> values = Map.of(lastHandledItem.getName(), value);
469            for (int i = lastHandledIndex - 2; i >= 0; i--)
470            {
471                ModelItem item = items.get(i);
472                if (item instanceof RepeaterDefinition)
473                {
474                    values = Map.of(item.getName(), SynchronizableRepeater.appendOrRemove(List.of(values), Set.of()));
475                }
476                else
477                {
478                    values = Map.of(item.getName(), values);
479                }
480            }
481            
482            // Find the edit action to use
483            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
484        }
485    }
486    
487    private Pair<RepeaterDefinition, Integer> _getLastRepeaterDefinitionAndIndex(List<ModelItem> items)
488    {
489        int index = items.size() > 1 ? items.size() - 2 : -1;
490        RepeaterDefinition lastRepeaterDefinition = null;
491        int lastRepeaterIndex = -1;
492        while (index >= 0 && lastRepeaterDefinition == null)
493        {
494            ModelItem item = items.get(index);
495            if (item instanceof RepeaterDefinition)
496            {
497                lastRepeaterDefinition = (RepeaterDefinition) item;
498                lastRepeaterIndex = index;
499            }
500            
501            index--;
502        }
503        
504        return new ImmutablePair<>(lastRepeaterDefinition, lastRepeaterIndex);
505    }
506    
507    private ContentValue[] _getContentValues(List<String> ids)
508    {
509        return ids.stream().map(s -> new ContentValue(_resolver, s)).toArray(ContentValue[]::new);
510    }
511    
512    private List<String> _reorder(List<String> currentElements, List<String> elementsToReorder, int newPosition)
513    {
514        List<String> reorderedList = new ArrayList<>(currentElements);
515        
516        // 1/ in currentElements, replace the ones to reorder by null, in order to keep all indexes
517        for (int i = 0; i < currentElements.size(); i++)
518        {
519            String element = currentElements.get(i);
520            if (elementsToReorder.contains(element))
521            {
522                reorderedList.set(i, null);
523            }
524        }
525        
526        // 2/ insert the elements to reorder at the new position
527        reorderedList.addAll(newPosition, elementsToReorder);
528        
529        // 3/ remove null elements, corresponding to the old positions of the elements that were reordered
530        reorderedList.removeIf(Objects::isNull);
531        
532        return reorderedList;
533    }
534
535    private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages)
536    {
537        Integer actionId = null; 
538        
539        int[] actionIds = _contentWorkflowHelper.getAvailableActions(content);
540        for (String workflowActionIdToTryAsString : workflowActionIds)
541        {
542            Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString);
543            if (ArrayUtils.contains(actionIds, workflowActionIdToTry))
544            {
545                actionId = workflowActionIdToTry;
546                break;
547            }
548        }
549        
550        if (actionId == null)
551        {
552            List<String> parameters = new ArrayList<>();
553            parameters.add(_contentHelper.getTitle(content));
554            parameters.add(content.getName());
555            parameters.add(content.getId());
556            errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_WORKFLOW", parameters));
557            errorIds.add(content.getId());
558        }
559        else
560        {
561            // edit
562            Map<String, Object> contextParameters = new HashMap<>();
563            contextParameters.put(EditContentFunction.QUIT, true);
564            contextParameters.put(EditContentFunction.VALUES_KEY, values);
565            
566            Map<String, Object> inputs = new HashMap<>();
567            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
568            
569            try
570            {
571                _contentWorkflowHelper.doAction(content, actionId, inputs);
572            }
573            catch (Exception e)
574            {
575                getLogger().error("Content '" + _contentHelper.getTitle(content) + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e);
576                
577                Map<String, I18nizableTextParameter> parameters = new HashMap<>();
578                parameters.put("0", new I18nizableText(_contentHelper.getTitle(content)));
579                parameters.put("1", new I18nizableText(content.getName()));
580                parameters.put("2", new I18nizableText(content.getId()));
581                
582                if (e instanceof InvalidInputWorkflowException iiwe)
583                {
584                    I18nizableText rootError = null;
585                    
586                    Map<String, List<I18nizableText>> allErrorsMap = iiwe.getValidationResults().getAllErrors();
587                    for (String errorDataPath : allErrorsMap.keySet())
588                    {
589                        List<I18nizableText> errors = allErrorsMap.get(errorDataPath);
590                        I18nizableText insideError = null;
591                        for (I18nizableText error : errors)
592                        {
593                            Map<String, I18nizableTextParameter> i18nparameters = new HashMap<>();
594                            i18nparameters.put("0", error);
595                            
596                            I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE_CHAIN", i18nparameters);
597                            
598                            if (insideError == null)
599                            {
600                                insideError = localError;
601                            }
602                            else
603                            {
604                                insideError.getParameterMap().put("1", localError);
605                            }
606                        }
607
608                        Map<String, I18nizableTextParameter> i18ngeneralparameters = new HashMap<>();
609                        
610                        String i18ngeneralkey = null;
611                        if (EditContentFunction.GLOBAL_VALIDATION_RESULT_KEY.equals(errorDataPath))
612                        {
613                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_GLOBAL_VALIDATION";
614                            i18ngeneralparameters.put("1", insideError);
615                        }
616                        else
617                        {
618                            i18ngeneralkey = "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_VALIDATION_ATTRIBUTE";
619                            i18ngeneralparameters.put("0", new I18nizableText(errorDataPath));
620                            i18ngeneralparameters.put("1", insideError);
621                        }
622
623                        I18nizableText generalError = new I18nizableText("plugin.cms", i18ngeneralkey, i18ngeneralparameters);
624                        if (rootError == null)
625                        {
626                            rootError = generalError;
627                        }
628                        else
629                        {
630                            rootError.getParameterMap().put("2", generalError);
631                        }
632                    }
633                    
634                    parameters.put("3", rootError);
635                }
636                else
637                {
638                    if (e.getMessage() != null)
639                    {
640                        parameters.put("3", new I18nizableText(e.getMessage()));
641                    }
642                    else
643                    {
644                        parameters.put("3", new I18nizableText(e.getClass().getName()));
645                    }
646                }
647                errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_EDIT", parameters));
648                errorIds.add(content.getId());
649            }
650        }
651    }
652    
653    /**
654     * Resolve content by their identifiers
655     * @param contentIds The id of contents to resolve
656     * @return the contents
657     */
658    protected List<? extends Content> _resolve(List<String> contentIds)
659    {
660        List<Content> contents = new ArrayList<>();
661
662        for (String contentId: contentIds)
663        {
664            Content content = _resolver.resolveById(contentId);
665            contents.add(content);
666        }
667        
668        return contents;
669    }
670    
671    /**
672     * Resolve content by their identifiers
673     * @param contentIds The id of contents to resolve
674     * @return the contents
675     */
676    protected Map<? extends Content, Integer> _resolve(Map<String, Integer> contentIds)
677    {
678        Map<Content, Integer> contents = new LinkedHashMap<>();
679        
680        for (Map.Entry<String, Integer> entry: contentIds.entrySet())
681        {
682            Content content = _resolver.resolveById(entry.getKey());
683            contents.put(content, entry.getValue());
684        }
685        
686        return contents;
687    }
688    
689    private Collection<String> _getContentTypesIntersection(Collection<? extends Content> contents)
690    {
691        Collection<String> contentTypes = new ArrayList<>();
692        for (Content content: contents)
693        {
694            Set<String> ancestorsAndMySelf = new HashSet<>();
695            
696            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
697            for (String id : allContentTypes)
698            {
699                ancestorsAndMySelf.addAll(_contentTypesHelper.getAncestors(id));
700                ancestorsAndMySelf.add(id);
701            }
702            
703            if (contentTypes.isEmpty())
704            {
705                contentTypes = ancestorsAndMySelf;
706            }
707            else
708            {
709                contentTypes = CollectionUtils.intersection(contentTypes, ancestorsAndMySelf);
710            }
711        }
712        return contentTypes;
713    }
714}