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