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