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