/*
 * #%L
 * Alfresco Repository
 * %%
 * 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.tools;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.alfresco.model.ContentModel;
import org.alfresco.repo.batch.BatchProcessWorkProvider;
import org.alfresco.repo.batch.BatchProcessor;
import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorkerAdaptor;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.person.PersonServiceImpl;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.NoSuchPersonException;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.VmShutdownListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Rename user tool. This tool provides minimal support for renaming users.
 * See {@link #displayHelp} message for restrictions.
 * <pre>
 * Usage: renameUser -user username [options] oldUsername newUsername");
 *        renameUser -user username [options] -file filename");
 * </pre>
 * The csv file has a simple comma separated list, with
 * a pair of usernames on each line. Comments and blank
 * lines may also be included. For example:
 * <pre>
 * # List of usernames to change
 * 
 * # oldUsername,newUsername
 * johnp,ceo # President and CEO
 * johnn,cto # CTO and Chairman
 * </pre>
 * 
 * @author Alan Davis
 */
public class RenameUser extends Tool
{
    private static Log logger = LogFactory.getLog(RenameUser.class);
    
    /** User Rename Tool Context */
    protected RenameUserToolContext context;
    private boolean login = true;
    
    PersonService personService;
    NodeService nodeService;

    private PersonService getPersonService()
    {
        if (personService == null)
        {
            personService = getServiceRegistry().getPersonService();
        }
        return personService;
    }

    private NodeService getNodeService()
    {
        if (nodeService == null)
        {
            nodeService = getServiceRegistry().getNodeService();
        }
        return nodeService;
    }
    
    public void setLogin(boolean login)
    {
        this.login = login;
    }

    /**
     * Entry Point
     * 
     * @param args
     */
    public static void main(String[] args)
    {
        Tool tool = new RenameUser();
        tool.start(args);
    }
    
