org.alfresco.repo.web.scripts.person.UserCSVUploadPost.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.web.scripts.person.UserCSVUploadPost.java

Source

/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.web.scripts.person;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;

import javax.transaction.UserTransaction;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.person.PersonServiceImpl;
import org.alfresco.repo.tenant.TenantDomainMismatchException;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVStrategy;
import org.apache.commons.lang.mutable.MutableInt;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.hssf.util.PaneInformation;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.extensions.webscripts.DeclarativeWebScript;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.servlet.FormData;

/**
 * Webscript implementation for the POST method for uploading a
 *  CSV of users to have them created.
 * 
 * @author Nick Burch
 * @since 3.5
 */
public class UserCSVUploadPost extends DeclarativeWebScript {
    protected static final QName[] COLUMNS = new QName[] { ContentModel.PROP_USERNAME, ContentModel.PROP_FIRSTNAME,
            ContentModel.PROP_LASTNAME, ContentModel.PROP_EMAIL, null, ContentModel.PROP_PASSWORD,
            ContentModel.PROP_ORGANIZATION, ContentModel.PROP_JOBTITLE, ContentModel.PROP_LOCATION,
            ContentModel.PROP_TELEPHONE, ContentModel.PROP_MOBILE, ContentModel.PROP_SKYPE,
            ContentModel.PROP_INSTANTMSG, ContentModel.PROP_GOOGLEUSERNAME, ContentModel.PROP_COMPANYADDRESS1,
            ContentModel.PROP_COMPANYADDRESS2, ContentModel.PROP_COMPANYADDRESS3, ContentModel.PROP_COMPANYPOSTCODE,
            ContentModel.PROP_COMPANYTELEPHONE, ContentModel.PROP_COMPANYFAX, ContentModel.PROP_COMPANYEMAIL };

    private static final String ERROR_BAD_FORM = "person.err.userCSV.invalidForm";
    private static final String ERROR_NO_FILE = "person.err.userCSV.noFile";
    private static final String ERROR_CORRUPT_FILE = "person.err.userCSV.corruptFile";
    private static final String ERROR_GENERAL = "person.err.userCSV.general";
    private static final String ERROR_BLANK_COLUMN = "person.err.userCSV.blankColumn";
    private static final String MSG_CREATED = "person.msg.userCSV.created";
    private static final String MSG_EXISTING = "person.msg.userCSV.existing";

    private static Log logger = LogFactory.getLog(UserCSVUploadPost.class);

    private MutableAuthenticationService authenticationService;
    private AuthorityService authorityService;
    private PersonService personService;
    private TenantService tenantService;
    private DictionaryService dictionaryService;
    private RetryingTransactionHelper retryingTransactionHelper;

    /**
     * @param authenticationService    the AuthenticationService to set
     */
    public void setAuthenticationService(MutableAuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    /**
     * @param authorityService          the AuthorityService to set
     */
    public void setAuthorityService(AuthorityService authorityService) {
        this.authorityService = authorityService;
    }

    /**
     * @param personService          the PersonService to set
     */
    public void setPersonService(PersonService personService) {
        this.personService = personService;
    }

    /**
     * @param tenantService          the TenantService to set
     */
    public void setTenantService(TenantService tenantService) {
        this.tenantService = tenantService;
    }

    /**
     * @param dictionaryService          the DictionaryService to set
     */
    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    /**
     * @param retryingTransactionHelper  the helper for running transactions to set
     */
    public void setTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) {
        this.retryingTransactionHelper = retryingTransactionHelper;
    }

    /**
     * @see DeclarativeWebScript#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status)
     */
    @Override
    protected Map<String, Object> executeImpl(WebScriptRequest req, Status status) {
        final List<Map<QName, String>> users = new ArrayList<Map<QName, String>>();
        final ResourceBundle rb = getResources();

        // Try to load the user details from the upload
        FormData form = (FormData) req.parseContent();
        if (form == null || !form.getIsMultiPart()) {
            throw new ResourceBundleWebScriptException(Status.STATUS_BAD_REQUEST, rb, ERROR_BAD_FORM);
        }

        boolean processed = false;
        for (FormData.FormField field : form.getFields()) {
            if (field.getIsFile()) {
                processUpload(field.getInputStream(), field.getFilename(), users);
                processed = true;
                break;
            }
        }

        if (!processed) {
            throw new ResourceBundleWebScriptException(Status.STATUS_BAD_REQUEST, rb, ERROR_NO_FILE);
        }

        // Should we send emails?
        boolean sendEmails = true;
        if (req.getParameter("email") != null) {
            sendEmails = Boolean.parseBoolean(req.getParameter("email"));
        }

        if (form.hasField("email")) {
            sendEmails = Boolean.parseBoolean(form.getParameters().get("email")[0]);
        }

        // Now process the users
        final MutableInt totalUsers = new MutableInt(0);
        final MutableInt addedUsers = new MutableInt(0);
        final Map<String, String> results = new HashMap<String, String>();
        final boolean doSendEmails = sendEmails;

        // Do the work in a new transaction, so that if we hit a problem
        //  during the commit stage (eg too many users) then we get to
        //  hear about it, and handle it ourselves.
        // Otherwise, commit exceptions occur deep inside RepositoryContainer
        //  and we can't control the status code
        RetryingTransactionCallback<Void> work = new RetryingTransactionCallback<Void>() {
            public Void execute() throws Throwable {
                try {
                    doAddUsers(totalUsers, addedUsers, results, users, rb, doSendEmails);
                    return null;
                } catch (Throwable t) {
                    // Make sure we rollback from this
                    UserTransaction userTrx = RetryingTransactionHelper.getActiveUserTransaction();
                    if (userTrx != null && userTrx.getStatus() != javax.transaction.Status.STATUS_MARKED_ROLLBACK) {
                        try {
                            userTrx.setRollbackOnly();
                        } catch (Throwable t2) {
                        }
                    }
                    // Report the problem further down
                    throw t;
                }
            }
        };

        try {
            retryingTransactionHelper.doInTransaction(work);
        } catch (Throwable t) {
            // Tell the client of the problem
            if (t instanceof WebScriptException) {
                // We've already wrapped it properly, all good
                throw (WebScriptException) t;
            } else {
                // Something unexpected has ripped up
                // Return the details with a 200, so that Share does the right thing
                throw new ResourceBundleWebScriptException(Status.STATUS_OK, rb, ERROR_GENERAL, t);
            }
        }

        // If we get here, then adding the users didn't throw any exceptions,
        //  so tell the client which users went in and which didn't
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("totalUsers", totalUsers);
        model.put("addedUsers", addedUsers);
        model.put("users", results);

        return model;
    }

