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