001/* 002 * (C) Copyright 2012-2014 Nuxeo SA (http://nuxeo.com/) and others. 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 * 016 * Contributors: 017 * Thomas Roger 018 * Florent Guillaume 019 * Julien Carsique 020 */ 021package org.nuxeo.ecm.csv.core; 022 023import static org.nuxeo.ecm.csv.core.CSVImportLog.Status.ERROR; 024import static org.nuxeo.ecm.csv.core.Constants.CSV_NAME_COL; 025import static org.nuxeo.ecm.csv.core.Constants.CSV_TYPE_COL; 026 027import java.io.BufferedReader; 028import java.io.File; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.InputStreamReader; 032import java.io.Reader; 033import java.io.Serializable; 034import java.text.DateFormat; 035import java.text.ParseException; 036import java.text.SimpleDateFormat; 037import java.util.ArrayList; 038import java.util.Arrays; 039import java.util.Collections; 040import java.util.Date; 041import java.util.HashMap; 042import java.util.List; 043import java.util.Map; 044 045import org.apache.commons.csv.CSVFormat; 046import org.apache.commons.csv.CSVParser; 047import org.apache.commons.csv.CSVRecord; 048import org.apache.commons.io.Charsets; 049import org.apache.commons.io.FilenameUtils; 050import org.apache.commons.io.IOUtils; 051import org.apache.commons.io.input.BOMInputStream; 052import org.apache.commons.lang.StringUtils; 053import org.apache.commons.logging.Log; 054import org.apache.commons.logging.LogFactory; 055import org.nuxeo.common.utils.ExceptionUtils; 056import org.nuxeo.common.utils.Path; 057import org.nuxeo.ecm.automation.AutomationService; 058import org.nuxeo.ecm.automation.OperationChain; 059import org.nuxeo.ecm.automation.OperationContext; 060import org.nuxeo.ecm.automation.core.operations.notification.MailTemplateHelper; 061import org.nuxeo.ecm.automation.core.operations.notification.SendMail; 062import org.nuxeo.ecm.automation.core.scripting.Expression; 063import org.nuxeo.ecm.automation.core.scripting.Scripting; 064import org.nuxeo.ecm.automation.core.util.ComplexTypeJSONDecoder; 065import org.nuxeo.ecm.automation.core.util.StringList; 066import org.nuxeo.ecm.core.api.Blob; 067import org.nuxeo.ecm.core.api.Blobs; 068import org.nuxeo.ecm.core.api.DocumentModel; 069import org.nuxeo.ecm.core.api.DocumentRef; 070import org.nuxeo.ecm.core.api.NuxeoException; 071import org.nuxeo.ecm.core.api.NuxeoPrincipal; 072import org.nuxeo.ecm.core.api.PathRef; 073import org.nuxeo.ecm.core.query.sql.NXQL; 074import org.nuxeo.ecm.core.schema.DocumentType; 075import org.nuxeo.ecm.core.schema.SchemaManager; 076import org.nuxeo.ecm.core.schema.types.ComplexType; 077import org.nuxeo.ecm.core.schema.types.CompositeType; 078import org.nuxeo.ecm.core.schema.types.Field; 079import org.nuxeo.ecm.core.schema.types.ListType; 080import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl; 081import org.nuxeo.ecm.core.schema.types.Type; 082import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; 083import org.nuxeo.ecm.core.schema.types.primitives.DateType; 084import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; 085import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; 086import org.nuxeo.ecm.core.schema.types.primitives.LongType; 087import org.nuxeo.ecm.core.schema.types.primitives.StringType; 088import org.nuxeo.ecm.core.transientstore.api.TransientStore; 089import org.nuxeo.ecm.core.transientstore.work.TransientStoreWork; 090import org.nuxeo.ecm.core.work.api.WorkManager; 091import org.nuxeo.ecm.csv.core.CSVImportLog.Status; 092import org.nuxeo.ecm.platform.ec.notification.NotificationEventListener; 093import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper; 094import org.nuxeo.ecm.platform.types.TypeManager; 095import org.nuxeo.ecm.platform.url.api.DocumentViewCodecManager; 096import org.nuxeo.ecm.platform.url.codec.api.DocumentViewCodec; 097import org.nuxeo.ecm.platform.usermanager.UserManager; 098import org.nuxeo.runtime.api.Framework; 099 100; 101 102/** 103 * Work task to import form a CSV file. Because the file is read from the local filesystem, this must be executed in a 104 * local queue. Since NXP-15252 the CSV reader manages "records", not "lines". 105 * 106 * @since 5.7 107 */ 108public class CSVImporterWork extends TransientStoreWork { 109 110 public static final String NUXEO_CSV_MAIL_TO = "nuxeo.csv.mail.to"; 111 112 public static final String LABEL_CSV_IMPORTER_NOT_EXISTING_FIELD = "label.csv.importer.notExistingField"; 113 114 public static final String LABEL_CSV_IMPORTER_CANNOT_CONVERT_FIELD_VALUE = "label.csv.importer.cannotConvertFieldValue"; 115 116 public static final String LABEL_CSV_IMPORTER_NOT_EXISTING_FILE = "label.csv.importer.notExistingFile"; 117 118 public static final String NUXEO_CSV_BLOBS_FOLDER = "nuxeo.csv.blobs.folder"; 119 120 public static final String LABEL_CSV_IMPORTER_DOCUMENT_ALREADY_EXISTS = "label.csv.importer.documentAlreadyExists"; 121 122 public static final String LABEL_CSV_IMPORTER_UNABLE_TO_UPDATE = "label.csv.importer.unableToUpdate"; 123 124 public static final String LABEL_CSV_IMPORTER_DOCUMENT_UPDATED = "label.csv.importer.documentUpdated"; 125 126 public static final String LABEL_CSV_IMPORTER_UNABLE_TO_CREATE = "label.csv.importer.unableToCreate"; 127 128 public static final String LABEL_CSV_IMPORTER_PARENT_DOES_NOT_EXIST = "label.csv.importer.parentDoesNotExist"; 129 130 public static final String LABEL_CSV_IMPORTER_DOCUMENT_CREATED = "label.csv.importer.documentCreated"; 131 132 public static final String LABEL_CSV_IMPORTER_NOT_ALLOWED_SUB_TYPE = "label.csv.importer.notAllowedSubType"; 133 134 public static final String LABEL_CSV_IMPORTER_UNABLE_TO_SAVE = "label.csv.importer.unableToSave"; 135 136 public static final String LABEL_CSV_IMPORTER_ERROR_IMPORTING_LINE = "label.csv.importer.errorImportingLine"; 137 138 public static final String LABEL_CSV_IMPORTER_NOT_EXISTING_TYPE = "label.csv.importer.notExistingType"; 139 140 public static final String LABEL_CSV_IMPORTER_MISSING_TYPE_VALUE = "label.csv.importer.missingTypeValue"; 141 142 public static final String LABEL_CSV_IMPORTER_MISSING_NAME_VALUE = "label.csv.importer.missingNameValue"; 143 144 public static final String LABEL_CSV_IMPORTER_MISSING_NAME_COLUMN = "label.csv.importer.missingNameColumn"; 145 146 public static final String LABEL_CSV_IMPORTER_EMPTY_FILE = "label.csv.importer.emptyFile"; 147 148 public static final String LABEL_CSV_IMPORTER_ERROR_DURING_IMPORT = "label.csv.importer.errorDuringImport"; 149 150 public static final String LABEL_CSV_IMPORTER_EMPTY_LINE = "label.csv.importer.emptyLine"; 151 152 private static final long serialVersionUID = 1L; 153 154 private static final Log log = LogFactory.getLog(CSVImporterWork.class); 155 156 private static final String TEMPLATE_IMPORT_RESULT = "templates/csvImportResult.ftl"; 157 158 public static final String CATEGORY_CSV_IMPORTER = "csvImporter"; 159 160 public static final String CONTENT_FILED_TYPE_NAME = "content"; 161 162 private static final long COMPUTE_TOTAL_THRESHOLD_KB = 1000; 163 164 /** 165 * CSV headers that won't be checked if the field exists on the document type. 166 * 167 * @since 7.3 168 */ 169 public static List<String> AUTHORIZED_HEADERS = Arrays.asList(NXQL.ECM_LIFECYCLESTATE, NXQL.ECM_UUID); 170 171 protected String parentPath; 172 173 protected String username; 174 175 protected CSVImporterOptions options; 176 177 protected transient DateFormat dateformat; 178 179 protected boolean hasTypeColumn; 180 181 protected Date startDate; 182 183 protected ArrayList<CSVImportLog> importLogs = new ArrayList<>(); 184 185 protected boolean computeTotal = false; 186 187 protected long total = -1L; 188 189 protected long docsCreatedCount; 190 191 public CSVImporterWork(String id) { 192 super(id); 193 } 194 195 public CSVImporterWork(String repositoryName, String parentPath, String username, Blob csvBlob, 196 CSVImporterOptions options) { 197 super(CSVImportId.create(repositoryName, parentPath, csvBlob)); 198 getStore().putBlobs(id, Collections.singletonList(csvBlob)); 199 setDocument(repositoryName, null); 200 setOriginatingUsername(username); 201 this.parentPath = parentPath; 202 this.username = username; 203 if (csvBlob.getLength() >= 0 && csvBlob.getLength() / 1024 < COMPUTE_TOTAL_THRESHOLD_KB) { 204 computeTotal = true; 205 } 206 this.options = options; 207 startDate = new Date(); 208 } 209 210 211 @Override 212 public String getCategory() { 213 return CATEGORY_CSV_IMPORTER; 214 } 215 216 @Override 217 public String getTitle() { 218 return String.format("CSV import in '%s'", parentPath); 219 } 220 221 public List<CSVImportLog> getImportLogs() { 222 return new ArrayList<>(importLogs); 223 } 224 225 @Override 226 public void work() { 227 TransientStore store = getStore(); 228 setStatus("Importing"); 229 openUserSession(); 230 CSVFormat csvFormat = CSVFormat.DEFAULT.withHeader().withEscape(options.getEscapeCharacter()).withCommentMarker( 231 options.getCommentMarker()); 232 try (Reader in = newReader(getBlob()); CSVParser parser = csvFormat.parse(in)) { 233 doImport(parser); 234 } catch (IOException e) { 235 logError(0, "Error while doing the import: %s", LABEL_CSV_IMPORTER_ERROR_DURING_IMPORT, e.getMessage()); 236 log.debug(e, e); 237 } 238 store.putParameter(id, "logs", importLogs); 239 if (options.sendEmail()) { 240 setStatus("Sending email"); 241 sendMail(); 242 } 243 setStatus(null); 244 } 245 246 @Override 247 public void cleanUp(boolean ok, Exception e) { 248 try { 249 super.cleanUp(ok, e); 250 } finally { 251 getStore().putParameter(id, "status", new CSVImportStatus(CSVImportStatus.State.COMPLETED, total, total)); 252 } 253 } 254 255 static final Serializable EMPTY_LOGS = new ArrayList<CSVImportLog>(); 256 257 String launch() { 258 WorkManager works = Framework.getLocalService(WorkManager.class); 259 260 TransientStore store = getStore(); 261 store.putParameter(id, "logs", EMPTY_LOGS); 262 store.putParameter(id, "status", new CSVImportStatus(CSVImportStatus.State.SCHEDULED)); 263 works.schedule(this, WorkManager.Scheduling.IF_NOT_RUNNING_OR_SCHEDULED); 264 return id; 265 } 266 267 static CSVImportStatus getStatus(String id) { 268 TransientStore store = getStore(); 269 if (!store.exists(id)) { 270 return null; 271 } 272 return (CSVImportStatus) store.getParameter(id, "status"); 273 } 274 275 @SuppressWarnings("unchecked") 276 static List<CSVImportLog> getLastImportLogs(String id) { 277 TransientStore store = getStore(); 278 if (!store.exists(id)) { 279 return Collections.emptyList(); 280 } 281 return (ArrayList<CSVImportLog>) store.getParameter(id, "logs"); 282 } 283 284 /** 285 * @throws IOException 286 * @since 7.3 287 */ 288 protected BufferedReader newReader(Blob blob) throws IOException { 289 return new BufferedReader(new InputStreamReader(new BOMInputStream(blob.getStream()))); 290 } 291 292 protected void doImport(CSVParser parser) { 293 log.info(String.format("Importing CSV file: %s", getBlob().getFilename())); 294 Map<String, Integer> header = parser.getHeaderMap(); 295 if (header == null) { 296 logError(0, "No header line, empty file?", LABEL_CSV_IMPORTER_EMPTY_FILE); 297 return; 298 } 299 if (!header.containsKey(CSV_NAME_COL)) { 300 logError(0, "Missing 'name' column", LABEL_CSV_IMPORTER_MISSING_NAME_COLUMN); 301 return; 302 } 303 hasTypeColumn = header.containsKey(CSV_TYPE_COL); 304 305 try { 306 int batchSize = options.getBatchSize(); 307 Iterable<CSVRecord> it = parser; 308 if (computeTotal) { 309 try { 310 List<CSVRecord> l = parser.getRecords(); 311 total = l.size(); 312 it = l; 313 } catch (IOException e) { 314 log.warn("Could not compute total number of document to be imported"); 315 } 316 } 317 for (CSVRecord record : it) { 318 if (record.size() == 0) { 319 // empty record 320 importLogs.add(new CSVImportLog(record.getRecordNumber(), Status.SKIPPED, "Empty record", 321 LABEL_CSV_IMPORTER_EMPTY_LINE)); 322 continue; 323 } 324 try { 325 if (importRecord(record, header)) { 326 docsCreatedCount++; 327 getStore().putParameter(id, "status", new CSVImportStatus(CSVImportStatus.State.RUNNING, 328 docsCreatedCount, total)); 329 if (docsCreatedCount % batchSize == 0) { 330 commitOrRollbackTransaction(); 331 startTransaction(); 332 } 333 } 334 } catch (NuxeoException e) { 335 // try next line 336 Throwable unwrappedException = unwrapException(e); 337 logError(parser.getRecordNumber(), "Error while importing line: %s", 338 LABEL_CSV_IMPORTER_ERROR_IMPORTING_LINE, unwrappedException.getMessage()); 339 log.debug(unwrappedException, unwrappedException); 340 } 341 } 342 343 try { 344 session.save(); 345 } catch (NuxeoException e) { 346 Throwable ue = unwrapException(e); 347 logError(parser.getRecordNumber(), "Unable to save: %s", LABEL_CSV_IMPORTER_UNABLE_TO_SAVE, 348 ue.getMessage()); 349 log.debug(ue, ue); 350 } 351 } finally { 352 commitOrRollbackTransaction(); 353 startTransaction(); 354 } 355 log.info(String.format("Done importing CSV file: %s", getBlob().getFilename())); 356 } 357 358 /** 359 * Import a line from the CSV file. 360 * 361 * @return {@code true} if a document has been created or updated, {@code false} otherwise. 362 * @since 6.0 363 */ 364 protected boolean importRecord(CSVRecord record, Map<String, Integer> header) { 365 String name = record.get(CSV_NAME_COL); 366 if (StringUtils.isBlank(name)) { 367 log.debug("record.isSet=" + record.isSet(CSV_NAME_COL)); 368 logError(record.getRecordNumber(), "Missing 'name' value", LABEL_CSV_IMPORTER_MISSING_NAME_VALUE); 369 return false; 370 } 371 372 Path targetPath = new Path(parentPath).append(name); 373 name = targetPath.lastSegment(); 374 String newParentPath = targetPath.removeLastSegments(1).toString(); 375 boolean exists = options.getCSVImporterDocumentFactory().exists(session, newParentPath, name, null); 376 377 DocumentRef docRef = null; 378 String type = null; 379 if (exists) { 380 docRef = new PathRef(targetPath.toString()); 381 type = session.getDocument(docRef).getType(); 382 } else { 383 if (hasTypeColumn) { 384 type = record.get(CSV_TYPE_COL); 385 } 386 if (StringUtils.isBlank(type)) { 387 log.debug("record.isSet=" + record.isSet(CSV_TYPE_COL)); 388 logError(record.getRecordNumber(), "Missing 'type' value", LABEL_CSV_IMPORTER_MISSING_TYPE_VALUE); 389 return false; 390 } 391 } 392 393 DocumentType docType = Framework.getLocalService(SchemaManager.class).getDocumentType(type); 394 if (docType == null) { 395 logError(record.getRecordNumber(), "The type '%s' does not exist", LABEL_CSV_IMPORTER_NOT_EXISTING_TYPE, 396 type); 397 return false; 398 } 399 Map<String, Serializable> properties = computePropertiesMap(record, docType, header); 400 if (properties == null) { 401 // skip this line 402 return false; 403 } 404 405 long lineNumber = record.getRecordNumber(); 406 if (exists) { 407 return updateDocument(lineNumber, docRef, properties); 408 } else { 409 return createDocument(lineNumber, newParentPath, name, type, properties); 410 } 411 } 412 413 /** 414 * @since 6.0 415 */ 416 protected Map<String, Serializable> computePropertiesMap(CSVRecord record, CompositeType compositeType, 417 Map<String, Integer> header) { 418 Map<String, Serializable> values = new HashMap<>(); 419 for (String headerValue : header.keySet()) { 420 String lineValue = record.get(headerValue); 421 lineValue = lineValue.trim(); 422 String fieldName = headerValue; 423 if (!CSV_NAME_COL.equals(headerValue) && !CSV_TYPE_COL.equals(headerValue)) { 424 if (AUTHORIZED_HEADERS.contains(headerValue) && !StringUtils.isBlank(lineValue)) { 425 values.put(headerValue, lineValue); 426 } else { 427 if (!compositeType.hasField(fieldName)) { 428 fieldName = fieldName.split(":")[1]; 429 } 430 if (compositeType.hasField(fieldName) && !StringUtils.isBlank(lineValue)) { 431 Serializable convertedValue = convertValue(compositeType, fieldName, headerValue, lineValue, 432 record.getRecordNumber()); 433 if (convertedValue == null) { 434 return null; 435 } 436 values.put(headerValue, convertedValue); 437 } 438 } 439 } 440 } 441 return values; 442 } 443 444 protected Serializable convertValue(CompositeType compositeType, String fieldName, String headerValue, 445 String stringValue, long lineNumber) { 446 if (compositeType.hasField(fieldName)) { 447 Field field = compositeType.getField(fieldName); 448 if (field != null) { 449 try { 450 Serializable fieldValue = null; 451 Type fieldType = field.getType(); 452 if (fieldType.isComplexType()) { 453 if (fieldType.getName().equals(CONTENT_FILED_TYPE_NAME)) { 454 String blobsFolderPath = Framework.getProperty(NUXEO_CSV_BLOBS_FOLDER); 455 String path = FilenameUtils.normalize(blobsFolderPath + "/" + stringValue); 456 File file = new File(path); 457 if (file.exists()) { 458 fieldValue = (Serializable) Blobs.createBlob(file); 459 } else { 460 logError(lineNumber, "The file '%s' does not exist", 461 LABEL_CSV_IMPORTER_NOT_EXISTING_FILE, stringValue); 462 return null; 463 } 464 } else { 465 fieldValue = (Serializable) ComplexTypeJSONDecoder.decode((ComplexType) fieldType, 466 stringValue); 467 } 468 } else { 469 if (fieldType.isListType()) { 470 Type listFieldType = ((ListType) fieldType).getFieldType(); 471 if (listFieldType.isSimpleType()) { 472 /* 473 * Array. 474 */ 475 fieldValue = stringValue.split(options.getListSeparatorRegex()); 476 } else { 477 /* 478 * Complex list. 479 */ 480 fieldValue = (Serializable) ComplexTypeJSONDecoder.decodeList((ListType) fieldType, 481 stringValue); 482 } 483 } else { 484 /* 485 * Primitive type. 486 */ 487 Type type = field.getType(); 488 if (type instanceof SimpleTypeImpl) { 489 type = type.getSuperType(); 490 } 491 if (type.isSimpleType()) { 492 if (type instanceof StringType) { 493 fieldValue = stringValue; 494 } else if (type instanceof IntegerType) { 495 fieldValue = Integer.valueOf(stringValue); 496 } else if (type instanceof LongType) { 497 fieldValue = Long.valueOf(stringValue); 498 } else if (type instanceof DoubleType) { 499 fieldValue = Double.valueOf(stringValue); 500 } else if (type instanceof BooleanType) { 501 fieldValue = Boolean.valueOf(stringValue); 502 } else if (type instanceof DateType) { 503 fieldValue = getDateFormat().parse(stringValue); 504 } 505 } 506 } 507 } 508 return fieldValue; 509 } catch (ParseException | NumberFormatException | IOException e) { 510 logError(lineNumber, "Unable to convert field '%s' with value '%s'", 511 LABEL_CSV_IMPORTER_CANNOT_CONVERT_FIELD_VALUE, headerValue, stringValue); 512 log.debug(e, e); 513 } 514 } 515 } else { 516 logError(lineNumber, "Field '%s' does not exist on type '%s'", LABEL_CSV_IMPORTER_NOT_EXISTING_FIELD, 517 headerValue, compositeType.getName()); 518 } 519 return null; 520 } 521 522 protected DateFormat getDateFormat() { 523 // transient field so may become null 524 if (dateformat == null) { 525 dateformat = new SimpleDateFormat(options.getDateFormat()); 526 } 527 return dateformat; 528 } 529 530 protected boolean createDocument(long lineNumber, String newParentPath, String name, String type, 531 Map<String, Serializable> properties) { 532 try { 533 DocumentRef parentRef = new PathRef(newParentPath); 534 if (session.exists(parentRef)) { 535 DocumentModel parent = session.getDocument(parentRef); 536 537 TypeManager typeManager = Framework.getLocalService(TypeManager.class); 538 if (options.checkAllowedSubTypes() && !typeManager.isAllowedSubType(type, parent.getType())) { 539 logError(lineNumber, "'%s' type is not allowed in '%s'", LABEL_CSV_IMPORTER_NOT_ALLOWED_SUB_TYPE, 540 type, parent.getType()); 541 } else { 542 options.getCSVImporterDocumentFactory().createDocument(session, newParentPath, name, type, 543 properties); 544 importLogs.add(new CSVImportLog(lineNumber, Status.SUCCESS, "Document created", 545 LABEL_CSV_IMPORTER_DOCUMENT_CREATED)); 546 return true; 547 } 548 } else { 549 logError(lineNumber, "Parent document '%s' does not exist", LABEL_CSV_IMPORTER_PARENT_DOES_NOT_EXIST, 550 newParentPath); 551 } 552 } catch (RuntimeException e) { 553 Throwable unwrappedException = unwrapException(e); 554 logError(lineNumber, "Unable to create document: %s", LABEL_CSV_IMPORTER_UNABLE_TO_CREATE, 555 unwrappedException.getMessage()); 556 log.debug(unwrappedException, unwrappedException); 557 } 558 return false; 559 } 560 561 protected boolean updateDocument(long lineNumber, DocumentRef docRef, Map<String, Serializable> properties) { 562 if (options.updateExisting()) { 563 try { 564 options.getCSVImporterDocumentFactory().updateDocument(session, docRef, properties); 565 importLogs.add(new CSVImportLog(lineNumber, Status.SUCCESS, "Document updated", 566 LABEL_CSV_IMPORTER_DOCUMENT_UPDATED)); 567 return true; 568 } catch (RuntimeException e) { 569 Throwable unwrappedException = unwrapException(e); 570 logError(lineNumber, "Unable to update document: %s", LABEL_CSV_IMPORTER_UNABLE_TO_UPDATE, 571 unwrappedException.getMessage()); 572 log.debug(unwrappedException, unwrappedException); 573 } 574 } else { 575 importLogs.add(new CSVImportLog(lineNumber, Status.SKIPPED, "Document already exists", 576 LABEL_CSV_IMPORTER_DOCUMENT_ALREADY_EXISTS)); 577 } 578 return false; 579 } 580 581 protected void logError(long lineNumber, String message, String localizedMessage, String... params) { 582 importLogs.add(new CSVImportLog(lineNumber, ERROR, String.format(message, (Object[]) params), localizedMessage, 583 params)); 584 String lineMessage = String.format("Line %d", lineNumber); 585 String errorMessage = String.format(message, (Object[]) params); 586 log.error(String.format("%s: %s", lineMessage, errorMessage)); 587 getStore().putParameter(id, "status", 588 new CSVImportStatus(CSVImportStatus.State.ERROR, docsCreatedCount, docsCreatedCount)); 589 } 590 591 protected void sendMail() { 592 UserManager userManager = Framework.getLocalService(UserManager.class); 593 NuxeoPrincipal principal = userManager.getPrincipal(username); 594 String email = principal.getEmail(); 595 if (email == null) { 596 log.info(String.format("Not sending import result email to '%s', no email configured", username)); 597 return; 598 } 599 600 try (OperationContext ctx = new OperationContext(session)) { 601 ctx.setInput(session.getRootDocument()); 602 603 CSVImporter csvImporter = Framework.getLocalService(CSVImporter.class); 604 List<CSVImportLog> importerLogs = csvImporter.getImportLogs(getId()); 605 CSVImportResult importResult = CSVImportResult.fromImportLogs(importerLogs); 606 List<CSVImportLog> skippedAndErrorImportLogs = csvImporter.getImportLogs(getId(), Status.SKIPPED, 607 Status.ERROR); 608 ctx.put("importResult", importResult); 609 ctx.put("skippedAndErrorImportLogs", skippedAndErrorImportLogs); 610 ctx.put("csvFilename", getBlob().getFilename()); 611 ctx.put("startDate", DateFormat.getInstance().format(startDate)); 612 ctx.put("username", username); 613 614 DocumentModel importFolder = session.getDocument(new PathRef(parentPath)); 615 String importFolderUrl = getDocumentUrl(importFolder); 616 ctx.put("importFolderTitle", importFolder.getTitle()); 617 ctx.put("importFolderUrl", importFolderUrl); 618 ctx.put("userUrl", getUserUrl()); 619 620 StringList to = buildRecipientsList(email); 621 Expression from = Scripting.newExpression("Env[\"mail.from\"]"); 622 String subject = "CSV Import result of " + getBlob().getFilename(); 623 String message = loadTemplate(TEMPLATE_IMPORT_RESULT); 624 625 OperationChain chain = new OperationChain("SendMail"); 626 chain.add(SendMail.ID).set("from", from).set("to", to).set("HTML", true).set("subject", subject) 627 .set("message", message); 628 Framework.getLocalService(AutomationService.class).run(ctx, chain); 629 } catch (Exception e) { 630 ExceptionUtils.checkInterrupt(e); 631 log.error(String.format("Unable to notify user '%s' for import result of '%s': %s", username, 632 getBlob().getFilename(), e.getMessage())); 633 log.debug(e, e); 634 throw ExceptionUtils.runtimeException(e); 635 } 636 } 637 638 /** 639 * @since 9.1 640 */ 641 private Blob getBlob() { 642 return getStore().getBlobs(id).get(0); 643 } 644 645 protected String getDocumentUrl(DocumentModel doc) { 646 return MailTemplateHelper.getDocumentUrl(doc, null); 647 } 648 649 protected String getUserUrl() { 650 DocumentViewCodecManager codecService = Framework.getService(DocumentViewCodecManager.class); 651 DocumentViewCodec codec = codecService.getCodec(NotificationEventListener.NOTIFICATION_DOCUMENT_ID_CODEC_NAME); 652 boolean isNotificationCodec = codec != null; 653 boolean isJSFUI = isNotificationCodec 654 && NotificationEventListener.JSF_NOTIFICATION_DOCUMENT_ID_CODEC_PREFIX.equals(codec.getPrefix()); 655 StringBuilder userUrl = new StringBuilder(); 656 if (isNotificationCodec) { 657 658 userUrl.append(NotificationServiceHelper.getNotificationService().getServerUrlPrefix()); 659 if (!isJSFUI) { 660 userUrl.append("ui/"); 661 userUrl.append("#!/"); 662 } 663 userUrl.append("user/").append(username); 664 } 665 return userUrl.toString(); 666 } 667 668 protected StringList buildRecipientsList(String userEmail) { 669 String csvMailTo = Framework.getProperty(NUXEO_CSV_MAIL_TO); 670 if (StringUtils.isBlank(csvMailTo)) { 671 return new StringList(new String[] { userEmail }); 672 } else { 673 return new StringList(new String[] { userEmail, csvMailTo }); 674 } 675 } 676 677 private static String loadTemplate(String key) { 678 InputStream io = CSVImporterWork.class.getClassLoader().getResourceAsStream(key); 679 if (io != null) { 680 try { 681 return IOUtils.toString(io, Charsets.UTF_8); 682 } catch (IOException e) { 683 // cannot happen 684 throw new NuxeoException(e); 685 } finally { 686 try { 687 io.close(); 688 } catch (IOException e) { 689 // nothing to do 690 } 691 } 692 } 693 return null; 694 } 695 696 public static Throwable unwrapException(Throwable t) { 697 Throwable cause = null; 698 if (t != null) { 699 cause = t.getCause(); 700 } 701 if (cause == null) { 702 return t; 703 } else { 704 return unwrapException(cause); 705 } 706 } 707 708}