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