()
+ {
+ public Void execute() throws Throwable
+ {
+ String newUserName = (String) nodeService.getProperty(person, ContentModel.PROP_USERNAME);
+ assertEquals(newUsername, newUserName);
+
+ // Check the person exists
+ assertEquals(newUsername, nodeService.getProperty(person, ContentModel.PROP_USERNAME));
+ assertEquals(person, personService.getPerson(newUsername));
+ assertFalse("old user should no longer exist", personService.personExists(oldUsername));
+
+ // Get rid of the test person
+ personService.deletePerson(newUsername);
+ return null;
+ }
+ }, false, true);
+ }
}
diff --git a/source/java/org/alfresco/tools/RenameUser.java b/source/java/org/alfresco/tools/RenameUser.java
new file mode 100644
index 0000000000..66bd017c95
--- /dev/null
+++ b/source/java/org/alfresco/tools/RenameUser.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+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.
+ *
+ * Usage: renameUser -user username [options] oldUsername newUsername");
+ * renameUser -user username [options] -file filename");
+ *
+ * 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:
+ *
+ * # List of usernames to change
+ *
+ * # oldUsername,newUsername
+ * johnp,ceo # President and CEO
+ * johnn,cto # CTO and Chairman
+ *
+ *
+ * @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 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 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 for the option -encoding must be specified");
+ }
+ try
+ {
+ context.encoding = Charset.forName(args[i]);
+ }
+ catch (IllegalCharsetNameException e)
+ {
+ throw new ToolArgumentException("The value is not recognised");
+ }
+ catch (UnsupportedCharsetException e)
+ {
+ throw new ToolArgumentException("The value 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 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 must be specified");
+ }
+ if (context.userCount() > 0)
+ {
+ throw new ToolArgumentException("Only one 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 worker = new BatchProcessWorkerAdaptor()
+ {
+ public void process(final User user) throws Throwable
+ {
+ RunAsWork runAsWork = new RunAsWork()
+ {
+ @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 processor = new BatchProcessor(
+ "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 usernames = new ArrayList();
+
+ // Internal - used check the name has not been used before.
+ private Set uniqueNames = new HashSet();
+
+ /**
+ * 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 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(" should not have been specified if " +
+ " 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 .", 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
+ {
+ private static final int BATCH_SIZE = 100;
+
+ private final VmShutdownListener vmShutdownLister = new VmShutdownListener("getRenameUserWorkProvider");
+ private final Iterator 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 getNextWork()
+ {
+ if (vmShutdownLister.isVmShuttingDown())
+ {
+ return Collections.emptyList();
+ }
+
+ Collection results = new ArrayList(BATCH_SIZE);
+ while (results.size() < BATCH_SIZE && iterator.hasNext())
+ {
+ results.add(iterator.next());
+ }
+ return results;
+ }
+ }
+}
diff --git a/source/java/org/alfresco/tools/RenameUserTest.java b/source/java/org/alfresco/tools/RenameUserTest.java
new file mode 100644
index 0000000000..2b333e45ee
--- /dev/null
+++ b/source/java/org/alfresco/tools/RenameUserTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * 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 .
+ */
+package org.alfresco.tools;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.util.Iterator;
+
+import org.alfresco.tools.RenameUser.User;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit test for RenameUser. {@link PersonTest} contains integration tests.
+ *
+ * @author Alan Davis
+ */
+public class RenameUserTest
+{
+ private String[] args;
+ private File file;
+ private RenameUser renameUser = new RenameUser();
+
+ @Before
+ public void setUp() throws Exception
+ {
+ args = new String[6];
+ args[0] = "-user";
+ args[1] = "admin";
+ args[2] = "-pwd";
+ args[3] = "admin";
+ args[4] = "oldUsername";
+ args[5] = "newUsername";
+ }
+
+ @After
+ public void tearDown()// throws Exception
+ {
+ if (file != null && file.exists())
+ {
+ file.delete();
+ }
+ }
+
+ private void createFile(String content) throws Exception
+ {
+ file = File.createTempFile("RenameUserTest", ".txt");
+ args[4] = "-file";
+ args[5] = file.getPath();
+
+ BufferedWriter out = new BufferedWriter(new FileWriter(file));
+ out.write(content);
+ out.close();
+ }
+
+ private void processArgsAndValidate()
+ {
+ renameUser.processArgs(args);
+ renameUser.context.validate();
+ }
+
+ // Check that the expected (supplied) usernames are in the context
+ private void assertUsers(String... usernames)
+ {
+ int length = usernames.length/2;
+ assertEquals("Must have an even number of usernames passed to assertUsers", usernames.length, length*2);
+
+ assertEquals(length, renameUser.context.userCount());
+ Iterator iterator = renameUser.context.iterator();
+ for (int i=0; i