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