From 9e7f59238010b9e196d2550cab63f9fedc00aad8 Mon Sep 17 00:00:00 2001
From: brinn <brinn>
Date: Sat, 12 Apr 2008 12:34:05 +0000
Subject: [PATCH] refactor: introduce better class structure for better
 testability add: unit tests for UserEntry and LineBasedUserStore

SVN: 5481
---
 .../file/FileAuthenticationService.java       |  25 +-
 .../file/FileBasedLineStore.java              | 188 +++++++++
 .../cisd/authentication/file/ILineStore.java  |  60 +++
 .../cisd/authentication/file/IUserStore.java  |  70 ++++
 .../file/LineBasedUserStore.java              | 152 +++++++
 .../file/PasswordEditorCommand.java           |   5 +-
 .../authentication/file/PasswordFile.java     | 291 -------------
 .../cisd/authentication/file/UserEntry.java   |   3 +-
 .../file/LineBasedUserStoreTest.java          | 383 ++++++++++++++++++
 .../authentication/file/UserEntryTest.java    | 108 +++++
 10 files changed, 982 insertions(+), 303 deletions(-)
 create mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java
 create mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java
 create mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java
 create mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java
 delete mode 100644 authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordFile.java
 create mode 100644 authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java
 create mode 100644 authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/UserEntryTest.java

diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
index cf0c15355da..28b4791bee5 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
@@ -29,7 +29,7 @@ import ch.systemsx.cisd.common.logging.LogFactory;
 
 /**
  * An implementation of {@link IAuthenticationService} that gets the authentication information from
- * a password file.
+ * a password store (which is usually backed by a file).
  * <p>
  * The file contains:
  * <ul>
@@ -51,21 +51,28 @@ public class FileAuthenticationService implements IAuthenticationService
     private static final Logger operationLog =
             LogFactory.getLogger(LogCategory.OPERATION, FileAuthenticationService.class);
 
-    private final PasswordFile passwordFile;
+    private final IUserStore userStore;
+
+    private static IUserStore createUserStore(final String passwordFileName)
+    {
+        final ILineStore lineStore =
+                new FileBasedLineStore(new File(passwordFileName), "Password file");
+        return new LineBasedUserStore(lineStore);
+    }
 
     public FileAuthenticationService(final String passwordFileName)
     {
-        this.passwordFile = new PasswordFile(new File(passwordFileName));
+        this(createUserStore(passwordFileName));
     }
 
-    public FileAuthenticationService(final File passwordFile)
+    public FileAuthenticationService(IUserStore userStore)
     {
-        this.passwordFile = new PasswordFile(passwordFile);
+        this.userStore = userStore;
     }
 
     private String getToken()
     {
-        return passwordFile.getPath();
+        return userStore.getId();
     }
 
     /**
@@ -84,7 +91,7 @@ public class FileAuthenticationService implements IAuthenticationService
             operationLog.warn(String.format(TOKEN_FAILURE_MSG_TEMPLATE, token, applicationToken));
             return false;
         }
-        return passwordFile.isPasswordCorrect(user, password);
+        return userStore.isPasswordCorrect(user, password);
     }
 
     public Principal getPrincipal(String applicationToken, String user)
@@ -95,12 +102,12 @@ public class FileAuthenticationService implements IAuthenticationService
             operationLog.warn(String.format(TOKEN_FAILURE_MSG_TEMPLATE, token, applicationToken));
             return null;
         }
-        return passwordFile.tryGetUser(user).asPrincial();
+        return userStore.tryGetUser(user).asPrincial();
     }
 
     public void check() throws EnvironmentFailureException, ConfigurationFailureException
     {
-        passwordFile.check();
+        userStore.check();
     }
 
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java
new file mode 100644
index 00000000000..70a5f680ab1
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * 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.
+ */
+
+package ch.systemsx.cisd.authentication.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+
+/**
+ * An implementation of a {@line ILineStore} that is based on a file.
+ * 
+ * @author Bernd Rinn
+ */
+final class FileBasedLineStore implements ILineStore
+{
+
+    private static final Logger machineLog =
+            LogFactory.getLogger(LogCategory.MACHINE, FileBasedLineStore.class);
+
+    private static final Logger operationLog =
+            LogFactory.getLogger(LogCategory.OPERATION, FileBasedLineStore.class);
+
+    private final File file;
+
+    private final File oldFile;
+
+    private final File newFile;
+
+    private final String fileDescription;
+
+    FileBasedLineStore(File file, String fileDescription)
+    {
+        this.file = file;
+        this.oldFile = new File(file.getPath() + ".sv");
+        this.newFile = new File(file.getPath() + ".tmp");
+        this.fileDescription = fileDescription;
+    }
+
+    public void check() throws ConfigurationFailureException
+    {
+        if (file.canRead() == false)
+        {
+            final String msg =
+                    String.format(file.exists() ? "%s '%s' is not readable."
+                            : "%s '%s' does not exist.", fileDescription, file.getAbsolutePath());
+            operationLog.error(msg);
+            throw new ConfigurationFailureException(msg);
+        }
+        try
+        {
+            checkWritable();
+        } catch (EnvironmentFailureException ex)
+        {
+            throw new ConfigurationFailureException(ex.getMessage());
+        }
+    }
+
+    private static void checkWritable(File file, String fileDescription)
+            throws EnvironmentFailureException
+    {
+        if (file.exists() == false)
+        {
+            try
+            {
+                FileUtils.touch(file);
+            } catch (IOException ex)
+            {
+                final String msg =
+                        String.format("%s '%s' is not writable.", fileDescription, file
+                                .getAbsolutePath());
+                operationLog.error(msg);
+                throw new EnvironmentFailureException(msg);
+            }
+        }
+        if (file.canWrite() == false)
+        {
+            final String msg =
+                    String.format("%s '%s' is not writable.", fileDescription, file
+                            .getAbsolutePath());
+            operationLog.error(msg);
+            throw new EnvironmentFailureException(msg);
+        }
+    }
+
+    public boolean exists()
+    {
+        return file.exists();
+    }
+
+    public String getId()
+    {
+        return file.getPath();
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<String> primReadLines(File file) throws IOException
+    {
+        return FileUtils.readLines(file);
+    }
+
+    public List<String> readLines() throws ConfigurationFailureException
+    {
+        if (file.exists() == false)
+        {
+            return new ArrayList<String>();
+        }
+        if (file.canRead() == false)
+        {
+            final String msg =
+                    String.format(file.exists() ? "%s '%s' cannot be read."
+                            : "%s '%s' does not exist.", file.getAbsolutePath());
+            operationLog.error(msg);
+            throw new ConfigurationFailureException(msg);
+        }
+        try
+        {
+            return primReadLines(file);
+        } catch (IOException ex)
+        {
+            final String msg =
+                    String.format("Error when reading file '%s'.", file.getAbsolutePath());
+            machineLog.error(msg, ex);
+            throw new EnvironmentFailureException(msg, ex);
+        }
+    }
+
+    private static void primWriteLines(File file, List<String> lines)
+    {
+        if (file.canWrite() == false)
+        {
+            final String msg =
+                    String.format(file.exists() ? "File '%s' cannot be written."
+                            : "File '%s' does not exist.", file.getAbsolutePath());
+            operationLog.error(msg);
+            throw new ConfigurationFailureException(msg);
+        }
+        try
+        {
+            FileUtils.writeLines(file, lines);
+        } catch (IOException ex)
+        {
+            final String msg =
+                    String.format("Error when writing file '%s'.", file.getAbsolutePath());
+            machineLog.error(msg, ex);
+            throw new EnvironmentFailureException(msg, ex);
+        }
+    }
+
+    private void checkWritable() throws EnvironmentFailureException
+    {
+        checkWritable(file, fileDescription);
+        checkWritable(oldFile, fileDescription);
+        checkWritable(newFile, fileDescription);
+    }
+
+    public void writeLines(List<String> lines)
+    {
+        checkWritable();
+        primWriteLines(newFile, lines);
+        oldFile.delete();
+        file.renameTo(oldFile);
+        newFile.renameTo(file);
+    }
+
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java
new file mode 100644
index 00000000000..e4db6720e4b
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * 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.
+ */
+
+package ch.systemsx.cisd.authentication.file;
+
+import java.util.List;
+
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+
+/**
+ * An abstraction of a file that allows to store and retrieve lines.
+ *
+ * @author Bernd Rinn
+ */
+interface ILineStore
+{
+
+    /**
+     * Returns a unique identifier for this line store.
+     */
+    String getId();
+    
+    /**
+     * Returns <code>true</code>, if this store exists.
+     */
+    boolean exists();
+    
+    /**
+     * Checks whether the store is operational.
+     * <p>
+     * Supposed to be called at program start up.
+     * 
+     * @throws ConfigurationFailureException If this store is not operational.
+     */
+    void check() throws ConfigurationFailureException;
+    
+    /**
+     * Returns the lines currently in this store.
+     */
+    List<String> readLines() throws EnvironmentFailureException;
+    
+    /**
+     * Writes the <var>lines</var> to the store.
+     */
+    void writeLines(List<String> lines) throws EnvironmentFailureException;
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java
new file mode 100644
index 00000000000..f223e76f38e
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * 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.
+ */
+
+package ch.systemsx.cisd.authentication.file;
+
+import java.util.List;
+
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.utilities.ISelfTestable;
+
+/**
+ * An abstraction of a store for {@link UserEntry}s.
+ *
+ * @author Bernd Rinn
+ */
+interface IUserStore extends ISelfTestable
+{
+    /**
+     * Returns the unique identifier of the store.
+     */
+    String getId();
+
+    /**
+     * Returns <code>true</code>, if the password file backing this class exists.
+     */
+    boolean exists();
+    
+    /**
+     * Returns the {@link UserEntry} of <var>user</var>, or <code>null</code>, if this user does
+     * not exist.
+     */
+    UserEntry tryGetUser(String user) throws EnvironmentFailureException;
+    
+    /**
+     * Adds the <var>user</var> if it exists, otherwise updates (replaces) the entry with the given
+     * entry.
+     */
+    void addOrUpdateUser(UserEntry user) throws EnvironmentFailureException;
+
+    /**
+     * Removes the user with id <var>userId</var> if it exists.
+     * 
+     * @return <code>true</code>, if the user has been removed.
+     */
+    boolean removeUser(String userId) throws EnvironmentFailureException;
+    
+    /**
+     * Returns <code>true</code>, if <var>user</var> is known and has the given <var>password</var>.
+     */
+    boolean isPasswordCorrect(String user, String password) throws EnvironmentFailureException;
+    
+    /**
+     * Returns a list of all users currently found in the password file.
+     */
+    List<UserEntry> listUsers() throws EnvironmentFailureException;
+
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java
new file mode 100644
index 00000000000..0dda072f481
--- /dev/null
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * 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.
+ */
+
+package ch.systemsx.cisd.authentication.file;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+
+/**
+ * A class to read and write {@link UserEntry}.
+ * 
+ * @author Bernd Rinn
+ */
+final class LineBasedUserStore implements IUserStore
+{
+
+    private final ILineStore lineStore;
+    
+    LineBasedUserStore(final ILineStore lineStore)
+    {
+        this.lineStore = lineStore;
+    }
+
+    private UserEntry tryFindUserEntry(String user, List<String> passwordLines)
+    {
+        assert user != null;
+        assert passwordLines != null;
+
+        for (String line : passwordLines)
+        {
+            final UserEntry entry = new UserEntry(line);
+            if (user.equals(entry.getUserId()))
+            {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    public String getId()
+    {
+        return lineStore.getId();
+    }
+
+    public boolean exists()
+    {
+        return lineStore.exists();
+    }
+
+    public UserEntry tryGetUser(String user)
+    {
+        return tryFindUserEntry(user, lineStore.readLines());
+    }
+
+    public void addOrUpdateUser(UserEntry user)
+    {
+        assert user != null;
+
+        final List<String> passwordLines = lineStore.readLines();
+        boolean found = false;
+        for (int i = 0; i < passwordLines.size(); ++i)
+        {
+            final String line = passwordLines.get(i);
+            final UserEntry entry = new UserEntry(line);
+            if (entry.getUserId().equals(user.getUserId()))
+            {
+                passwordLines.set(i, user.asPasswordLine());
+                found = true;
+                break;
+            }
+        }
+        if (found == false)
+        {
+            passwordLines.add(user.asPasswordLine());
+        }
+        lineStore.writeLines(passwordLines);
+    }
+
+    public boolean removeUser(String userId)
+    {
+        assert userId != null;
+
+        final List<String> passwordLines = lineStore.readLines();
+        boolean found = false;
+        for (int i = 0; i < passwordLines.size(); ++i)
+        {
+            final String line = passwordLines.get(i);
+            final UserEntry entry = new UserEntry(line);
+            if (userId.equals(entry.getUserId()))
+            {
+                passwordLines.remove(i);
+                found = true;
+                break;
+            }
+        }
+        if (found)
+        {
+            lineStore.writeLines(passwordLines);
+        }
+        return found;
+    }
+
+    public boolean isPasswordCorrect(String user, String password)
+    {
+        assert user != null;
+        assert password != null;
+
+        final UserEntry userEntryOrNull = tryFindUserEntry(user, lineStore.readLines());
+        if (userEntryOrNull == null)
+        {
+            return false;
+        }
+        return userEntryOrNull.isPasswordCorrect(password);
+    }
+
+    public List<UserEntry> listUsers()
+    {
+        final List<UserEntry> list = new ArrayList<UserEntry>();
+        for (String line : lineStore.readLines())
+        {
+            final UserEntry user = new UserEntry(line);
+            list.add(user);
+        }
+        return list;
+    }
+
+    /**
+     * Checks whether this store is operational.
+     * 
+     * @throws ConfigurationFailureException If the store is not operational.
+     */
+    public void check() throws ConfigurationFailureException
+    {
+        lineStore.check();
+    }
+
+}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordEditorCommand.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordEditorCommand.java
index 7437b4d667c..a838c778c46 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordEditorCommand.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordEditorCommand.java
@@ -92,10 +92,11 @@ public class PasswordEditorCommand
     public static void main(String[] args)
     {
         final Parameters params = new Parameters(args);
-        final PasswordFile pwFile = new PasswordFile(getPasswordFile());
+        final ILineStore lineStore = new FileBasedLineStore(getPasswordFile(), "Password file");
+        final LineBasedUserStore pwFile = new LineBasedUserStore(lineStore);
         if (params.getCommand().equals(Parameters.Command.ADD) == false && pwFile.exists() == false)
         {
-            System.err.printf("File '%s' does not exist.\n", pwFile.getPath());
+            System.err.printf("File '%s' does not exist.\n", pwFile.getId());
             System.exit(1);
         }
         switch (params.getCommand())
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordFile.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordFile.java
deleted file mode 100644
index f51a097d34f..00000000000
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordFile.java
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * Copyright 2008 ETH Zuerich, CISD
- *
- * 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.
- */
-
-package ch.systemsx.cisd.authentication.file;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.log4j.Logger;
-
-import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
-import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
-import ch.systemsx.cisd.common.exceptions.UserFailureException;
-import ch.systemsx.cisd.common.logging.LogCategory;
-import ch.systemsx.cisd.common.logging.LogFactory;
-
-/**
- * A class to read and write password files.
- * 
- * @author Bernd Rinn
- */
-final class PasswordFile
-{
-
-    private static final Logger machineLog =
-            LogFactory.getLogger(LogCategory.MACHINE, PasswordFile.class);
-
-    private static final Logger operationLog =
-            LogFactory.getLogger(LogCategory.OPERATION, PasswordFile.class);
-
-    private final File passwordFile;
-
-    PasswordFile(final File passwordFile)
-    {
-        this.passwordFile = passwordFile;
-    }
-
-    @SuppressWarnings("unchecked")
-    private static List<String> primReadLines(File file) throws IOException
-    {
-        return FileUtils.readLines(file);
-    }
-
-    private List<String> readPasswordLines()
-    {
-        if (passwordFile.canRead() == false)
-        {
-            final String msg =
-                    String.format(passwordFile.exists() ? "File '%s' cannot be read."
-                            : "File '%s' does not exist.", passwordFile.getAbsolutePath());
-            operationLog.error(msg);
-            throw new ConfigurationFailureException(msg);
-        }
-        try
-        {
-            return primReadLines(passwordFile);
-        } catch (IOException ex)
-        {
-            final String msg =
-                    String.format("Error when reading file '%s'.", passwordFile.getAbsolutePath());
-            machineLog.error(msg, ex);
-            throw new EnvironmentFailureException(msg, ex);
-        }
-    }
-
-    private static void writePasswordLines(File file, List<String> lines)
-    {
-        if (file.canWrite() == false)
-        {
-            final String msg =
-                    String.format(file.exists() ? "File '%s' cannot be written."
-                            : "File '%s' does not exist.", file.getAbsolutePath());
-            operationLog.error(msg);
-            throw new ConfigurationFailureException(msg);
-        }
-        try
-        {
-            FileUtils.writeLines(file, lines);
-        } catch (IOException ex)
-        {
-            final String msg =
-                    String.format("Error when writing file '%s'.", file.getAbsolutePath());
-            machineLog.error(msg, ex);
-            throw new EnvironmentFailureException(msg, ex);
-        }
-    }
-
-    private UserEntry tryFindUserEntry(String user, List<String> passwordLines)
-    {
-        assert user != null;
-        assert passwordLines != null;
-
-        for (String line : passwordLines)
-        {
-            final UserEntry entry = new UserEntry(line);
-            if (user.equals(entry.getUserId()))
-            {
-                return entry;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Returns the path of the password file.
-     */
-    String getPath()
-    {
-        return passwordFile.getPath();
-    }
-
-    /**
-     * Returns <code>true</code>, if the password file backing this class exists.
-     */
-    boolean exists()
-    {
-        return passwordFile.exists();
-    }
-
-    /**
-     * Returns the {@link UserEntry} of <var>user</var>, or <code>null</code>, if this user does
-     * not exist.
-     */
-    UserEntry tryGetUser(String user)
-    {
-        return tryFindUserEntry(user, readPasswordLines());
-    }
-
-    /**
-     * Adds the <var>user</var> if it exists, otherwise updates (replaces) the entry with the given
-     * entry.
-     */
-    void addOrUpdateUser(UserEntry user)
-    {
-        assert user != null;
-
-        final File oldPasswordFile = new File(passwordFile.getPath() + ".sv");
-        final File newPasswordFile = new File(passwordFile.getPath() + ".tmp");
-
-        checkWritable(passwordFile);
-        checkWritable(oldPasswordFile);
-        checkWritable(newPasswordFile);
-
-        final List<String> passwordLines =
-                passwordFile.exists() ? readPasswordLines() : new ArrayList<String>();
-        boolean found = false;
-        for (int i = 0; i < passwordLines.size(); ++i)
-        {
-            final String line = passwordLines.get(i);
-            final UserEntry entry = new UserEntry(line);
-            if (entry.getUserId().equals(user.getUserId()))
-            {
-                passwordLines.set(i, user.asPasswordLine());
-                found = true;
-                break;
-            }
-        }
-        if (found == false)
-        {
-            passwordLines.add(user.asPasswordLine());
-        }
-        writePasswordLines(newPasswordFile, passwordLines);
-        oldPasswordFile.delete();
-        passwordFile.renameTo(oldPasswordFile);
-        newPasswordFile.renameTo(passwordFile);
-    }
-
-    /**
-     * Removes the user with id <var>userId</var> if it exists.
-     * 
-     * @return <code>true</code>, if the user has been removed.
-     */
-    boolean removeUser(String userId)
-    {
-        assert userId != null;
-
-        final File oldPasswordFile = new File(passwordFile.getPath() + ".sv");
-        final File newPasswordFile = new File(passwordFile.getPath() + ".tmp");
-
-        checkWritable(passwordFile);
-        checkWritable(oldPasswordFile);
-        checkWritable(newPasswordFile);
-
-        final List<String> passwordLines = readPasswordLines();
-        boolean found = false;
-        for (int i = 0; i < passwordLines.size(); ++i)
-        {
-            final String line = passwordLines.get(i);
-            final UserEntry entry = new UserEntry(line);
-            if (userId.equals(entry.getUserId()))
-            {
-                passwordLines.remove(i);
-                found = true;
-                break;
-            }
-        }
-        if (found)
-        {
-            writePasswordLines(newPasswordFile, passwordLines);
-            oldPasswordFile.delete();
-            passwordFile.renameTo(oldPasswordFile);
-            newPasswordFile.renameTo(passwordFile);
-        }
-        return found;
-    }
-
-    private void checkWritable(File file) throws UserFailureException
-    {
-        if (file.exists() == false)
-        {
-            try
-            {
-                FileUtils.touch(file);
-            } catch (IOException ex)
-            {
-                final String msg =
-                        String
-                                .format("Password file '%s' is not writable.", file
-                                        .getAbsolutePath());
-                operationLog.error(msg);
-                throw new UserFailureException(msg);
-            }
-        }
-        if (file.canWrite() == false)
-        {
-            final String msg =
-                    String.format("Password file '%s' is not writable.", file.getAbsolutePath());
-            operationLog.error(msg);
-            throw new UserFailureException(msg);
-        }
-    }
-
-    /**
-     * Returns <code>true</code>, if <var>user</var> is known and has the given <var>password</var>.
-     */
-    boolean isPasswordCorrect(String user, String password)
-    {
-        assert user != null;
-        assert password != null;
-
-        final UserEntry userEntryOrNull = tryFindUserEntry(user, readPasswordLines());
-        if (userEntryOrNull == null)
-        {
-            return false;
-        }
-        return userEntryOrNull.isPasswordCorrect(password);
-    }
-
-    /**
-     * Returns a list of all users currently found in the password file.
-     */
-    List<UserEntry> listUsers()
-    {
-        final List<UserEntry> list = new ArrayList<UserEntry>();
-        for (String line : readPasswordLines())
-        {
-            final UserEntry user = new UserEntry(line);
-            list.add(user);
-        }
-        return list;
-    }
-
-    void check() throws EnvironmentFailureException, ConfigurationFailureException
-    {
-        if (passwordFile.canRead() == false)
-        {
-            final String msg =
-                    String.format(passwordFile.exists() ? "Password file '%s' is not readable."
-                            : "Password file '%s' does not exist.", passwordFile.getAbsolutePath());
-            operationLog.error(msg);
-            throw new ConfigurationFailureException(msg);
-        }
-    }
-
-}
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/UserEntry.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/UserEntry.java
index eed778c9f7a..57afdd103f0 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/UserEntry.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/UserEntry.java
@@ -19,6 +19,7 @@ package ch.systemsx.cisd.authentication.file;
 import org.apache.commons.lang.StringUtils;
 
 import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.utilities.AbstractHashable;
 import ch.systemsx.cisd.common.utilities.PasswordHasher;
 
 /**
@@ -26,7 +27,7 @@ import ch.systemsx.cisd.common.utilities.PasswordHasher;
  * 
  * @author Bernd Rinn
  */
-class UserEntry
+class UserEntry extends AbstractHashable
 {
     private static final int NUMBER_OF_COLUMNS_IN_PASSWORD_FILE = 5;
 
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java
new file mode 100644
index 00000000000..ca83a31e381
--- /dev/null
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * 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.
+ */
+
+package ch.systemsx.cisd.authentication.file;
+
+import static org.testng.AssertJUnit.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+
+/**
+ * Test cases for the {@link LineBasedUserStore}.
+ * 
+ * @author Bernd Rinn
+ */
+public class LineBasedUserStoreTest
+{
+    private Mockery context;
+
+    private ILineStore lineStore;
+
+    private LineBasedUserStore userStore;
+
+    @BeforeMethod
+    public final void setUp()
+    {
+        context = new Mockery();
+        lineStore = context.mock(ILineStore.class);
+        userStore = new LineBasedUserStore(lineStore);
+    }
+
+    @AfterMethod
+    public void tearDown()
+    {
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testGetId()
+    {
+        final String id = "Some Identifier";
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).getId();
+                    will(returnValue(id));
+                }
+            });
+        assertEquals(id, userStore.getId());
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testStoreExists()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).exists();
+                    will(returnValue(true));
+                }
+            });
+        assertTrue(userStore.exists());
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testStoreDoesntExist()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).exists();
+                    will(returnValue(false));
+                }
+            });
+        assertFalse(userStore.exists());
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testCheck()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).check();
+                }
+            });
+        userStore.check();
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testCheckFailed()
+    {
+        final String errorMessage = "My Message";
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).check();
+                    will(throwException(new ConfigurationFailureException(errorMessage)));
+                }
+            });
+        try
+        {
+            userStore.check();
+            fail("Failed to signal error condition");
+        } catch (ConfigurationFailureException ex)
+        {
+            assertEquals(errorMessage, ex.getMessage());
+        }
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testListUsers()
+    {
+        final UserEntry u1 = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry("uid2", "email2", "first2", "last2", "pwd2");
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3.asPasswordLine());
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                }
+            });
+        final List<UserEntry> list = userStore.listUsers();
+        assertEquals(lines.size(), list.size());
+        assertEquals(u1, list.get(0));
+        assertEquals(u2, list.get(1));
+        assertEquals(u3, list.get(2));
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testTryGetUserFailedEmptyStore()
+    {
+        final List<String> lines = Collections.emptyList();
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                }
+            });
+        assertNull(userStore.tryGetUser("uid"));
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testTryGetUserFailed()
+    {
+        final UserEntry u1 = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry("uid2", "email2", "first2", "last2", "pwd2");
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3.asPasswordLine());
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                }
+            });
+        assertNull(userStore.tryGetUser("non-existent"));
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testTryGetUserSuccess()
+    {
+        final UserEntry u1 = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry("uid2", "email2", "first2", "last2", "pwd2");
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3.asPasswordLine());
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                }
+            });
+        assertEquals(u1, userStore.tryGetUser("uid1"));
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testIsPasswordCorrectSuccess()
+    {
+        final String uid2 = "uid2";
+        final String pwd2 = "pwd2";
+        final UserEntry u1 = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry(uid2, "email2", "first2", "last2", pwd2);
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3.asPasswordLine());
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                }
+            });
+        assertTrue(userStore.isPasswordCorrect(uid2, pwd2));
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testIsPasswordCorrectFailure()
+    {
+        final String uid2 = "uid2";
+        final String pwd2 = "pwd2";
+        final UserEntry u1 = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry(uid2, "email2", "first2", "last2", pwd2);
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3.asPasswordLine());
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                }
+            });
+        assertFalse(userStore.isPasswordCorrect(uid2, pwd2.toUpperCase()));
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testAddFirstUser()
+    {
+        final UserEntry user = new UserEntry("uid", "email", "first", "last", "pwd");
+        final String userLine = user.asPasswordLine();
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(new ArrayList<String>()));
+                    one(lineStore).writeLines(Collections.singletonList(userLine));
+                }
+            });
+        userStore.addOrUpdateUser(user);
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testAddSecondUser()
+    {
+        final UserEntry oldUser = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry newUser = new UserEntry("uid2", "email2", "first2", "last2", "pwd2");
+        final String oldUserLine = oldUser.asPasswordLine();
+        final String newUserLine = newUser.asPasswordLine();
+        context.checking(new Expectations()
+            {
+                {
+                    final List<String> lines = new ArrayList<String>();
+                    lines.add(oldUserLine);
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                    one(lineStore).writeLines(Arrays.asList(oldUserLine, newUserLine));
+                }
+            });
+        userStore.addOrUpdateUser(newUser);
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testUpdateUser()
+    {
+        final UserEntry u1 = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry("uid2", "email2", "first2", "last2", "pwd2");
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3.asPasswordLine());
+
+        final UserEntry u3Updated = new UserEntry("uid3", "email3U", "first3U", "last3U", "pwd3U");
+        final List<String> linesUpdated =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3Updated.asPasswordLine());
+
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                    one(lineStore).writeLines(linesUpdated);
+                }
+            });
+        userStore.addOrUpdateUser(u3Updated);
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testRemoveUser()
+    {
+        final String uid1 = "uid1";
+        final UserEntry u1 = new UserEntry(uid1, "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry("uid2", "email2", "first2", "last2", "pwd2");
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                new ArrayList<String>(Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3
+                        .asPasswordLine()));
+
+        final List<String> linesUpdated = Arrays.asList(u2.asPasswordLine(), u3.asPasswordLine());
+
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                    one(lineStore).writeLines(linesUpdated);
+                }
+            });
+        userStore.removeUser(uid1);
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testRemoveNonExistingUser()
+    {
+        final UserEntry u1 = new UserEntry("uid1", "email1", "first1", "last1", "pwd1");
+        final UserEntry u2 = new UserEntry("uid2", "email2", "first2", "last2", "pwd2");
+        final UserEntry u3 = new UserEntry("uid3", "email3", "first3", "last3", "pwd3");
+        final List<String> lines =
+                Arrays.asList(u1.asPasswordLine(), u2.asPasswordLine(), u3.asPasswordLine());
+
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                }
+            });
+        userStore.removeUser("non-existing");
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testRemoveLastUser()
+    {
+        final String uid1 = "uid1";
+        final UserEntry u1 = new UserEntry(uid1, "email1", "first1", "last1", "pwd1");
+        final List<String> lines = new ArrayList<String>(Arrays.asList(u1.asPasswordLine()));
+
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).readLines();
+                    will(returnValue(lines));
+                    one(lineStore).writeLines(Collections.<String> emptyList());
+                }
+            });
+        userStore.removeUser(uid1);
+        context.assertIsSatisfied();
+    }
+}
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/UserEntryTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/UserEntryTest.java
new file mode 100644
index 00000000000..d7e4571b920
--- /dev/null
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/UserEntryTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2008 ETH Zuerich, CISD
+ *
+ * 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.
+ */
+
+package ch.systemsx.cisd.authentication.file;
+
+import org.testng.annotations.Test;
+
+import ch.systemsx.cisd.authentication.Principal;
+import ch.systemsx.cisd.common.utilities.PasswordHasher;
+
+import static org.testng.AssertJUnit.*;
+
+/**
+ * Test cases for {@link UserEntry}.
+ *
+ * @author Bernd Rinn
+ */
+public class UserEntryTest
+{
+    private static final String PASSWORD = "passw0rd";
+    
+    private static final String PASSWORD_HASH = PasswordHasher.computeSaltedHash(PASSWORD);
+    
+    private static final String PASSWORD_LINE = "id:email@dot.org:First:Last:" + PASSWORD_HASH;
+
+    @Test
+    public void testRoundtrip()
+    {
+        final String line = PASSWORD_LINE;
+        final UserEntry entry = new UserEntry(line);
+        assertEquals(line, entry.asPasswordLine());
+    }
+
+    @Test
+    public void testGetters()
+    {
+        final UserEntry entry = new UserEntry(PASSWORD_LINE);
+        assertEquals("id", entry.getUserId());
+        assertEquals("First", entry.getFirstName());
+        assertEquals("Last", entry.getLastName());
+        assertEquals("email@dot.org", entry.getEmail());
+        assertTrue(entry.isPasswordCorrect(PASSWORD));
+        assertFalse(entry.isPasswordCorrect(PASSWORD.replace('0', 'o')));
+    }
+    
+    @Test
+    public void testConstructor()
+    {
+        final String id = "ID";
+        final String firstName = "first";
+        final String lastName = "laST";
+        final String email = "a@b.edu";
+        final UserEntry entry = new UserEntry(id, email, firstName, lastName, PASSWORD);
+        assertEquals(id, entry.getUserId());
+        assertEquals(firstName, entry.getFirstName());
+        assertEquals(lastName, entry.getLastName());
+        assertEquals(email, entry.getEmail());
+        assertTrue(entry.isPasswordCorrect(PASSWORD));
+        assertFalse(entry.isPasswordCorrect(PASSWORD.replace('0', 'o')));
+    }
+
+    @Test
+    public void testSetters()
+    {
+        final String id = "id";
+        final String firstName = "first1";
+        final String lastName = "laST2";
+        final String email = "a@b.edu";
+        final UserEntry entry = new UserEntry(PASSWORD_LINE);
+        assertEquals(id, entry.getUserId());
+        entry.setFirstName(firstName);
+        assertEquals(firstName, entry.getFirstName());
+        entry.setLastName(lastName);
+        assertEquals(lastName, entry.getLastName());
+        entry.setEmail(email);
+        assertEquals(email, entry.getEmail());
+        entry.setPassword(PASSWORD.replace('0', 'o'));
+        assertFalse(entry.isPasswordCorrect(PASSWORD));
+        assertTrue(entry.isPasswordCorrect(PASSWORD.replace('0', 'o')));
+        final String line = String.format("%s:%s:%s:%s:", id, email, firstName, lastName, email);
+        assertTrue(entry.asPasswordLine().startsWith(line));
+    }
+    
+    @Test
+    public void testAsPrincial()
+    {
+        final UserEntry entry = new UserEntry(PASSWORD_LINE);
+        final Principal principal = entry.asPrincial();
+        assertEquals("id", principal.getUserId());
+        assertEquals("First", principal.getFirstName());
+        assertEquals("Last", principal.getLastName());
+        assertEquals("email@dot.org", principal.getEmail());
+    }
+
+}
-- 
GitLab