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