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}