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.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.commons.collections.CollectionUtils;
031import org.apache.commons.lang.ArrayUtils;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.cms.contenttype.ContentConstants;
035import org.ametys.cms.contenttype.ContentType;
036import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
037import org.ametys.cms.contenttype.ContentTypesHelper;
038import org.ametys.cms.contenttype.MetadataDefinition;
039import org.ametys.cms.contenttype.MetadataType;
040import org.ametys.cms.contenttype.RepeaterDefinition;
041import org.ametys.cms.form.AbstractField.MODE;
042import org.ametys.cms.repository.Content;
043import org.ametys.cms.repository.WorkflowAwareContent;
044import org.ametys.cms.workflow.AllErrors;
045import org.ametys.cms.workflow.ContentWorkflowHelper;
046import org.ametys.cms.workflow.EditContentFunction;
047import org.ametys.cms.workflow.InvalidInputWorkflowException;
048import org.ametys.core.ui.Callable;
049import org.ametys.core.ui.StaticClientSideRelation;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.workflow.AbstractWorkflowComponent;
052import org.ametys.runtime.i18n.I18nizableText;
053import org.ametys.runtime.parameter.Errors;
054
055/**
056 * Set the metadata of type 'content' of a content, with another content 
057 */
058public class SetContentMetadataClientSideElement extends StaticClientSideRelation implements Component
059{
060    /** The Ametys object resolver */
061    protected AmetysObjectResolver _resolver;
062    /** The content type helper */
063    protected ContentTypesHelper _ctypesHelper;
064    /** The content types extension point */
065    protected ContentTypeExtensionPoint _ctypesEP;
066    /** The content workflow helper */
067    protected ContentWorkflowHelper _contentWorkflowHelper;
068    /** The EP to filter meta */
069    protected FilterCompatibleContentMetadataExtensionPoint _filterCompatibleContentMetadataExtensionPoint;
070
071    @Override
072    public void service(ServiceManager manager) throws ServiceException
073    {
074        super.service(manager);
075        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
076        _ctypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
077        _ctypesEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
078        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
079        _filterCompatibleContentMetadataExtensionPoint = (FilterCompatibleContentMetadataExtensionPoint) manager.lookup(FilterCompatibleContentMetadataExtensionPoint.ROLE);
080    }
081    
082    /**
083     * Find a metadata of type content in definition of 'contentIdsToReference' and set the metadata of contents 'contentIdsToEdit' with value 'contentIdsToReference'
084     * @param contentIdsToReference The list of content ids that will be added as values in the content field
085     * @param contentIdsToEdit The list of content ids to edit and that will have a metadata of type content modified
086     * @return A map with key success: true or false. if false, it can be due to errors (list of error messages) or because metadata cannot be determined then 'metadata' is a list of possible metadata
087     */
088    @Callable
089    public Map<String, Object> getCompatibleMetadata(List<String> contentIdsToReference, List<String> contentIdsToEdit)
090    {
091        @SuppressWarnings("unchecked")
092        List<Content> contentToReference = (List<Content>) _resolve(contentIdsToReference);
093        @SuppressWarnings("unchecked")
094        List<Content> contentToEdit = (List<Content>) _resolve(contentIdsToEdit);
095
096        Set<MetadataDefinition> metadatadefs = _findCompatibleMetadata(contentToReference, contentToEdit);
097        
098        Map<String, Object> returnValues = new HashMap<>();
099        returnValues.put("success", false);
100        returnValues.put("metadata", _convert(metadatadefs));
101        return returnValues;
102    }
103    
104    /**
105     * Convert metadata definitions to JSON object
106     * @param metadatadefs The metadata definitions
107     * @return the JSON object
108     */
109    protected List<Map<String, Object>> _convert(Set<MetadataDefinition> metadatadefs)
110    {
111        List<Map<String, Object>> metadatapaths = new ArrayList<>(); 
112        
113        for (MetadataDefinition metadataDef : metadatadefs) 
114        {
115            metadatapaths.add(_convert(metadataDef));
116        }
117        
118        return metadatapaths;
119    }
120
121    /**
122     * Convert a metadata definition to JSON
123     * @param metadatadef the metadata definition
124     * @return the JSON object
125     */
126    protected Map<String, Object> _convert(MetadataDefinition metadatadef)
127    {
128        String metaPath = metadatadef.getId();
129        
130        Map<String, Object> def = new HashMap<>();
131        def.put("id", metaPath);
132        def.put("name", metadatadef.getName());
133        def.put("label", metadatadef.getLabel());
134        def.put("description", metadatadef.getDescription());
135
136        if (metaPath.contains(ContentConstants.METADATA_PATH_SEPARATOR))
137        {
138            String parentPath = StringUtils.substringBeforeLast(metaPath, ContentConstants.METADATA_PATH_SEPARATOR);
139            
140            String cTypeId = metadatadef.getReferenceContentType();
141            ContentType cType = _ctypesEP.getExtension(cTypeId);
142            
143            MetadataDefinition parentMetadatadef = cType.getMetadataDefinitionByPath(parentPath);
144            def.put("parent", _convert(parentMetadatadef));
145        }
146        
147        return def;
148    }
149    
150    /**
151     * Find the list of compatible metadata definition
152     * @param contentToReference the contents to reference
153     * @param contentToEdit the contents to edit
154     * @return the list of compatible metadata definition
155     */
156    protected Set<MetadataDefinition> _findCompatibleMetadata(List<Content> contentToReference, List<Content> contentToEdit)
157    {
158        // First we need to find the type of the target metadata we are looking for
159        Collection<String> contentTypesToReference = new HashSet<>();
160        for (Content content: contentToReference)
161        {
162            Set<String> ancestorsAndMySelf = new HashSet<>();
163            
164            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
165            for (String id : allContentTypes)
166            {
167                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
168                ancestorsAndMySelf.add(id);
169            }
170            
171            if (contentTypesToReference.isEmpty())
172            {
173                contentTypesToReference = ancestorsAndMySelf;
174            }
175            else
176            {
177                contentTypesToReference = CollectionUtils.intersection(contentTypesToReference, ancestorsAndMySelf);
178            }
179        }
180
181        // Second we need to know if this metadata will be mulitple or not
182        boolean requiresMultiple = contentToReference.size() > 1;
183        
184        // Third we need to know the target content type
185        Collection<String> contentTypesToEdit = new ArrayList<>();
186        for (Content content: contentToEdit)
187        {
188            Set<String> ancestorsAndMySelf = new HashSet<>();
189            
190            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
191            for (String id : allContentTypes)
192            {
193                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
194                ancestorsAndMySelf.add(id);
195            }
196            
197            if (contentTypesToEdit.isEmpty())
198            {
199                contentTypesToEdit = ancestorsAndMySelf;
200            }
201            else
202            {
203                contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf);
204            }
205        }
206        
207        // Now lets navigate in the target content type to find a content metadata limited to the metadata content type (or its parent types), that is multiple if necessary
208        Set<MetadataDefinition> metadata = new HashSet<>();
209        
210        for (String targetContentTypeName : contentTypesToEdit)
211        {
212            ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
213            for (String metadataName : targetContentType.getMetadataNames())
214            {
215                MetadataDefinition metaDef = targetContentType.getMetadataDefinition(metadataName);
216                metadata.addAll(_findCompatibleMetadata(contentToReference, contentToEdit, targetContentTypeName, metaDef, false, contentTypesToReference, requiresMultiple));
217            }
218        }
219        
220        return metadata;
221    }
222    
223    private Set<MetadataDefinition> _findCompatibleMetadata(List<Content> contentToReference, List<Content> contentToEdit, String targetContentTypeName, MetadataDefinition metaDef, boolean anyParentIsMultiple, Collection<String> compatibleContentTypes, boolean requiresMultiple)
224    {
225        Set<MetadataDefinition> metadata = new HashSet<>();
226        
227        if (MetadataType.CONTENT.equals(metaDef.getType()) 
228                && (metaDef.getContentType() == null || compatibleContentTypes.contains(metaDef.getContentType()))
229                && (!requiresMultiple || metaDef.isMultiple() || anyParentIsMultiple)
230                && metaDef.getReferenceContentType().equals(targetContentTypeName)
231                && _filterCompatibleContentMetadataExtensionPoint.filter(targetContentTypeName, metaDef, contentToReference, compatibleContentTypes)
232                && _hasRight(contentToEdit, metaDef))
233        {
234            metadata.add(metaDef);
235        }
236        else if (MetadataType.COMPOSITE.equals(metaDef.getType()))
237        {
238            for (String subMetadataName : metaDef.getMetadataNames())
239            {
240                MetadataDefinition subMetaDef = metaDef.getMetadataDefinition(subMetadataName);
241                metadata.addAll(_findCompatibleMetadata(contentToReference, contentToEdit, targetContentTypeName, subMetaDef, anyParentIsMultiple || metaDef instanceof RepeaterDefinition, compatibleContentTypes, requiresMultiple));
242            }
243        }
244        
245        return metadata;
246    }
247    
248    private boolean _hasRight (List<Content> contentToEdit, MetadataDefinition metaDef)
249    {
250        for (Content content : contentToEdit)
251        {
252            if (!_ctypesHelper.canWrite(content, metaDef))
253            {
254                return false;
255            }
256        }
257        return true;
258    }
259
260    /**
261     * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
262     * @param contentIdsToReference The list of content ids that will be added as values in the content field
263     * @param contentIdsToEdit The list of content ids to edit and that will have a metadata of type content modified
264     * @param contentsToEditToRemove  The list of content to edit to remove currently referenced content. Keys are "contentId" and "valueToRemove"
265     * @param metadatapath The metadata path selected to do modification in the contentIdsToEdit contents
266     * @param workflowActionIds The ids of workflow actions to use to edit the metadata. Actions will be tested in this order and first available action will be used
267     * @return A map with key success: true or false. if false, it can be due to errors (list of error messages)
268     */
269    @SuppressWarnings("unchecked")
270    @Callable
271    public Map<String, Object> setContentMetatada(List<String> contentIdsToReference, List<String> contentIdsToEdit, List<Map<String, String>> contentsToEditToRemove, String metadatapath, List<String> workflowActionIds)
272    {
273        List<WorkflowAwareContent> contentToEdit = (List<WorkflowAwareContent>) _resolve(contentIdsToEdit);
274 
275        List<String> errorIds = new ArrayList<>();
276        List<I18nizableText> errorMessages = new ArrayList<>();
277
278        _clean(contentsToEditToRemove, workflowActionIds, errorMessages, errorIds);
279        
280        if (contentIdsToEdit.isEmpty() || contentIdsToReference.isEmpty())
281        {
282            return _returnValue(errorMessages, errorIds);
283        }
284        
285        Collection<String> contentTypesToEdit = new ArrayList<>();
286        for (Content content: contentToEdit)
287        {
288            Set<String> ancestorsAndMySelf = new HashSet<>();
289            
290            String[] allContentTypes = (String[]) ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
291            for (String id : allContentTypes)
292            {
293                ancestorsAndMySelf.addAll(_ctypesHelper.getAncestors(id));
294                ancestorsAndMySelf.add(id);
295            }
296            
297            if (contentTypesToEdit.isEmpty())
298            {
299                contentTypesToEdit = ancestorsAndMySelf;
300            }
301            else
302            {
303                contentTypesToEdit = CollectionUtils.intersection(contentTypesToEdit, ancestorsAndMySelf);
304            }
305        }
306        
307        for (String targetContentTypeName : contentTypesToEdit)
308        {
309            ContentType targetContentType = _ctypesEP.getExtension(targetContentTypeName);
310            MetadataDefinition metadataDef = targetContentType.getMetadataDefinitionByPath(metadatapath);
311            if (metadataDef != null)
312            {
313                _setContentMetatada(contentIdsToReference, contentToEdit, targetContentType, metadatapath, workflowActionIds, errorMessages, errorIds);
314
315                return _returnValue(errorMessages, errorIds);
316            }
317        }
318        
319        throw new IllegalStateException("Unable to find medatata definition to path '" + metadatapath + "'.");
320    }
321    
322    private Map<String, Object> _returnValue(List<I18nizableText> errorMessages, List<String> errorIds)
323    {
324        Map<String, Object> returnValues = new HashMap<>();
325        returnValues.put("success", errorMessages.isEmpty() && errorIds.isEmpty());
326        if (!errorMessages.isEmpty())
327        {
328            returnValues.put("errorMessages", errorMessages);
329        }
330        if (!errorIds.isEmpty())
331        {
332            returnValues.put("errorIds", errorIds);
333        }
334        return returnValues;
335    }
336    
337    private void _clean(List<Map<String, String>> contentsToEditToRemove, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds)
338    {
339        for (Map<String, String> removeObject : contentsToEditToRemove)
340        {
341            String contentIdToEdit = removeObject.get("contentId");
342            String referencingMetadataPath = removeObject.get("referencingMetadataPath");
343            String valueToRemove = removeObject.get("valueToRemove");
344            
345            WorkflowAwareContent content = _resolver.resolveById(contentIdToEdit);
346            
347            Map<String, Object> values = new HashMap<>();
348            values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + referencingMetadataPath + ".mode", MODE.REMOVE.name());
349            
350            MetadataDefinition metadataDef = _ctypesHelper.getMetadataDefinitionByMetadataValuePath(referencingMetadataPath, content);
351            if (metadataDef.isMultiple())
352            {
353                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, Arrays.asList(valueToRemove));
354            }
355            else
356            {
357                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + referencingMetadataPath, valueToRemove);
358            }
359            
360            if (getLogger().isDebugEnabled())
361            {
362                getLogger().debug("Content " + contentIdToEdit + " must be edited at " + referencingMetadataPath +  " to remove " + valueToRemove);
363            }
364            
365            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
366        }
367    }
368    
369
370    /**
371     * Set the metadata 'metadatapath' of contents 'contentIdsToEdit' with value 'contentIdsToReference'
372     * @param contentIdsToReference The list of content ids that will be added as values in the content field
373     * @param contentToEdit The list of content to edit and that will have a metadata of type content modified
374     * @param contentType The content type
375     * @param metadataPath The metadata selected to do modification in the contentIdsToEdit contents
376     * @param workflowActionIds The ids of workflow actions to use to edit the metadata. Actions will be tested in this order and first available action will be used
377     * @param errorMessages The list that will be felt with error messages of content that had an issue during the operation 
378     * @param errorIds The list that will be felt with ids of content that had an issue during the operation
379     */
380    protected void _setContentMetatada(List<String> contentIdsToReference, List<WorkflowAwareContent> contentToEdit, ContentType contentType, String metadataPath, List<String> workflowActionIds, List<I18nizableText> errorMessages, List<String> errorIds)
381    {
382        // On each content
383        for (WorkflowAwareContent content : contentToEdit)
384        {
385            Map<String, Object> values = new HashMap<>();
386            
387            String metadataName = "";
388            String[] metadataDefPath = StringUtils.split(metadataPath, '/');
389            
390            // Find repeaters in the path
391            String lastRepeaterPath = null;
392            MetadataDefinition metadataDef = null;
393            for (String element : metadataDefPath)
394            {
395                metadataDef = metadataDef == null ? contentType.getMetadataDefinition(element) : metadataDef.getMetadataDefinition(element);
396                metadataName += ("".equals(metadataName) ? "" : ".") + metadataDef.getName();
397                
398                if (metadataDef instanceof RepeaterDefinition)
399                {
400                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".size", "1");
401                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
402                    lastRepeaterPath = metadataName;
403                    metadataName += ".1";
404                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".position", "0"); // 0 means at the end
405                }
406            }
407
408            if (metadataDef == null)
409            {
410                throw new IllegalStateException("Definition cannot be null");
411            }
412            
413            // The value to set
414            if (metadataDef.isMultiple())
415            {
416                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference);
417                values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
418            }
419            else if (lastRepeaterPath != null) // Special case in there is a repeater in the path and the metadata is single valued.
420            {
421                // create as many repeater entries as there is referenced contents.
422                int nbContentIdsToReference = contentIdsToReference.size();
423                values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + ".size", String.valueOf(nbContentIdsToReference));
424                
425                String inRepeaterPath = StringUtils.removeStart(metadataName, lastRepeaterPath + ".1");
426                
427                for (int i = 1; i <= nbContentIdsToReference; i++)
428                {
429                    values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath, contentIdsToReference.get(i - 1));
430                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + inRepeaterPath + ".mode", MODE.INSERT.name());
431                    
432                    values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + lastRepeaterPath + "." + i + ".position", "0"); // 0 means at the end
433                }
434            }
435            else
436            {
437                values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + metadataName, contentIdsToReference.get(0));
438                values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + metadataName + ".mode", MODE.INSERT.name());
439            }
440            
441            // Find the edit action to use
442            _doAction(content, workflowActionIds, values, errorIds, errorMessages);
443        }
444    }
445
446    private void _doAction(WorkflowAwareContent content, List<String> workflowActionIds, Map<String, Object> values, List<String> errorIds, List<I18nizableText> errorMessages)
447    {
448        Integer actionId = null; 
449        
450        int[] actionIds = _contentWorkflowHelper.getAvailableActions(content);
451        for (String workflowActionIdToTryAsString : workflowActionIds)
452        {
453            Integer workflowActionIdToTry = Integer.parseInt(workflowActionIdToTryAsString);
454            if (ArrayUtils.contains(actionIds, workflowActionIdToTry))
455            {
456                actionId = workflowActionIdToTry;
457                break;
458            }
459        }
460        
461        if (actionId == null)
462        {
463            List<String> parameters = new ArrayList<>();
464            parameters.add(content.getTitle());
465            parameters.add(content.getName());
466            parameters.add(content.getId());
467            errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_WORKFLOW", parameters));
468            errorIds.add(content.getId());
469        }
470        else
471        {
472            // edit
473            Map<String, Object> contextParameters = new HashMap<>();
474            contextParameters.put("quit", true);
475            contextParameters.put("values", values);
476            
477            Map<String, Object> inputs = new HashMap<>();
478            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
479            
480            try
481            {
482                _contentWorkflowHelper.doAction(content, actionId, inputs);
483            }
484            catch (Exception e)
485            {
486                getLogger().error("Content '" + content.getTitle() + "' (" + content.getName() + "/" + content.getId() + ") was not modified", e);
487                
488                Map<String, I18nizableText> parameters = new HashMap<>();
489                parameters.put("0", new I18nizableText(content.getTitle()));
490                parameters.put("1", new I18nizableText(content.getName()));
491                parameters.put("2", new I18nizableText(content.getId()));
492                
493                if (e instanceof InvalidInputWorkflowException)
494                {
495                    I18nizableText rootError = null;
496                    
497                    AllErrors allErrors = ((InvalidInputWorkflowException) e).getErrors();
498                    Map<String, Errors> allErrorsMap = allErrors.getAllErrors();
499                    for (String errorMetadataPath : allErrorsMap.keySet())
500                    {
501                        Errors errors = allErrorsMap.get(errorMetadataPath);
502                        
503                        I18nizableText insideError = null;
504                        
505                        List<I18nizableText> errorsAsList = errors.getErrors();
506                        for (I18nizableText error : errorsAsList)
507                        {
508                            Map<String, I18nizableText> i18nparameters = new HashMap<>();
509                            i18nparameters.put("0", error);
510                            
511                            I18nizableText localError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA_CHAIN", i18nparameters);
512                            
513                            if (insideError == null)
514                            {
515                                insideError = localError;
516                            }
517                            else
518                            {
519                                insideError.getParameterMap().put("1", localError);
520                            }
521                        }
522
523                        Map<String, I18nizableText> i18ngeneralparameters = new HashMap<>();
524                        i18ngeneralparameters.put("0", new I18nizableText(errorMetadataPath));
525                        i18ngeneralparameters.put("1", insideError);
526
527                        I18nizableText generalError = new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_VALIDATION_METADATA", i18ngeneralparameters);
528                        if (rootError == null)
529                        {
530                            rootError = generalError;
531                        }
532                        else
533                        {
534                            rootError.getParameterMap().put("2", generalError);
535                        }
536                    }
537                    
538                    parameters.put("3", rootError);
539                }
540                else
541                {
542                    if (e.getMessage() != null)
543                    {
544                        parameters.put("3", new I18nizableText(e.getMessage()));
545                    }
546                    else
547                    {
548                        parameters.put("3", new I18nizableText(e.getClass().getName()));
549                    }
550                }
551                errorMessages.add(new I18nizableText("plugin.cms", "PLUGINS_CMS_RELATIONS_SETCONTENTMETADATA_REFERENCE_ERROR_EDIT", parameters));
552                errorIds.add(content.getId());
553            }
554        }
555    }
556    
557    /**
558     * Resolve content by their ids
559     * @param contentIds The id of contents to resolve
560     * @return the contents
561     */
562    protected List<? extends Content> _resolve(List<String> contentIds)
563    {
564        List<Content> contents = new ArrayList<>();
565
566        for (String contentId: contentIds)
567        {
568            Content content = _resolver.resolveById(contentId);
569            contents.add(content);
570        }
571        
572        return contents;
573    }
574}