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