    /*
     * (non-Javadoc)
     * @see org.alfresco.tools.Tool#processArgs(java.lang.String[])
     */
    @Override
    protected ToolContext processArgs(String[] args)
        throws ToolArgumentException
    {
        context = new RenameUserToolContext();
        context.setLogin(login);

        int i = 0;
        while (i < args.length)
        {
            if (args[i].equals("-h") || args[i].equals("-help"))
            {
                context.setHelp(true);
                break;
            }
            else if (args[i].equals("-user"))
            {
                i++;
                if (i == args.length || args[i].length() == 0)
                {
                    throw new ToolArgumentException("The value <user> for the option -user must be specified");
                }
                context.setUsername(args[i]);
            }
            else if (args[i].equals("-pwd"))
            {
                i++;
                if (i == args.length || args[i].length() == 0)
                {
                    throw new ToolArgumentException("The value <password> for the option -pwd must be specified");
                }
                context.setPassword(args[i]);
            }
            else if (args[i].equals("-encoding"))
            {
                i++;
                if (i == args.length || args[i].length() == 0)
                {
                    throw new ToolArgumentException("The value <encoding> for the option -encoding must be specified");
                }
                try
                {
                    context.encoding = Charset.forName(args[i]);
                }
                catch (IllegalCharsetNameException e)
                { 
                    throw new ToolArgumentException("The value <encoding> is not recognised");
                }
                catch (UnsupportedCharsetException e)
                {
                    throw new ToolArgumentException("The value <encoding> is unsupported");
                }
            }
            else if (args[i].equals("-quiet"))
            {
                context.setQuiet(true);
            }
            else if (args[i].equals("-verbose"))
            {
                context.setVerbose(true);
            }
            else if (args[i].equals("-f") || args[i].equals("-file"))
            {
                i++;
                if (i == args.length || args[i].length() == 0)
                {
                    throw new ToolArgumentException("The value <filename> for the option -file must be specified");
                }
                context.setFilename(args[i]);
            }
            else if (!args[i].startsWith("-"))
            {
                i++;
                if (i == args.length || args[i-1].trim().length() == 0 || args[i].trim().length() == 0)
                {
                    throw new ToolArgumentException("Both <oldUsername> <newUsername> must be specified");
                }
                if (context.userCount() > 0)
                {
                    throw new ToolArgumentException("Only one <oldUsername> <newUsername> pair may be " +
                    		"specified on the command line. See the -file option");
                }
                String oldUsername = args[i-1].trim();
                String newUsername = args[i].trim();
                String error = context.add(-1, null, oldUsername, newUsername);
                if (error != null)
                {
                    throw new ToolArgumentException(error);
                }
            }
            else
            {
                throw new ToolArgumentException("Unknown option " + args[i]);
            }

            // next argument
            i++;
        }

        return context;
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.tools.Tool#displayHelp()
     */
    protected @Override
    /*package*/ void displayHelp()
    {
        logError("This tool provides minimal support for renaming users. It fixes");
        logError("authorities, group memberships and current zone (older versions");
        logError("still require a property change).");
        logError("");
        logError("WARNING: It does NOT change properties that store the username such");
        logError("         as (creator, modifier, lock owner or owner). Of these owner");
        logError("         and lock affect user rights. The username is also used");
        logError("         directly in workflow, for RM caveats, for Share invites and");
        logError("         auditing");
        logError("");
        logError("Usage: renameUser -user username [options] oldUsername newUsername");
        logError("       renameUser -user username [options] -file filename");
        logError("");
        logError("   username: username for login");
        logError("oldUsername: current username ");
        logError("newUsername: replacement username ");
        logError("");
        logError("Options:");
        logError(" -h[elp] display this help");
        logError(" -pwd password for login");
        logError(" -f[ile] csv file of old and new usernames");
        logError(" -encoding for source file (default: " + Charset.defaultCharset() + ")");
        logError(" -quiet do not display any messages during rename");
        logError(" -verbose report rename progress");
    }
    
    /*
     * (non-Javadoc)
     * @see org.alfresco.tools.Tool#getToolName()
     */
    @Override
    protected String getToolName()
    {
        return "Alfresco Rename User";
    }
    
    /*
     * (non-Javadoc)
     * @see org.alfresco.tools.Tool#execute()
     */
    @Override
    protected int execute() throws ToolException
    {
        // Used for ability to be final and have a set
        final AtomicInteger status = new AtomicInteger(0);

        BatchProcessWorker<User> worker = new BatchProcessWorkerAdaptor<User>()
        {
            public void process(final User user) throws Throwable
            {
                RunAsWork<Void> runAsWork = new RunAsWork<Void>()
                {
                    @Override
                    public Void doWork() throws Exception
                    {
                        try
                        {
                            renameUser(user.getOldUsername(), user.getNewUsername());
                        }
                        catch (Throwable t)
                        {
                            status.set(handleError(t));
                        }
                        return null;
                    }
                };
                AuthenticationUtil.runAs(runAsWork, context.getUsername());
            }
        };
        
        // Use 2 threads, 20 User objects per transaction. Log every 100 entries.
        BatchProcessor<User> processor = new BatchProcessor<User>(
                "HomeFolderProviderSynchronizer",
                getServiceRegistry().getTransactionService().getRetryingTransactionHelper(),
                new WorkProvider(context),
                2, 20,
                null,
                logger, 100);
        processor.process(worker, true);

        return status.get();
    }
    
    private void renameUser(String oldUsername, String newUsername)
    {
        logInfo("\""+oldUsername+"\" --> \""+newUsername+"\""); 
        try
        {
            NodeRef person = getPersonService().getPerson(oldUsername, false);
            
            // Allow us to update the username just like the LDAP process
            AlfrescoTransactionSupport.bindResource(PersonServiceImpl.KEY_ALLOW_UID_UPDATE, Boolean.TRUE);

            // Update the username property which will result in a PersonServiceImpl.onUpdateProperties call
            // on commit.
            getNodeService().setProperty(person, ContentModel.PROP_USERNAME, newUsername);
        }
        catch (NoSuchPersonException e)
        {
            logError("User does not exist: "+oldUsername);
        }
    }
    
    public class User
    {
        private final String oldUsername;
        private final String newUsername;
        
        public User(String oldUsername, String newUsername)
        {
            this.oldUsername = oldUsername;
            this.newUsername = newUsername;
        }

        public String getOldUsername()
        {
            return oldUsername;
        }

        public String getNewUsername()
        {
            return newUsername;
        }
    }
    
    public class RenameUserToolContext extends ToolContext
    {
        /**
         * Old and new usernames to change.
         */
        private List<User> usernames = new ArrayList<User>();
        
        // Internal - used check the name has not been used before.
        private Set<String> uniqueNames = new HashSet<String>();
        
        /**
         * Source filename of usernames.
         */
        private String filename;
        
        /**
         * Encoding of filename of usernames.
         */
        private Charset encoding = Charset.defaultCharset();
        
        public void setFilename(String filename)
        {
            this.filename = filename;
        }
        
        public String add(int lineNumber, String line, String oldUsername, String newUsername)
        {
            String error = null;
            if (oldUsername.equals(newUsername))
            {
                error = "Old and new usernames are the same";
                if (line != null)
                    error = "Error on line " + lineNumber + " ("+error+"): " + line;
            }
            else if (uniqueNames.contains(oldUsername))
            {
                error = "Old username already specified";
                if (line != null)
                    error = "Error on line " + lineNumber + " ("+error+"): " + line;
            }
            else if (uniqueNames.contains(newUsername))
            {
                error = "New username already specified";
                if (line != null)
                    error = "Error on line " + lineNumber + " ("+error+"): " + line;
            }
            else
            {
                add(new User(oldUsername, newUsername));
            }
            return error;
        }

        private void add(User user)
        {
            usernames.add(user);
            uniqueNames.add(user.getOldUsername());
            uniqueNames.add(user.getNewUsername());
        }
        
        public int userCount()
        {
            return usernames.size();
        }
        
        public Iterator<User> iterator()
        {
            return usernames.iterator();
        }

        /*
         * (non-Javadoc)
         * @see org.alfresco.tools.ToolContext#validate()
         */
        @Override
        /*package*/ void validate()
        {
            super.validate();
            
            if (filename != null)
            {
                if (userCount() > 0)
                {
                    throw new ToolArgumentException("<filename> should not have been specified if " +
                            "<oldUsername> <newUsername> has been specified on the command line.");
                }
                File file = new File(filename);
                if (!file.exists())
                {
                    throw new ToolArgumentException("File " + filename + " does not exist.");
                }
                if (!readFile(file))
                {
                    throw new ToolArgumentException("File " + filename + " contained errors.");
                }
            }
            
            if (userCount() == 0)
            {
                throw new ToolArgumentException("No old and new usernames have been specified.");
            }
        }

        /**
         * Read the user names out of the file.
         * @param file to be read
         * @return {@code true} if there were no problems found with the file contents.
         */
        private boolean readFile(File file)
        {
            BufferedReader in = null;
            boolean noErrors = true;
            try
            {
                in  = new BufferedReader(new InputStreamReader(new FileInputStream(file), encoding.name()));
                int lineNumber = 1;
                for (String line = in.readLine(); line != null; line = in.readLine(), lineNumber++)
                {
                    int i = line.indexOf('#');
                    if (i != -1)
                    {
                        line = line.substring(0, i);
                    }
                    if (line.trim().length() != 0)
                    {
                        String[] names = line.split(",");
                        String oldUsername = names[0].trim();
                        String newUsername = names[1].trim();
                        if (names.length != 2 || oldUsername.length() == 0 || newUsername.length() == 0)
                        {
                            RenameUser.this.logError("Error on line " + lineNumber + ": " + line);
                            noErrors = false;
                        }
                        else
                        {
                            String error = context.add(lineNumber, line, oldUsername, newUsername);
                            if (error != null)
                            {
                                RenameUser.this.logError(error);
                                noErrors = false;
                            }
                        }
                    }
                }
            }
            catch (IOException e)
            {
                throw new ToolArgumentException("Failed to read <filename>.", e);
            }
            finally
            {
                if (in != null)
                {
                    try
                    {
                        in.close();
                    } catch (IOException e)
                    {
                        // ignore
                    }
                }
            }
            return noErrors;
        }
    }

    // BatchProcessWorkProvider returns batches of 100 User objects.
    private class WorkProvider implements BatchProcessWorkProvider<User>
    {
        private static final int BATCH_SIZE = 100;
        
        private final VmShutdownListener vmShutdownLister = new VmShutdownListener("getRenameUserWorkProvider");
        private final Iterator<User> iterator;
        private final int size;
        
        public WorkProvider(RenameUserToolContext context)
        {
            iterator = context.iterator();
            size = context.userCount();
        }

        @Override
        public synchronized int getTotalEstimatedWorkSize()
        {
            return size;
        }

        @Override
        public synchronized Collection<User> getNextWork()
        {
            if (vmShutdownLister.isVmShuttingDown())
            {
                return Collections.emptyList();
            }
            
            Collection<User> results = new ArrayList<User>(BATCH_SIZE);
            while (results.size() < BATCH_SIZE && iterator.hasNext())
            {
                results.add(iterator.next());
            }
            return results;
        }
    }
}