    private void doAddUsers(final MutableInt totalUsers, final MutableInt addedUsers,
            final Map<String, String> results, final List<Map<QName, String>> users, final ResourceBundle rb,
            final boolean sendEmails) {
        for (Map<QName, String> user : users) {
            totalUsers.setValue(totalUsers.intValue() + 1);

            // Grab the username, and do any MT magic on it
            String username = user.get(ContentModel.PROP_USERNAME);
            try {
                username = PersonServiceImpl.updateUsernameForTenancy(username, tenantService);
            } catch (TenantDomainMismatchException e) {
                throw new ResourceBundleWebScriptException(Status.STATUS_OK, rb, ERROR_GENERAL, e);
            }

            // Do they already exist?
            if (personService.personExists(username)) {
                results.put(username, rb.getString(MSG_EXISTING));
                if (logger.isDebugEnabled()) {
                    logger.debug("Not creating user as already exists: " + username + " for " + user);
                }
            } else {
                String password = user.get(ContentModel.PROP_PASSWORD);
                user.remove(ContentModel.PROP_PASSWORD); // Not for the person service

                try {
                    // Add the person
                    personService.createPerson((Map<QName, Serializable>) (Map) user);

                    // Add the underlying user
                    authenticationService.createAuthentication(username, password.toCharArray());

                    // If required, notify the user
                    if (sendEmails) {
                        personService.notifyPerson(username, password);
                    }

                    if (logger.isDebugEnabled()) {
                        logger.debug("Creating user from upload: " + username + " for " + user);
                    }

                    // All done
                    String msg = MessageFormat.format(rb.getString(MSG_CREATED),
                            new Object[] { user.get(ContentModel.PROP_EMAIL) });
                    results.put(username, msg);
                    addedUsers.setValue(addedUsers.intValue() + 1);
                } catch (Throwable t) {
                    throw new ResourceBundleWebScriptException(Status.STATUS_OK, rb, ERROR_GENERAL, t);
                }
            }
        }
    }

    protected void processUpload(InputStream input, String filename, List<Map<QName, String>> users) {
        try {
            if (filename != null && filename.length() > 0) {
                if (filename.endsWith(".csv")) {
                    processCSVUpload(input, users);
                    return;
                }
                if (filename.endsWith(".xls")) {
                    processXLSUpload(input, users);
                    return;
                }
                if (filename.endsWith(".xlsx")) {
                    processXLSXUpload(input, users);
                    return;
                }
            }

            // If in doubt, assume it's probably a .csv
            processCSVUpload(input, users);
        } catch (IOException e) {
            // Return the error as a 200 so the user gets a friendly
            //  display of the error message in share
            throw new ResourceBundleWebScriptException(Status.STATUS_OK, getResources(), ERROR_CORRUPT_FILE, e);
        }
    }

    protected void processCSVUpload(InputStream input, List<Map<QName, String>> users) throws IOException {
        InputStreamReader reader = new InputStreamReader(input, Charset.forName("UTF-8"));
        CSVParser csv = new CSVParser(reader, CSVStrategy.EXCEL_STRATEGY);
        String[][] data = csv.getAllValues();
        if (data != null && data.length > 0) {
            processSpreadsheetUpload(data, users);
        }
    }

    protected void processXLSUpload(InputStream input, List<Map<QName, String>> users) throws IOException {
        Workbook wb = new HSSFWorkbook(input);
        processSpreadsheetUpload(wb, users);
    }

    protected void processXLSXUpload(InputStream input, List<Map<QName, String>> users) throws IOException {
        Workbook wb = new XSSFWorkbook(input);
        processSpreadsheetUpload(wb, users);
    }

