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