001/*
002 *  Copyright 2020 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.plugins.contentio.csv;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Optional;
026import java.util.Set;
027import java.util.function.Function;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.commons.lang3.StringUtils;
034import org.supercsv.io.ICsvListReader;
035import org.supercsv.util.Util;
036
037import org.ametys.cms.contenttype.ContentAttributeDefinition;
038import org.ametys.cms.contenttype.ContentType;
039import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
040import org.ametys.cms.data.ContentValue;
041import org.ametys.cms.data.type.AbstractMultilingualStringElementType;
042import org.ametys.cms.data.type.impl.MultilingualStringRepositoryElementType;
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.repository.ContentQueryHelper;
045import org.ametys.cms.repository.ModifiableDefaultContent;
046import org.ametys.cms.repository.ModifiableWorkflowAwareContent;
047import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
048import org.ametys.cms.workflow.ContentWorkflowHelper;
049import org.ametys.cms.workflow.CreateContentFunction;
050import org.ametys.core.util.I18nUtils;
051import org.ametys.plugins.contentio.in.ContentImportException;
052import org.ametys.plugins.repository.AmetysObjectIterable;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.data.holder.group.impl.ModifiableModelAwareRepeater;
055import org.ametys.plugins.repository.data.holder.values.SynchronizableRepeater;
056import org.ametys.plugins.repository.data.holder.values.SynchronizableValue;
057import org.ametys.plugins.repository.data.holder.values.SynchronizableValue.Mode;
058import org.ametys.plugins.repository.data.type.ModelItemTypeConstants;
059import org.ametys.plugins.repository.metadata.MultilingualString;
060import org.ametys.plugins.repository.metadata.MultilingualStringHelper;
061import org.ametys.plugins.repository.query.expression.AndExpression;
062import org.ametys.plugins.repository.query.expression.Expression;
063import org.ametys.plugins.repository.query.expression.Expression.Operator;
064import org.ametys.plugins.repository.query.expression.ExpressionContext;
065import org.ametys.plugins.repository.query.expression.MultilingualStringExpression;
066import org.ametys.plugins.repository.query.expression.StringExpression;
067import org.ametys.runtime.model.ElementDefinition;
068import org.ametys.runtime.model.ModelItem;
069import org.ametys.runtime.model.ModelViewItemGroup;
070import org.ametys.runtime.model.View;
071import org.ametys.runtime.model.ViewElement;
072import org.ametys.runtime.model.ViewElementAccessor;
073import org.ametys.runtime.model.ViewItem;
074import org.ametys.runtime.model.ViewItemAccessor;
075import org.ametys.runtime.model.type.ElementType;
076import org.ametys.runtime.plugin.component.AbstractLogEnabled;
077
078import com.opensymphony.workflow.WorkflowException;
079
080/**
081 * Import contents from an uploaded CSV file.
082 */
083public class CSVImporter extends AbstractLogEnabled implements Component, Serviceable
084{
085    /** Avalon Role */
086    public static final String ROLE = CSVImporter.class.getName();
087
088    private ContentWorkflowHelper _contentWorkflowHelper;
089
090    private ContentTypeExtensionPoint _contentTypeEP;
091    
092    private AmetysObjectResolver _resolver;
093
094    private I18nUtils _i18nUtils;
095    
096    @Override
097    public void service(ServiceManager smanager) throws ServiceException
098    {
099        _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE);
100        _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE);
101        _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE);
102        _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE);
103    }
104
105    /**
106     * Extract contents from CSV file
107     * @param mapping mapping of content attributes and CSV file header
108     * @param view View of importing content
109     * @param contentType content type to import
110     * @param listReader mapReader to parse CSV file
111     * @param createAction creation action id
112     * @param editAction edition action id
113     * @param workflowName workflow name
114     * @param language language of created content.
115     * @return list of created contents
116     * @throws IOException IOException while reading CSV
117     */
118    public Map<String, Object> importContentsFromCSV(Map<String, Object> mapping, View view, ContentType contentType, ICsvListReader listReader, int createAction, int editAction, String workflowName, String language) throws IOException
119    {
120        List<String> contentIds = new ArrayList<>();
121        String[] columns = listReader.getHeader(true);
122        int nbErrors = 0;
123        int nbWarnings = 0;
124        List<String> row = null;
125        
126        while ((row = listReader.read()) != null)
127        {
128            try
129            {
130
131                if (listReader.length() != columns.length) 
132                {
133                    getLogger().error("Import from CSV file: content skipped because of invalid row: {}", row);
134                    nbErrors++;
135                    continue;
136                }
137
138                Map<String, String> rowMap = new HashMap<>();
139                Util.filterListToMap(rowMap, columns, row);
140                List<ViewItem> errors = new ArrayList<>();
141                Content content = _processContent(view, rowMap, contentType, mapping, createAction, editAction, workflowName, language, errors);
142                contentIds.add(content.getId());
143                if (!errors.isEmpty())
144                {
145                    nbWarnings++;
146                }
147            }
148            catch (Exception e)
149            {
150                nbErrors++;
151                getLogger().error("Import from CSV file: error importing the content on line {}", listReader.getLineNumber(), e);
152            }
153        }
154        
155        Map<String, Object> results = new HashMap<>();
156        results.put("contentIds", contentIds);
157        results.put("nbErrors", nbErrors);
158        results.put("nbWarnings", nbWarnings);
159        return results;
160    }
161    
162    private Content _processContent(View view, Map<String, String> row, ContentType contentType, Map<String, Object> mapping, int createAction, int editAction, String workflowName, String language, List<ViewItem> errors) throws Exception
163    {
164        @SuppressWarnings("unchecked")
165        List<String> attributeNames = (List<String>) mapping.get("id");
166        @SuppressWarnings("unchecked")
167        Map<String, Object> mappingValues = (Map<String, Object>) mapping.get("values");
168        Map<String, Object> values = new HashMap<>();
169        ModifiableWorkflowAwareContent content = _getOrCreateContent(mappingValues, row, contentType, Optional.of(workflowName), createAction, language, view, attributeNames, Optional.empty());
170        
171        for (ViewItem viewItem : view.getViewItems())
172        {
173            try
174            {
175                if (!_isId(viewItem, attributeNames))
176                {
177                    Object value = _getValue(content, viewItem, mappingValues, row, createAction, editAction, language, errors, "");
178                    if (value != null)
179                    {
180                        values.put(viewItem.getName(), value);
181                    }
182                }
183            }
184            catch (Exception e)
185            {
186                errors.add(viewItem);
187                getLogger().error("Import from CSV file: error while trying to get values for view: {}", viewItem.getName(), e);
188            }
189        }
190        
191        if (!values.isEmpty())
192        {
193            if (content == null)
194            {
195                // Throw this exception only when values are filled, as an empty content should not trigger any warning
196                throw new ContentImportException("Can't create and fill content of content type '" + contentType.getId() + "' and following values '" + values + "' : at least one of those identifiers is null : " + attributeNames);
197            }
198            else
199            {
200                _editContent(editAction, values, content);
201            }
202        }
203        
204        return content;
205    }
206
207    private void _editContent(int editAction, Map<String, Object> values, ModifiableWorkflowAwareContent content) throws WorkflowException
208    {
209        if (!values.isEmpty())
210        {
211            _contentWorkflowHelper.editContent(content, values, editAction);
212        }
213    }
214    
215    private boolean _isId(ViewItem viewItem, List<String> attributeNames)
216    {
217        if (viewItem instanceof ViewElement)
218        {
219            ViewElement viewElement = (ViewElement) viewItem;
220            ElementDefinition elementDefinition = viewElement.getDefinition();
221            if (!(elementDefinition instanceof ContentAttributeDefinition))
222            {
223                String elementName = elementDefinition.getName();
224                return attributeNames.contains(elementName);
225            }
226        }
227        return false;
228    }
229    
230    private Object _getValue(Content parentContent, ViewItem viewItem, Map<String, Object> mapping, Map<String, String> row, int createAction, int editAction, String language, List<ViewItem> errors, String prefix) throws Exception
231    {
232        if (viewItem instanceof ViewElement)
233        {
234            ViewElement viewElement = (ViewElement) viewItem;
235            ElementDefinition elementDefinition = viewElement.getDefinition();
236            if (elementDefinition instanceof ContentAttributeDefinition)
237            {
238                return _getContentAttributeDefinitionValues(parentContent, viewItem, mapping, row, createAction, editAction, language, viewElement, elementDefinition, errors);
239            }
240            else
241            {
242                return _getAttributeDefinitionValues(parentContent, mapping, row, elementDefinition, language, prefix);
243            }
244        }
245        else if (viewItem instanceof ModelViewItemGroup)
246        {
247            ModelViewItemGroup modelViewItemGroup = (ModelViewItemGroup) viewItem;
248            List<ViewItem> children = modelViewItemGroup.getViewItems();
249            @SuppressWarnings("unchecked")
250            Map<String, Object> nestedMap = (Map<String, Object>) mapping.get(viewItem.getName());
251            @SuppressWarnings("unchecked")
252            Map<String, Object> nestedMapValues = (Map<String, Object>) (nestedMap.get("values"));
253            if (ModelItemTypeConstants.REPEATER_TYPE_ID.equals(modelViewItemGroup.getDefinition().getType().getId()))
254            {
255                return _getRepeaterValues(parentContent, modelViewItemGroup, row, createAction, editAction, language, children, nestedMap, errors, prefix);
256            }
257            else
258            {
259                return _getCompositeValues(parentContent, viewItem, row, createAction, editAction, language, children, nestedMapValues, errors, prefix);
260            }
261        }
262        else
263        {
264            errors.add(viewItem);
265            throw new RuntimeException("Import from CSV file: unsupported type of ViewItem for view: " + viewItem.getName());
266        }
267    }
268
269    private Map<String, Object> _getCompositeValues(Content parentContent, ViewItem viewItem, Map<String, String> row, int createAction, int editAction,
270            String language, List<ViewItem> children, Map<String, Object> nestedMapValues, List<ViewItem> errors, String prefix)
271    {
272        Map<String, Object> compositeValues = new HashMap<>();
273        for (ViewItem child : children)
274        {
275            try
276            {
277                compositeValues.put(child.getName(), _getValue(parentContent, child, nestedMapValues, row, createAction, editAction, language, errors, prefix + viewItem.getName() + ModelItem.ITEM_PATH_SEPARATOR));
278            }
279            catch (Exception e)
280            {
281                errors.add(viewItem);
282                getLogger().error("Import from CSV file: error while trying to get values for view: {}", viewItem.getName(), e);
283            }
284        }
285        return compositeValues;
286    }
287
288    private SynchronizableRepeater _getRepeaterValues(Content parentContent, ModelViewItemGroup viewItem, Map<String, String> row, int createAction, int editAction, String language,
289            List<ViewItem> children, Map<String, Object> nestedMap, List<ViewItem> errors, String prefix)
290    {
291        @SuppressWarnings("unchecked")
292        Map<String, Object> mappingValues = (Map<String, Object>) (nestedMap.get("values"));
293        @SuppressWarnings("unchecked")
294        List<String> attributeNames = (List<String>) nestedMap.get("id");
295        Map<String, Object> repeaterValues = new HashMap<>();
296        List<Map<String, Object>> repeaterValuesList = new ArrayList<>();
297        List<Integer> indexList = new ArrayList<>();
298
299        if (_allAttributesFilled(mappingValues, attributeNames) && parentContent != null && parentContent.hasValue(prefix + viewItem.getName()))
300        {
301            ModifiableModelAwareRepeater repeater = parentContent.getValue(prefix + viewItem.getName());
302            
303            repeater.getEntries().forEach(entry -> 
304            {
305                boolean allEquals = !attributeNames.isEmpty() && attributeNames.stream()
306                    .allMatch(attributeName -> 
307                    {
308                        String idValue = (String) mappingValues.get(attributeName);
309                        ViewElement viewElement = (ViewElement) viewItem.getModelViewItem(attributeName);
310                        ElementDefinition elementDefinition = viewElement.getDefinition();
311                        ElementType elementType = elementDefinition.getType();
312                        Object value = elementType.castValue(row.get(idValue));
313                        return value != null && value.equals(entry.getValue(attributeName));
314                    });
315                
316                if (allEquals)
317                {
318                    indexList.add(entry.getPosition());
319                }
320            }); 
321        }
322        
323        Integer rowIndex = indexList.size() > 0 ? indexList.get(0) : 1;
324        
325        for (ViewItem child : children)
326        {
327            try
328            {
329                Object entryValues = _getValue(parentContent, child, mappingValues, row, createAction, editAction, language, errors, prefix + viewItem.getName() + "[" + rowIndex + "]" + ModelItem.ITEM_PATH_SEPARATOR);
330                if (entryValues != null)
331                {
332                    repeaterValues.put(child.getName(), entryValues);
333                }
334            }
335            catch (Exception e)
336            {
337                errors.add(viewItem);
338                getLogger().error("Import from CSV file: error while trying to get values for view: {}", viewItem.getName(), e);
339            }
340        }
341        repeaterValuesList.add(repeaterValues);
342        
343        
344        if (indexList.size() == 1)
345        {
346            return SynchronizableRepeater.replace(repeaterValuesList, indexList);
347        }
348        // If several rows match the id, only replace the first but add a warning
349        else if (indexList.size() > 1)
350        {
351            errors.add(viewItem);
352            return SynchronizableRepeater.replace(repeaterValuesList, List.of(indexList.get(0)));
353        }
354        else
355        {
356            return SynchronizableRepeater.appendOrRemove(repeaterValuesList, Set.of());
357        }
358        
359    }
360    
361    private Object _getContentAttributeDefinitionValues(Content parentContent, ViewItem viewItem, Map<String, Object> mapping, Map<String, String> row,
362            int createAction, int editAction, String language, ViewElement viewElement, ElementDefinition elementDefinition, List<ViewItem> errors) throws Exception
363    {
364        ViewElementAccessor viewElementAccessor = (ViewElementAccessor) viewElement;
365        ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) elementDefinition;
366        String contentTypeId = contentAttributeDefinition.getContentTypeId();
367        ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
368        @SuppressWarnings("unchecked")
369        Map<String, Object> nestedMap = (Map<String, Object>) mapping.get(viewItem.getName());
370        @SuppressWarnings("unchecked")
371        Map<String, Object> mappingValues = (Map<String, Object>) (nestedMap.get("values"));
372        @SuppressWarnings("unchecked")
373        List<String> attributeNames = (List<String>) nestedMap.get("id");
374        if (_allAttributesFilled(mappingValues, attributeNames))
375        {
376            Map<String, Object> values = new HashMap<>();
377            ModifiableWorkflowAwareContent attachedContent = _getOrCreateContent(mappingValues, row, contentType, Optional.empty(), createAction, language, viewElementAccessor, attributeNames, Optional.of(parentContent));
378            for (ViewItem nestedViewItem : viewElementAccessor.getViewItems())
379            {
380                try
381                {
382                    if (!_isId(nestedViewItem, attributeNames))
383                    {
384                        Object value = _getValue(attachedContent, nestedViewItem, mappingValues, row, createAction, editAction, language, errors, "");
385                        if (value != null)
386                        {
387                            values.put(nestedViewItem.getName(), value);
388                        }
389                    }
390                }
391                catch (Exception e)
392                {
393                    errors.add(viewItem);
394                    getLogger().error("Import from CSV file: error while trying to get values for view: {}", viewItem.getName(), e);
395                }
396            } 
397
398            if (!values.isEmpty())
399            {
400                if (attachedContent == null)
401                {
402                    // Throw this exception only when values are filled, as an empty content should not trigger any warning
403                    throw new ContentImportException("Can't create and fill content of content type '" + contentType.getId() + "' and following values '" + values + "' : at least one of those identifiers is null : " + attributeNames);
404                }
405                else
406                {
407                    _editContent(editAction, values, attachedContent);
408                }
409            }
410            
411            //If content is multiple, we keep the old value list and we check if content was already inside, and add it otherwise
412            if (contentAttributeDefinition.isMultiple() && attachedContent != null)
413            {
414                ContentValue[] multipleContents = parentContent.getValue(contentAttributeDefinition.getPath());
415                // If there is no list or if it is empty, add it. Otherwise, we have to check if it is inside.
416                if (!parentContent.hasValue(contentAttributeDefinition.getPath()) || !_containsContent(attachedContent, multipleContents))
417                {
418                    SynchronizableValue syncValue = new SynchronizableValue(List.of(attachedContent));
419                    syncValue.setMode(Mode.APPEND);
420                    return syncValue;
421                }
422            }
423            else
424            {
425                return attachedContent;
426            } 
427        }
428        return null;
429    }
430
431    private boolean _containsContent(ModifiableWorkflowAwareContent attachedContent, ContentValue[] multipleContents)
432    {
433        return Arrays.stream(multipleContents)
434                .anyMatch(contentValue -> attachedContent.getId().equals(contentValue.getContentId()));
435    }
436    
437
438    private Object _getAttributeDefinitionValues(Content parentContent, Map<String, Object> mapping, Map<String, String> row, ElementDefinition elementDefinition, String language, String prefix)
439    {
440        ElementType elementType = elementDefinition.getType();
441        String elementName = elementDefinition.getName();
442        String elementColumn = (String) mapping.get(elementName);
443        String valueAsString = row.get(elementColumn);
444        Object value;
445        if (elementType instanceof AbstractMultilingualStringElementType && !MultilingualStringHelper.matchesMultilingualStringPattern(valueAsString))
446        {
447            MultilingualString multilingualString = new MultilingualString();
448            multilingualString.add(new Locale(language), valueAsString);
449            value = multilingualString;
450        }
451        else
452        {
453            value = elementType.castValue(valueAsString);
454        }
455        if (elementDefinition.isMultiple())
456        {
457            // Build path with index for repeaters. 
458            String pathWithIndex = prefix + elementDefinition.getName();
459            
460            // If there is no list or if it is empty, add it. Otherwise, we have to check if it is inside. 
461            // If there is no parentContent, still append as we want to fill the values map anyway.
462            if (parentContent == null || !parentContent.hasValue(pathWithIndex) || !_containsValue(value, parentContent.getValue(pathWithIndex)))
463            {
464                SynchronizableValue syncValue = new SynchronizableValue(value != null ? List.of(value) : List.of());
465                syncValue.setMode(Mode.APPEND);
466                return syncValue;
467            }
468        }
469        else
470        {
471            return value;
472        }
473        return null;
474    }
475    
476    private boolean _containsValue(Object value, Object[] multipleContents)
477    {
478        return Arrays.stream(multipleContents)
479                .anyMatch(valueFromContent -> valueFromContent.equals(value));
480    }
481    
482    private ModifiableWorkflowAwareContent _getOrCreateContent(Map<String, Object> mapping, Map<String, String> row, ContentType contentType, Optional<String> workflowName, int createAction, String language, ViewItemAccessor viewItemAccessor, List<String> attributeNames, Optional<Content> parentContent) throws ContentImportException, WorkflowException
483    {
484        List<Expression> idExpressions = new ArrayList<>();
485        List<String> values = new ArrayList<>();
486        
487        for (String attributeName : attributeNames)
488        {
489            ViewElement viewElement = (ViewElement) viewItemAccessor.getModelViewItem(attributeName);
490            ElementDefinition elementDefinition = viewElement.getDefinition();
491            String attributePath = (String) mapping.get(attributeName);
492            final String value = row.get(attributePath);
493            values.add(value);
494            
495            if (value == null)
496            {
497                return null;
498            }
499            
500            // Get content
501            if (elementDefinition.getType() instanceof MultilingualStringRepositoryElementType)
502            {
503                idExpressions.add(new MultilingualStringExpression(attributeName, Operator.EQ, value, language));
504            }
505            else
506            {
507                idExpressions.add(new StringExpression(attributeName, Operator.EQ, value));
508            }
509        }
510
511        idExpressions.add(_contentTypeEP.createHierarchicalCTExpression(contentType.getId()));
512        
513        if (!contentType.isMultilingual())
514        {
515            idExpressions.add(new StringExpression("language", Operator.EQ, language, ExpressionContext.newInstance().withInternal(true)));
516        }
517        
518        Expression expression = new AndExpression(idExpressions.toArray(new Expression[idExpressions.size()]));
519
520        String xPathQuery = ContentQueryHelper.getContentXPathQuery(expression);
521        AmetysObjectIterable<ModifiableDefaultContent> matchingContents = _resolver.query(xPathQuery);
522        if (matchingContents.getSize() > 1)
523        {
524            throw new ContentImportException("More than one content found for type " + contentType.getLabel() + " with " 
525                    + attributeNames + " as identifier and " + values + " as value");
526        }
527        else if (matchingContents.getSize() == 1)
528        {
529            return matchingContents.iterator().next();
530        }
531        
532        // Create content
533
534        if (contentType.isAbstract())
535        {
536            throw new ContentImportException("Can not create content for type " + contentType.getLabel() + " with " 
537                    + attributeNames + " as identifier and " + values + " as value, the content type is abstract");
538        }
539        
540        Map<String, Object> result;
541        String title;
542        if (mapping.containsKey("title"))
543        {
544            title = row.get(mapping.get("title"));
545        }
546        else
547        {
548            title = _i18nUtils.translate(contentType.getDefaultTitle(), language);
549        }
550        
551        
552        String finalWorkflowName = workflowName.or(contentType::getDefaultWorkflowName)
553                                               .orElseThrow(() -> new ContentImportException("No workflow specified for content type " + contentType.getLabel() + " with " 
554                                                       + attributeNames + " as identifier and " + values + " as value"));
555
556        Map<String, Object> inputs = new HashMap<>();
557        inputs.put(CreateContentFunction.INITIAL_VALUE_SUPPLIER, new Function<List<String>, Object>()
558        {
559            public Object apply(List<String> keys)
560            {
561                // Browse the mapping to find the column related to the attribute
562                Object nestedValue = mapping;
563                for (String key : keys) 
564                {
565                    nestedValue = ((Map) nestedValue).get(key);
566                    // If nestedValue is null, the attribute is absent from the map, no value can be found
567                    if (nestedValue == null)
568                    {
569                        return null;
570                    }
571                    // If nestedValue is a map, the key is a complex element such a content or a composite, 
572                    // we need to keep browsing the map to find the column
573                    if (nestedValue instanceof Map)
574                    {
575                        nestedValue = ((Map) nestedValue).get("values");
576                    }
577                }
578                
579                // Get the value of the attribute for the current row 
580                return row.get(nestedValue);
581            }
582        });
583        
584        parentContent.ifPresent(content -> inputs.put(CreateContentFunction.PARENT_CONTEXT_VALUE, content.getId()));
585
586        // CONTENTIO-253 To avoid issue with title starting with a non letter character, we prefix the name with the contentTypeId
587        String prefix = StringUtils.substringAfterLast(contentType.getId(), ".").toLowerCase();
588        String contentName = prefix + "-" + title;
589        
590        if (contentType.isMultilingual())
591        {
592            inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, language);
593            result = _contentWorkflowHelper.createContent(finalWorkflowName, createAction, contentName, Map.of(language, title), new String[] {contentType.getId()}, null, null, null, inputs);
594        }
595        else
596        {
597            result = _contentWorkflowHelper.createContent(finalWorkflowName, createAction, contentName, title, new String[] {contentType.getId()}, null, language, null, null, inputs);
598        }
599        
600        ModifiableWorkflowAwareContent content = (ModifiableWorkflowAwareContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
601        
602
603        for (String attributeName : attributeNames)
604        {
605            ViewElement viewElement = (ViewElement) viewItemAccessor.getModelViewItem(attributeName);
606            ElementDefinition elementDefinition = viewElement.getDefinition();
607            String attributePath = (String) mapping.get(attributeName);
608            final String value = row.get(attributePath);
609            if (org.ametys.cms.data.type.ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(elementDefinition.getType().getId()))
610            {
611                MultilingualString multilingualString = new MultilingualString();
612                multilingualString.add(new Locale(language), value);
613                content.setValue(attributeName, multilingualString);
614            }
615            else
616            {
617                content.setValue(attributeName, elementDefinition.getType().castValue(value));
618            }
619        }
620        return content;
621    } 
622    
623    private boolean _allAttributesFilled(Map<String, Object> mappingValues, List<String> attributeNames)
624    {
625        return mappingValues.entrySet()
626            .stream()
627            .filter(entry -> attributeNames.contains(entry.getKey()))
628            .allMatch(entry -> entry.getValue() != null);
629    }
630}