    private void processSpreadsheetUpload(Workbook wb, List<Map<QName, String>> users) throws IOException {
        if (wb.getNumberOfSheets() > 1) {
            logger.info("Uploaded Excel file has " + wb.getNumberOfSheets()
                    + " sheets, ignoring  all except the first one");
        }

        int firstRow = 0;
        Sheet s = wb.getSheetAt(0);
        DataFormatter df = new DataFormatter();

        String[][] data = new String[s.getLastRowNum() + 1][];

        // If there is a heading freezepane row, skip it
        PaneInformation pane = s.getPaneInformation();
        if (pane != null && pane.isFreezePane() && pane.getHorizontalSplitTopRow() > 0) {
            firstRow = pane.getHorizontalSplitTopRow();
            logger.debug("Skipping excel freeze header of " + firstRow + " rows");
        }

        // Process each row in turn, getting columns up to our limit
        for (int row = firstRow; row <= s.getLastRowNum(); row++) {
            Row r = s.getRow(row);
            if (r != null) {
                String[] d = new String[COLUMNS.length];
                for (int cn = 0; cn < COLUMNS.length; cn++) {
                    Cell cell = r.getCell(cn);
                    if (cell != null && cell.getCellType() != Cell.CELL_TYPE_BLANK) {
                        d[cn] = df.formatCellValue(cell);
                    }
                }
                data[row] = d;
            }
        }

        // Handle the contents
        processSpreadsheetUpload(data, users);
    }

    /**
     * Builds user objects based on the supplied data. If a row is empty, then
     *  the child String array should be empty too. (Needs to be present so that
     *  the line number reporting works)
     */
    private void processSpreadsheetUpload(String[][] data, List<Map<QName, String>> users) {
        // What we consider to be the literal string "user name" when detecting
        //  if a row is an example/header one or not
        // Note - all of these want to be lower case!
        List<String> usernameIsUsername = new ArrayList<String>();
        // The English literals
        usernameIsUsername.add("username");
        usernameIsUsername.add("user name");
        // And the localised form too if found
        PropertyDefinition unPD = dictionaryService.getProperty(ContentModel.PROP_USERNAME);
        if (unPD != null) {
            if (unPD.getTitle(dictionaryService) != null)
                usernameIsUsername.add(unPD.getTitle(dictionaryService).toLowerCase());
            if (unPD.getDescription(dictionaryService) != null)
                usernameIsUsername.add(unPD.getDescription(dictionaryService).toLowerCase());
        }

        // Process the contents of the spreadsheet
        for (int lineNumber = 0; lineNumber < data.length; lineNumber++) {
            Map<QName, String> user = new HashMap<QName, String>();
            String[] userData = data[lineNumber];

            if (userData == null || userData.length == 0
                    || (userData.length == 1 && userData[0].trim().length() == 0)) {
                // Empty line, skip
                continue;
            }

            boolean required = true;
            for (int i = 0; i < COLUMNS.length; i++) {
                if (COLUMNS[i] == null) {
                    required = false;
                    continue;
                }

                String value = null;
                if (userData.length > i) {
                    value = userData[i];
                }
                if (value == null || value.length() == 0) {
                    if (required) {
                        throw new ResourceBundleWebScriptException(Status.STATUS_OK, getResources(),
                                ERROR_BLANK_COLUMN,
                                new Object[] { COLUMNS[i].getLocalName(), (i + 1), (lineNumber + 1) });
                    }
                } else {
                    user.put(COLUMNS[i], value);
                }
            }

            // If no password was given, use their surname
            if (!user.containsKey(ContentModel.PROP_PASSWORD)) {
                user.put(ContentModel.PROP_PASSWORD, user.get(ContentModel.PROP_LASTNAME));
            }

            // Skip any user who looks like an example file heading
            // i.e. a username of "username" or "user name"
            String username = user.get(ContentModel.PROP_USERNAME).toLowerCase();
            if (usernameIsUsername.contains(username)) {
                // Skip
            } else {
                // Looks like a real line, keep it
                users.add(user);
            }
        }
    }

    protected static class ResourceBundleWebScriptException extends WebScriptException {
        private String message;

        public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId, Object... args) {
            super(status, msgId, args);
            buildMessageIfPossible(rb, msgId, args);
        }

        public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId) {
            super(status, msgId);
            buildMessageIfPossible(rb, msgId, new Object[0]);
        }

        public ResourceBundleWebScriptException(int status, ResourceBundle rb, String msgId, Throwable cause) {
            super(status, msgId, cause);
            buildMessageIfPossible(rb, msgId, new Object[0]);
        }

        public String getMessage() {
            return message;
        }

        private void buildMessageIfPossible(ResourceBundle rb, String msgId, Object... args) {
            String msg = rb.getString(msgId);
            if (msg != null) {
                if (args == null || args.length == 0) {
                    message = msg;
                } else {
                    message = MessageFormat.format(msg, args);
                }
            }
            if (getCause() != null) {
                message = message + "\n" + getCause().getMessage();
            }
        }
    }
}