/* * (C) Copyright 2012-2013 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: */ package org.nuxeo.ecm.user.center.profile; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.Serializable; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.annotation.Experimental; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.impl.blob.FileBlob; import org.nuxeo.ecm.core.schema.DocumentType; import org.nuxeo.ecm.core.schema.SchemaManager; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; import org.nuxeo.ecm.core.schema.types.primitives.DateType; import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; import org.nuxeo.ecm.core.schema.types.primitives.LongType; import org.nuxeo.ecm.core.schema.types.primitives.StringType; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.transaction.TransactionHelper; /** * * @since 7.2 */ @Experimental(comment="https://jira.nuxeo.com/browse/NXP-12200") public class UserProfileImporter { private static final Log log = LogFactory.getLog(UserProfileImporter.class); public static final String CONTENT_FILED_TYPE_NAME = "content"; public static final String USER_PROFILE_IMPORTER_USERNAME_COL = "username"; protected Character escapeCharacter = '\\'; protected ImporterConfig config; protected String dataFileName; protected transient DateFormat dateformat; protected final Date startDate; protected long totalRecords = 0; protected long currentRecord = 0; public static final String BLOB_FOLDER_PROPERTY = "nuxeo.csv.blobs.folder"; public UserProfileImporter() { startDate = new Date(); } public void doImport(CoreSession session) { UserProfileService ups = Framework.getLocalService(UserProfileService.class); config = ups.getImporterConfig(); if (config == null) { log.error("No importer configuration could be found"); return; } dataFileName = config.getDataFileName(); if (dataFileName == null) { log.error("No importer dataFileName was supplied"); return; } InputStream is = getResourceAsStream(dataFileName); if (is == null) { log.error("Error locating CSV data file: " + dataFileName); return; } Reader in = new BufferedReader(new InputStreamReader(is)); CSVParser parser = null; try { parser = CSVFormat.DEFAULT.withEscape(escapeCharacter).withHeader().parse(in); doImport(session, parser, ups); } catch (IOException e) { log.error("Unable to read CSV file", e); } finally { if (parser != null) { try { parser.close(); } catch (IOException e) { log.debug(e, e); } } } } protected InputStream getResourceAsStream(String resource) { InputStream is = getClass().getClassLoader().getResourceAsStream(resource); if (is == null) { is = Framework.getResourceLoader().getResourceAsStream(resource); if (is == null) { return null; } } return is; } public void doImport(CoreSession session, CSVParser parser, UserProfileService userProfileService) throws IOException { log.info(String.format("Importing CSV file: %s", dataFileName)); DocumentType docType = Framework.getLocalService(SchemaManager.class).getDocumentType( UserProfileConstants.USER_PROFILE_DOCTYPE); if (docType == null) { log.error("The type " + UserProfileConstants.USER_PROFILE_DOCTYPE + " does not exist"); return; } Map<String, Integer> header = parser.getHeaderMap(); if (header == null) { // empty file? log.error("No header line, empty file?"); return; } // find the index for the required name and type values Integer nameIndex = header.get(UserProfileImporter.USER_PROFILE_IMPORTER_USERNAME_COL); if (nameIndex == null) { log.error("Missing 'username' column"); return; } long docsUpdatedCount = 0; totalRecords = parser.getRecordNumber(); try { int batchSize = config.getBatchSize(); long lineNumber = 0; for (CSVRecord record : parser.getRecords()) { lineNumber++; currentRecord = lineNumber; try { if (importLine(record, lineNumber, nameIndex, docType, session, userProfileService, header)) { docsUpdatedCount++; if (docsUpdatedCount % batchSize == 0) { commitOrRollbackTransaction(); startTransaction(); } } } catch (NuxeoException e) { // try next line Throwable unwrappedException = unwrapException(e); logImportError(lineNumber, "Error while importing line: %s", unwrappedException.getMessage()); log.debug(unwrappedException, unwrappedException); } } session.save(); } finally { commitOrRollbackTransaction(); startTransaction(); } log.info(String.format("Done importing %s entries from CSV file: %s", docsUpdatedCount, dataFileName)); } /** * Import a line from the CSV file. * * @param userProfileService * @param docType * @param session * @return {@code true} if a document has been created or updated, {@code false} otherwise. */ protected boolean importLine(CSVRecord record, final long lineNumber, Integer nameIndex, DocumentType docType, CoreSession session, UserProfileService userProfileService, Map<String, Integer> headerValues) { final String name = record.get(nameIndex); if (StringUtils.isBlank(name)) { logImportError(lineNumber, "Missing 'name' value", "label.csv.importer.missingNameValue"); return false; } Map<String, Serializable> values = computePropertiesMap(lineNumber, docType, headerValues, record); if (values == null) { // skip this line return false; } return updateDocument(lineNumber, name, docType, session, userProfileService, values); } protected Map<String, Serializable> computePropertiesMap(long lineNumber, DocumentType docType, Map<String, Integer> headerValues, CSVRecord record) { Map<String, Serializable> values = new HashMap<String, Serializable>(); for (String headerValue : headerValues.keySet()) { String lineValue = record.get(headerValue); lineValue = lineValue.trim(); String fieldName = headerValue; if (!UserProfileImporter.USER_PROFILE_IMPORTER_USERNAME_COL.equals(headerValue)) { if (!docType.hasField(fieldName)) { fieldName = fieldName.split(":")[1]; } if (docType.hasField(fieldName) && !StringUtils.isBlank(lineValue)) { Serializable convertedValue = convertValue(docType, fieldName, headerValue, lineValue, lineNumber); if (convertedValue == null) { return null; } values.put(headerValue, convertedValue); } } } return values; } protected Serializable convertValue(DocumentType docType, String fieldName, String headerValue, String stringValue, long lineNumber) { if (docType.hasField(fieldName)) { Field field = docType.getField(fieldName); if (field != null) { try { Serializable fieldValue = null; Type fieldType = field.getType(); if (fieldType.isComplexType()) { if (fieldType.getName().equals(CONTENT_FILED_TYPE_NAME)) { String blobsFolderPath = Framework.getProperty(BLOB_FOLDER_PROPERTY); String path = FilenameUtils.normalize(blobsFolderPath + "/" + stringValue); File file = new File(path); if (file.exists()) { FileBlob blob = new FileBlob(file); blob.setFilename(file.getName()); fieldValue = blob; } else { logImportError(lineNumber, "The file '%s' does not exist", stringValue); return null; } } // other types not supported } else { if (fieldType.isListType()) { Type listFieldType = ((ListType) fieldType).getFieldType(); if (listFieldType.isSimpleType()) { /* * Array. */ fieldValue = stringValue.split(config.getListSeparatorRegex()); } else { /* * Complex list. */ fieldValue = (Serializable) Arrays.asList(stringValue.split(config.getListSeparatorRegex())); } } else { /* * Primitive type. */ Type type = field.getType(); if (type instanceof SimpleTypeImpl) { type = type.getSuperType(); } if (type.isSimpleType()) { if (type instanceof StringType) { fieldValue = stringValue; } else if (type instanceof IntegerType) { fieldValue = Integer.valueOf(stringValue); } else if (type instanceof LongType) { fieldValue = Long.valueOf(stringValue); } else if (type instanceof DoubleType) { fieldValue = Double.valueOf(stringValue); } else if (type instanceof BooleanType) { fieldValue = Boolean.valueOf(stringValue); } else if (type instanceof DateType) { fieldValue = getDateFormat().parse(stringValue); } } } } return fieldValue; } catch (ParseException pe) { logImportError(lineNumber, "Unable to convert field '%s' with value '%s'", headerValue, stringValue); log.debug(pe, pe); } catch (NumberFormatException nfe) { logImportError(lineNumber, "Unable to convert field '%s' with value '%s'", headerValue, stringValue); log.debug(nfe, nfe); } } } else { logImportError(lineNumber, "Field '%s' does not exist on type '%s'", headerValue, docType.getName()); } return null; } protected DateFormat getDateFormat() { // transient field so may become null if (dateformat == null) { dateformat = new SimpleDateFormat(config.getDateFormat()); } return dateformat; } protected boolean updateDocument(long lineNumber, String name, DocumentType docType, CoreSession session, UserProfileService userProfileService, Map<String, Serializable> properties) { DocumentModel doc = userProfileService.getUserProfileDocument(name, session); Calendar createdDate = (Calendar) doc.getPropertyValue("dc:created"); boolean isCreated = (createdDate.getTime().after(startDate)); if (!isCreated && !config.isUpdateExisting()) { logImportInfo(lineNumber, "Document already exists for user: %s", name); return false; } for (Map.Entry<String, Serializable> entry : properties.entrySet()) { doc.setPropertyValue(entry.getKey(), entry.getValue()); } try { session.saveDocument(doc); } catch (NuxeoException e) { Throwable unwrappedException = unwrapException(e); logImportError(lineNumber, "Unable to update document for user: %s: %s", name, unwrappedException.getMessage()); log.debug(unwrappedException, unwrappedException); return false; } return true; } /** * Releases the transaction resources by committing the existing transaction (if any). This is recommended before * running a long process. */ protected void commitOrRollbackTransaction() { if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { TransactionHelper.commitOrRollbackTransaction(); } } /** * Starts a new transaction. * <p> * Usually called after {@code commitOrRollbackTransaction()}, for instance for saving back the results of a long * process. * * @return true if a new transaction was started */ protected boolean startTransaction() { return TransactionHelper.startTransaction(); } protected void logImportError(long lineNumber, String message, String... params) { String lineMessage = String.format("Line %d", lineNumber); String errorMessage = String.format(message, (Object[]) params); log.error(String.format("%s: %s", lineMessage, errorMessage)); } protected void logImportInfo(long lineNumber, String message, String... params) { String lineMessage = String.format("Line %d", lineNumber); String infoMessage = String.format(message, (Object[]) params); log.info(String.format("%s: %s", lineMessage, infoMessage)); } public static Throwable unwrapException(Throwable t) { Throwable cause = null; if (t != null) { cause = t.getCause(); } if (cause == null) { return t; } else { return unwrapException(cause); } } public long getTotalRecords() { return totalRecords; } public long getCurrentRecord() { return currentRecord; } }