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.BufferedReader; 019import java.io.ByteArrayInputStream; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.nio.charset.Charset; 025import java.nio.file.Files; 026import java.nio.file.Paths; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Objects; 033import java.util.Optional; 034import java.util.function.Predicate; 035import java.util.stream.Collectors; 036 037import org.apache.avalon.framework.activity.Initializable; 038import org.apache.avalon.framework.component.Component; 039import org.apache.avalon.framework.service.ServiceException; 040import org.apache.avalon.framework.service.ServiceManager; 041import org.apache.avalon.framework.service.Serviceable; 042import org.apache.commons.io.IOUtils; 043import org.apache.commons.lang3.StringUtils; 044import org.apache.tika.detect.DefaultEncodingDetector; 045import org.apache.tika.metadata.Metadata; 046import org.supercsv.io.CsvListReader; 047import org.supercsv.io.CsvMapReader; 048import org.supercsv.io.ICsvListReader; 049import org.supercsv.io.ICsvMapReader; 050import org.supercsv.prefs.CsvPreference; 051 052import org.ametys.cms.contenttype.ContentAttributeDefinition; 053import org.ametys.cms.contenttype.ContentType; 054import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 055import org.ametys.cms.contenttype.ContentTypesHelper; 056import org.ametys.core.ui.Callable; 057import org.ametys.core.user.CurrentUserProvider; 058import org.ametys.core.user.User; 059import org.ametys.core.user.UserIdentity; 060import org.ametys.core.user.UserManager; 061import org.ametys.core.util.I18nUtils; 062import org.ametys.core.util.mail.SendMailHelper; 063import org.ametys.core.util.mail.SendMailHelper.MailBuilder; 064import org.ametys.runtime.config.Config; 065import org.ametys.runtime.i18n.I18nizableText; 066import org.ametys.runtime.i18n.I18nizableTextParameter; 067import org.ametys.runtime.model.ElementDefinition; 068import org.ametys.runtime.model.ModelHelper; 069import org.ametys.runtime.model.ModelViewItemGroup; 070import org.ametys.runtime.model.View; 071import org.ametys.runtime.model.ViewElement; 072import org.ametys.runtime.model.ViewElementAccessor; 073import org.ametys.runtime.model.ViewItem; 074import org.ametys.runtime.model.exception.BadItemTypeException; 075import org.ametys.runtime.plugin.component.AbstractLogEnabled; 076 077import jakarta.mail.MessagingException; 078 079/** 080 * Import contents from an uploaded CSV file. 081 */ 082public class ImportCSVFileHelper extends AbstractLogEnabled implements Component, Serviceable, Initializable 083{ 084 /** Avalon Role */ 085 public static final String ROLE = ImportCSVFileHelper.class.getName(); 086 087 /** Result key containing updated content's ids */ 088 public static final String RESULT_IMPORTED_COUNT = "importedCount"; 089 /** Result key containing number of errors */ 090 public static final String RESULT_NB_ERRORS = "nbErrors"; 091 /** Result key containing number of warnings */ 092 public static final String RESULT_NB_WARNINGS = "nbWarnings"; 093 /** Result key containing a short an explanation for a general error */ 094 public static final String RESULT_ERROR = "error"; 095 /** Result key containing the filename */ 096 public static final String RESULT_FILENAME = "fileName"; 097 /** Column name of attribute path in CSV mapping */ 098 public static final String MAPPING_COLUMN_ATTRIBUTE_PATH = "attributePath"; 099 /** Column name of header in CSV mapping */ 100 public static final String MAPPING_COLUMN_HEADER = "header"; 101 /** Column name to identify if column is an ID in CSV mapping */ 102 public static final String MAPPING_COLUMN_IS_ID = "isId"; 103 /** Key in nested mapping to define the ID columns */ 104 public static final String NESTED_MAPPING_ID = "id"; 105 /** Key in nested mapping to define the values columns */ 106 public static final String NESTED_MAPPING_VALUES = "values"; 107 108 private CurrentUserProvider _currentUserProvider; 109 private UserManager _userManager; 110 111 private ContentTypeExtensionPoint _contentTypeEP; 112 private ContentTypesHelper _contentTypesHelper; 113 114 private I18nUtils _i18nUtils; 115 private String _sysadminMail; 116 117 private CSVImporter _csvImporter; 118 119 @Override 120 public void service(ServiceManager serviceManager) throws ServiceException 121 { 122 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 123 _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE); 124 125 _contentTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 126 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 127 128 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 129 130 _csvImporter = (CSVImporter) serviceManager.lookup(CSVImporter.ROLE); 131 } 132 133 public void initialize() throws Exception 134 { 135 _sysadminMail = Config.getInstance().getValue("smtp.mail.sysadminto"); 136 } 137 138 /** 139 * Retrieves the values for import CSV parameters 140 * @param contentTypeId the configured content type identifier 141 * @return the values for import CSV parameters 142 */ 143 @Callable 144 public Map<String, Object> getImportCSVParametersValues(String contentTypeId) 145 { 146 Map<String, Object> values = new HashMap<>(); 147 148 // Content types 149 List<String> contentTypeIds = StringUtils.isEmpty(contentTypeId) ? List.of() : List.of(contentTypeId); 150 Map<String, Object> filteredContentTypes = _contentTypesHelper.getContentTypesList(contentTypeIds, true, true, true, false, false); 151 values.put("availableContentTypes", filteredContentTypes.get("contentTypes")); 152 153 // Recipient - get current user email 154 String currentUserEmail = null; 155 UserIdentity currentUser = _currentUserProvider.getUser(); 156 157 String login = currentUser.getLogin(); 158 if (StringUtils.isNotBlank(login)) 159 { 160 String userPopulationId = currentUser.getPopulationId(); 161 User user = _userManager.getUser(userPopulationId, login); 162 currentUserEmail = user.getEmail(); 163 } 164 values.put("defaultRecipient", currentUserEmail); 165 166 return values; 167 } 168 169 /** 170 * Gets the configuration for creating/editing a collection of synchronizable contents. 171 * @param contentTypeId Content type id 172 * @param formValues map of form values 173 * @param mappingValues list of header and content attribute mapping 174 * @return A map containing information about what is needed to create/edit a collection of synchronizable contents 175 * @throws IOException IOException while reading CSV 176 */ 177 @SuppressWarnings("unchecked") 178 @Callable 179 public Map<String, Object> validateConfiguration(String contentTypeId, Map formValues, List<Map<String, Object>> mappingValues) throws IOException 180 { 181 Map<String, Object> result = new HashMap<>(); 182 183 List<Map<String, Object>> filteredMappingValues = _filterMapping(mappingValues); 184 185 List<String> contentAttributes = filteredMappingValues.stream() 186 .map(column -> column.get(MAPPING_COLUMN_ATTRIBUTE_PATH)) 187 .map(String.class::cast) 188 .collect(Collectors.toList()); 189 190 Map<String, Object> mapping = new HashMap<>(); 191 filteredMappingValues.forEach(column -> generateNestedMapping(mapping, column)); 192 193 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 194 result.put("contentTypeName", contentType.getLabel()); 195 View view = null; 196 for (String itemPath : contentAttributes) 197 { 198 if (!ModelHelper.hasModelItem(itemPath, List.of(contentType))) 199 { 200 result.put("error", "bad-mapping"); 201 result.put("details", List.of(itemPath, contentTypeId)); 202 return result; 203 } 204 } 205 206 try 207 { 208 view = View.of(contentType, contentAttributes.toArray(new String[contentAttributes.size()])); 209 } 210 catch (IllegalArgumentException | BadItemTypeException e) 211 { 212 getLogger().error("Error while creating view", e); 213 result.put("error", "cant-create-view"); 214 result.put("details", contentTypeId); 215 return result; 216 } 217 218 if (!mapping.containsKey(NESTED_MAPPING_ID)) 219 { 220 result.put("error", "missing-id"); 221 return result; 222 } 223 224 for (ViewItem viewItem : view.getViewItems()) 225 { 226 if (!_checkViewItemContainer(viewItem, (Map<String, Object>) mapping.get(NESTED_MAPPING_VALUES), result)) 227 { 228 return result; 229 } 230 } 231 result.put("success", true); 232 return result; 233 } 234 /** 235 * Gets the configuration for creating/editing a collection of synchronizable contents. 236 * @param config get all CSV related parameters: path, separating/escaping char, charset and contentType 237 * @param formValues map of form values 238 * @param mappingValues list of header and content attribute mapping 239 * @return A map containing information about what is needed to create/edit a collection of synchronizable contents 240 * @throws IOException IOException while reading CSV 241 */ 242 @Callable 243 public Map<String, Object> importContents(Map<String, Object> config, Map<String, Object> formValues, List<Map<String, Object>> mappingValues) throws IOException 244 { 245 Map<String, Object> result = new HashMap<>(); 246 result.put(RESULT_FILENAME, config.get("fileName")); 247 248 String path = (String) config.get("path"); 249 String contentTypeId = (String) config.get("contentType"); 250 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 251 252 // CSV configuration 253 CsvPreference csvPreference = new CsvPreference.Builder( 254 config.getOrDefault("escaping-char", '"').toString().charAt(0), 255 config.getOrDefault("separating-char", ';').toString().charAt(0), 256 "\r\n" 257 ) 258 .build(); 259 260 String encoding = (String) config.get("charset"); 261 Charset charset = Charset.forName(encoding); 262 263 String language = (String) formValues.get("language"); 264 int createAction = Integer.valueOf((String) formValues.getOrDefault("createAction", "1")); 265 int editAction = Integer.valueOf((String) formValues.getOrDefault("editAction", "2")); 266 String workflowName = (String) formValues.get("workflow"); 267 268 Optional<String> recipient = Optional.ofNullable(config.get("recipient")) 269 .filter(String.class::isInstance) 270 .map(String.class::cast) 271 .filter(StringUtils::isNotBlank); 272 273 List<Map<String, Object>> filteredMappingValues = _filterMapping(mappingValues); 274 275 try ( 276 InputStream fileInputStream = new FileInputStream(path); 277 InputStream inputStream = IOUtils.buffer(fileInputStream); 278 ) 279 { 280 result.putAll(_importContents(inputStream, csvPreference, charset, contentType, workflowName, language, createAction, editAction, filteredMappingValues)); 281 282 // If an exception occured and the recipient is empty, transfer the error mail to the configured sysadmin 283 if ("error".equals(result.get(RESULT_ERROR)) && recipient.isEmpty()) 284 { 285 recipient = Optional.ofNullable(_sysadminMail) 286 .filter(StringUtils::isNotBlank); 287 } 288 289 if (recipient.isPresent()) 290 { 291 _sendMail(result, recipient.get()); 292 } 293 } 294 finally 295 { 296 deleteFile(path); 297 } 298 299 return result; 300 } 301 302 /** 303 * Import contents from path, content type and language. 304 * CSV Excel north Europe preferences are used, charset is detected on the file, workflow is detected on content type, default creation (1) and 305 * edition (2) actions are used, values are automatically mapped from the header. Headers should contains a star (*) to detect identifier columns. 306 * @param inputStream The (buffered) input stream to the CSV resource 307 * @param contentType The content type 308 * @param language The language 309 * @return the result of the CSV import 310 * @throws IOException if an error occurs 311 */ 312 public Map<String, Object> importContents(InputStream inputStream, ContentType contentType, String language) throws IOException 313 { 314 CsvPreference csvPreference = CsvPreference.EXCEL_NORTH_EUROPE_PREFERENCE; 315 316 // Detect the charset of the current file 317 Charset charset = detectCharset(inputStream); 318 319 // Copy the first 8192 bytes of the initial input stream to another input stream (to get headers) 320 inputStream.mark(8192); 321 try (InputStream headerStream = new ByteArrayInputStream(inputStream.readNBytes(8192))) 322 { 323 inputStream.reset(); 324 325 // Get headers 326 String[] headers = extractHeaders(headerStream, csvPreference, charset); 327 328 // Get the mapping of the values 329 List<Map<String, Object>> mappingValues = getMapping(contentType, headers); 330 331 // Get the default workflow associated to the content type 332 String workflowName = contentType.getDefaultWorkflowName().orElseThrow(() -> new IllegalArgumentException("The workflow can't be defined.")); 333 334 // Import contents (last use of the input stream) 335 return _importContents(inputStream, csvPreference, charset, contentType, workflowName, language, 1, 2, mappingValues); 336 } 337 } 338 339 private Map<String, Object> _importContents(InputStream inputStream, CsvPreference csvPreference, Charset charset, ContentType contentType, String workflowName, String language, int createAction, int editAction, List<Map<String, Object>> mappingValues) 340 { 341 Map<String, Object> result = new HashMap<>(); 342 343 List<String> contentAttributes = mappingValues.stream() 344 .map(column -> column.get(MAPPING_COLUMN_ATTRIBUTE_PATH)) 345 .map(String.class::cast) 346 .collect(Collectors.toList()); 347 348 Map<String, Object> mapping = new HashMap<>(); 349 mappingValues.forEach(column -> generateNestedMapping(mapping, column)); 350 351 View view = View.of(contentType, contentAttributes.toArray(new String[contentAttributes.size()])); 352 353 try ( 354 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charset); 355 BufferedReader reader = new BufferedReader(inputStreamReader); 356 ICsvListReader csvMapReadernew = new CsvListReader(reader, csvPreference) 357 ) 358 { 359 Map<String, Object> importResult = _csvImporter.importContentsFromCSV(mapping, view, contentType, csvMapReadernew, createAction, editAction, workflowName, language); 360 361 @SuppressWarnings("unchecked") 362 List<String> contentIds = (List<String>) importResult.get(CSVImporter.RESULT_CONTENT_IDS); 363 if (contentIds.size() > 0) 364 { 365 result.put(RESULT_IMPORTED_COUNT, contentIds.size()); 366 result.put(RESULT_NB_ERRORS, importResult.get(CSVImporter.RESULT_NB_ERRORS)); 367 result.put(RESULT_NB_WARNINGS, importResult.get(CSVImporter.RESULT_NB_WARNINGS)); 368 } 369 else 370 { 371 result.put(RESULT_ERROR, "no-import"); 372 } 373 } 374 catch (Exception e) 375 { 376 getLogger().error("Error while importing contents", e); 377 result.put(RESULT_ERROR, "error"); 378 } 379 380 return result; 381 } 382 383 private boolean _checkViewItemContainer(ViewItem viewItem, Map<String, Object> mapping, Map<String, Object> result) 384 { 385 if (viewItem instanceof ViewElement) 386 { 387 ViewElement viewElement = (ViewElement) viewItem; 388 ElementDefinition elementDefinition = viewElement.getDefinition(); 389 if (elementDefinition instanceof ContentAttributeDefinition) 390 { 391 ViewElementAccessor viewElementAccessor = (ViewElementAccessor) viewElement; 392 ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) elementDefinition; 393 return _checkViewItemContentContainer(viewElementAccessor, mapping, result, contentAttributeDefinition); 394 } 395 else 396 { 397 return true; 398 } 399 } 400 else if (viewItem instanceof ModelViewItemGroup) 401 { 402 return _checkViewItemGroupContainer(viewItem, mapping, result); 403 } 404 else 405 { 406 result.put("error", "unknown-type"); 407 result.put("details", viewItem.getName()); 408 return false; 409 } 410 } 411 412 @SuppressWarnings("unchecked") 413 private boolean _checkViewItemContentContainer(ViewElementAccessor viewElementAccessor, Map<String, Object> mapping, Map<String, Object> result, ContentAttributeDefinition contentAttributeDefinition) 414 { 415 416 boolean success = true; 417 String contentTypeId = contentAttributeDefinition.getContentTypeId(); 418 ContentType contentType = _contentTypeEP.getExtension(contentTypeId); 419 if (mapping.get(viewElementAccessor.getName()) instanceof String) 420 { 421 result.put("error", "string-as-container"); 422 result.put("details", mapping.get(viewElementAccessor.getName())); 423 return false; 424 } 425 Map<String, Object> nestedMap = (Map<String, Object>) mapping.get(viewElementAccessor.getName()); 426 if (!nestedMap.containsKey(NESTED_MAPPING_ID) || ((List<String>) nestedMap.get(NESTED_MAPPING_ID)).isEmpty()) 427 { 428 result.put("error", "missing-id-content"); 429 result.put("details", List.of(contentType.getLabel(), contentAttributeDefinition.getName())); 430 return false; 431 } 432 Map<String, Object> nestedMapValues = (Map<String, Object>) (nestedMap.get(NESTED_MAPPING_VALUES)); 433 for (ViewItem nestedViewItem : viewElementAccessor.getViewItems()) 434 { 435 success = success && _checkViewItemContainer(nestedViewItem, nestedMapValues, result); 436 } 437 return success; 438 } 439 440 @SuppressWarnings("unchecked") 441 private boolean _checkViewItemGroupContainer(ViewItem viewItem, Map<String, Object> mapping, Map<String, Object> result) 442 { 443 boolean success = true; 444 ModelViewItemGroup modelViewItemGroup = (ModelViewItemGroup) viewItem; 445 List<ViewItem> elementDefinition = modelViewItemGroup.getViewItems(); 446 Map<String, Object> nestedMap = (Map<String, Object>) mapping.get(viewItem.getName()); 447 448 Map<String, Object> nestedMapValues = (Map<String, Object>) (nestedMap.get(NESTED_MAPPING_VALUES)); 449 450 for (ViewItem nestedViewItem : elementDefinition) 451 { 452 success = success && _checkViewItemContainer(nestedViewItem, nestedMapValues, result); 453 } 454 return success; 455 } 456 457 private static Map<String, Object> generateNestedMapping(Map<String, Object> mapping, Map<String, Object> column) 458 { 459 return generateNestedMapping(mapping, (String) column.get(MAPPING_COLUMN_ATTRIBUTE_PATH), (String) column.get(ImportCSVFileHelper.MAPPING_COLUMN_HEADER), (boolean) column.get(ImportCSVFileHelper.MAPPING_COLUMN_IS_ID)); 460 } 461 462 @SuppressWarnings("unchecked") 463 private static Map<String, Object> generateNestedMapping(Map<String, Object> mapping, String attributePath, String column, boolean isId) 464 { 465 Map<String, Object> values = (Map<String, Object>) mapping.computeIfAbsent(NESTED_MAPPING_VALUES, __ -> new HashMap<>()); 466 467 int separatorIndex = attributePath.indexOf('/'); 468 if (separatorIndex == -1) 469 { 470 if (isId) 471 { 472 List<String> ids = (List<String>) mapping.computeIfAbsent(NESTED_MAPPING_ID, __ -> new ArrayList<>()); 473 ids.add(attributePath); 474 } 475 values.put(attributePath, column); 476 } 477 else 478 { 479 Map<String, Object> subValuesMapping = (Map<String, Object>) values.computeIfAbsent(attributePath.substring(0, separatorIndex), __ -> new HashMap<>()); 480 generateNestedMapping(subValuesMapping, attributePath.substring(separatorIndex + 1), column, isId); 481 } 482 483 mapping.put(NESTED_MAPPING_VALUES, values); 484 return mapping; 485 } 486 487 private void _sendMail(Map<String, Object> importResult, String recipient) 488 { 489 I18nizableText subject = _getMailSubject(importResult); 490 I18nizableText body = _getMailBody(importResult); 491 492 try 493 { 494 MailBuilder mailBuilder = SendMailHelper.newMail() 495 .withSubject(_i18nUtils.translate(subject)) 496 .withRecipient(recipient) 497 .withTextBody(_i18nUtils.translate(body)); 498 499 mailBuilder.sendMail(); 500 } 501 catch (MessagingException | IOException e) 502 { 503 if (getLogger().isWarnEnabled()) 504 { 505 getLogger().warn("Unable to send the e-mail '" + subject + "' to '" + recipient + "'", e); 506 } 507 } 508 catch (Exception e) 509 { 510 getLogger().error("An unknown error has occured while sending the mail.", e); 511 } 512 } 513 514 private I18nizableText _getMailSubject(Map<String, Object> importResult) 515 { 516 Map<String, I18nizableTextParameter> i18nParams = Map.of("fileName", new I18nizableText((String) importResult.get(RESULT_FILENAME))); 517 if (importResult.containsKey(ImportCSVFileHelper.RESULT_ERROR)) 518 { 519 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_ERROR_SUBJECT", i18nParams); 520 } 521 else 522 { 523 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_SUCCESS_SUBJECT", i18nParams); 524 } 525 } 526 527 private I18nizableText _getMailBody(Map<String, Object> importResult) 528 { 529 Map<String, I18nizableTextParameter> i18nParams = new HashMap<>(); 530 i18nParams.put("fileName", new I18nizableText((String) importResult.get(RESULT_FILENAME))); 531 532 if (importResult.containsKey(ImportCSVFileHelper.RESULT_ERROR)) 533 { 534 if ("no-import".equals(importResult.get(ImportCSVFileHelper.RESULT_ERROR))) 535 { 536 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_ERROR_BODY_NO_IMPORT", i18nParams); 537 } 538 else 539 { 540 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_ERROR_BODY_TEXT", i18nParams); 541 } 542 } 543 else 544 { 545 int nbErrors = (int) importResult.get(RESULT_NB_ERRORS); 546 int nbWarnings = (int) importResult.get(RESULT_NB_WARNINGS); 547 i18nParams.put("importedCount", new I18nizableText(String.valueOf(importResult.get(RESULT_IMPORTED_COUNT)))); 548 i18nParams.put("nbErrors", new I18nizableText(String.valueOf(nbErrors))); 549 i18nParams.put("nbWarnings", new I18nizableText(String.valueOf(nbWarnings))); 550 551 if (nbErrors == 0 && nbWarnings == 0) 552 { 553 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_SUCCESS_BODY", i18nParams); 554 } 555 else if (nbWarnings == 0) 556 { 557 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_PARTIAL_SUCCESS_BODY_ERRORS", i18nParams); 558 } 559 else if (nbErrors == 0) 560 { 561 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_PARTIAL_SUCCESS_BODY_WARNINGS", i18nParams); 562 } 563 else 564 { 565 return new I18nizableText("plugin.contentio", "PLUGINS_CONTENTIO_CONTENT_IMPORT_MAIL_PARTIAL_SUCCESS_BODY_ERRORS_AND_WARNINGS", i18nParams); 566 } 567 } 568 } 569 570 /** 571 * Delete the file related to the given path 572 * @param path path of the file 573 * @throws IOException if an error occurs while deleting the file 574 */ 575 @Callable 576 public void deleteFile(String path) throws IOException 577 { 578 Files.delete(Paths.get(path)); 579 } 580 581 /** 582 * Detect the charset of a given resource. 583 * @param inputStream The input stream to the resource, need to support {@link InputStream#mark(int)} and {@link InputStream#reset()}. 584 * @return the charset 585 * @throws IOException if an error occurs 586 */ 587 public Charset detectCharset(InputStream inputStream) throws IOException 588 { 589 assert inputStream.markSupported(); 590 DefaultEncodingDetector defaultEncodingDetector = new DefaultEncodingDetector(); 591 return defaultEncodingDetector.detect(inputStream, new Metadata()); 592 } 593 594 /** 595 * Extract headers from the CSV resource. 596 * @param inputStream The input stream to the resource 597 * @param csvPreference The CSV preference (separators) 598 * @param charset The charset of the resource 599 * @return the list of headers in file 600 * @throws IOException if an error occurs 601 */ 602 public String[] extractHeaders(InputStream inputStream, CsvPreference csvPreference, Charset charset) throws IOException 603 { 604 try ( 605 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, charset); 606 BufferedReader reader = new BufferedReader(inputStreamReader); 607 ICsvMapReader mapReader = new CsvMapReader(reader, csvPreference); 608 ) 609 { 610 String[] headers = mapReader.getHeader(true); 611 return headers; 612 } 613 } 614 615 /** 616 * Get the mapping from the headers and corresponding to the content type. 617 * @param contentType The content type 618 * @param headers The CSV headers 619 * @return the list of columns descriptions. An item of the list is one column, each column is defined by a header "header" (text defined into the CSV header), an 620 * attribute path "attributePath" (real path extracted from the header, empty if the model item does not exist) and an identifier flag "isId" (<code>true</code> 621 * if the header ends with a star "*"). 622 */ 623 public List<Map<String, Object>> getMapping(ContentType contentType, String[] headers) 624 { 625 return Arrays.asList(headers) 626 .stream() 627 .filter(StringUtils::isNotEmpty) 628 .map(header -> _transformHeaderToMap(contentType, header)) 629 .collect(Collectors.toList()); 630 } 631 632 private Map<String, Object> _transformHeaderToMap(ContentType contentType, String header) 633 { 634 Map<String, Object> headerMap = new HashMap<>(); 635 headerMap.put(ImportCSVFileHelper.MAPPING_COLUMN_HEADER, header); 636 headerMap.put( 637 MAPPING_COLUMN_ATTRIBUTE_PATH, 638 Optional.of(header) 639 .map(s -> s.replace("*", "")) 640 .map(s -> s.replace(".", "/")) 641 .map(String::trim) 642 .filter(contentType::hasModelItem) 643 .orElse(StringUtils.EMPTY) 644 ); 645 headerMap.put(ImportCSVFileHelper.MAPPING_COLUMN_IS_ID, header.endsWith("*")); 646 return headerMap; 647 } 648 649 private List<Map<String, Object>> _filterMapping(List<Map<String, Object>> mappingValues) 650 { 651 // Suppress empty attributePath 652 // Transform attributePath with . to attributePath with / (Autocompletion in IHM has . as separator, but the API need /) 653 return mappingValues.stream() 654 .map(column -> 655 { 656 Optional<String> attributePath = Optional.of(MAPPING_COLUMN_ATTRIBUTE_PATH) 657 .map(column::get) 658 .map(String.class::cast) 659 .filter(StringUtils::isNotEmpty) 660 .map(s -> s.replace(".", "/")); 661 662 if (attributePath.isEmpty()) 663 { 664 return null; 665 } 666 667 // Replace original column 668 Map<String, Object> columnCopy = new HashMap<>(column); 669 columnCopy.put(MAPPING_COLUMN_ATTRIBUTE_PATH, attributePath.get()); 670 return columnCopy; 671 } 672 ) 673 .filter(Predicate.not(Objects::isNull)) 674 .collect(Collectors.toList()); 675 } 676}