From d7b9872847e2eb524335ef0ef9a0d24ebdeeca64 Mon Sep 17 00:00:00 2001
From: brinn <brinn>
Date: Sat, 19 Jan 2013 16:53:22 +0000
Subject: [PATCH] Add memory caching to FileAuthenticationService; add
 authentication by email to FileAuthenticationService.

SVN: 28132
---
 .../DummyAuthenticationService.java           |   6 +
 .../IAuthenticationService.java               |   6 +
 .../NullAuthenticationService.java            |   6 +
 .../crowd/CrowdAuthenticationService.java     |   6 +
 .../file/FileAuthenticationService.java       |  68 +++++----
 .../file/FileBasedLineStore.java              |  13 +-
 .../cisd/authentication/file/ILineStore.java  |   6 +
 .../cisd/authentication/file/IUserStore.java  |  10 +-
 .../file/LineBasedUserStore.java              | 136 +++++++++++-------
 .../file/PasswordEditorCommand.java           |   8 +-
 .../ldap/LDAPAuthenticationService.java       |   6 +
 .../stacked/StackedAuthenticationService.java |  11 ++
 .../file/FileAuthenticationServiceTest.java   |   4 +-
 .../file/FileBasedLineStoreTest.java          |   5 +
 .../file/LineBasedUserStoreTest.java          | 116 ++++++++++++++-
 15 files changed, 313 insertions(+), 94 deletions(-)

diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java
index 7c79d8f6885..91cabbd442a 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/DummyAuthenticationService.java
@@ -174,6 +174,12 @@ public final class DummyAuthenticationService implements IAuthenticationService
         return false;
     }
 
+    @Override
+    public boolean supportsAuthenticatingByEmail()
+    {
+        return true;
+    }
+
     @Override
     public final void check()
     {
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java
index c57f12e5fb8..f5d376ab488 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/IAuthenticationService.java
@@ -57,6 +57,12 @@ public interface IAuthenticationService extends ISelfTestable
      */
     public Principal getPrincipal(String user) throws IllegalArgumentException;
 
+    /**
+     * Returns <code>true</code> if this authentication service supports authenticating users by
+     * their email address.
+     */
+    public boolean supportsAuthenticatingByEmail();
+
     /**
      * Returns <code>true</code> if this authentication service supports listing of principals by
      * user id.
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java
index dbc53147503..431ad693858 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/NullAuthenticationService.java
@@ -107,6 +107,12 @@ public class NullAuthenticationService implements IAuthenticationService
         return false;
     }
 
+    @Override
+    public boolean supportsAuthenticatingByEmail()
+    {
+        return true;
+    }
+
     @Override
     public boolean authenticateUser(String user, String password)
     {
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java
index a7870206c8f..fad6abe0552 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/crowd/CrowdAuthenticationService.java
@@ -637,4 +637,10 @@ public class CrowdAuthenticationService implements IAuthenticationService
         return false;
     }
 
+    @Override
+    public boolean supportsAuthenticatingByEmail()
+    {
+        return false;
+    }
+
 }
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 14afe485e08..08951e79806 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileAuthenticationService.java
@@ -79,9 +79,9 @@ public class FileAuthenticationService implements IAuthenticationService
     }
 
     @Override
-    public boolean authenticateUser(String user, String password)
+    public boolean authenticateUser(String userId, String password)
     {
-        return userStore.isPasswordCorrect(user, password);
+        return userStore.isPasswordCorrect(userId, password);
     }
 
     @Override
@@ -90,54 +90,58 @@ public class FileAuthenticationService implements IAuthenticationService
     {
         return tryGetAndAuthenticateUser(user, passwordOrNull);
     }
-    
+
     @Override
     public Principal tryGetAndAuthenticateUser(String user,
             String passwordOrNull)
     {
-        final UserEntry userOrNull = userStore.tryGetUser(user);
-        if (userOrNull != null)
-        {
-            final Principal principal = userOrNull.asPrincipal();
-            if (passwordOrNull != null)
-            {
-                principal
-                        .setAuthenticated(authenticateUser(user, passwordOrNull));
-            }
-            return principal;
-        } else
-        {
-            return null;
-        }
+        return tryAuthenticateUser(userStore.tryGetUserById(user), passwordOrNull);
     }
 
     @Override
-    public Principal getPrincipal(String applicationToken, String user)
+    public Principal tryGetAndAuthenticateUserByEmail(String applicationToken, String email,
+            String passwordOrNull)
     {
-        return getPrincipal(user);
+        return tryGetAndAuthenticateUserByEmail(email, passwordOrNull);
     }
-    
+
     @Override
-    public Principal getPrincipal(String user)
+    public Principal tryGetAndAuthenticateUserByEmail(String email, String passwordOrNull)
     {
-        final Principal principalOrNull = tryGetAndAuthenticateUser(user, null);
-        if (principalOrNull == null)
+        return tryAuthenticateUser(userStore.tryGetUserByEmail(email), passwordOrNull);
+    }
+
+    private Principal tryAuthenticateUser(final UserEntry userOrNull,
+            String passwordOrNull)
+    {
+        if (userOrNull == null)
         {
-            throw new IllegalArgumentException("Cannot find user '" + user + "'.");
+            return null;
         }
-        return principalOrNull;
+        final Principal principal = userOrNull.asPrincipal();
+        if (passwordOrNull != null)
+        {
+            principal
+                    .setAuthenticated(authenticateUser(principal.getUserId(), passwordOrNull));
+        }
+        return principal;
     }
 
     @Override
-    public Principal tryGetAndAuthenticateUserByEmail(String applicationToken, String email, String passwordOrNull)
+    public Principal getPrincipal(String applicationToken, String userId)
     {
-        throw new UnsupportedOperationException();
+        return getPrincipal(userId);
     }
 
     @Override
-    public Principal tryGetAndAuthenticateUserByEmail(String email, String passwordOrNull)
+    public Principal getPrincipal(String userId)
     {
-        throw new UnsupportedOperationException();
+        final Principal principalOrNull = tryGetAndAuthenticateUser(userId, null);
+        if (principalOrNull == null)
+        {
+            throw new IllegalArgumentException("Cannot find user '" + userId + "'.");
+        }
+        return principalOrNull;
     }
 
     @Override
@@ -194,6 +198,12 @@ public class FileAuthenticationService implements IAuthenticationService
         return false;
     }
 
+    @Override
+    public boolean supportsAuthenticatingByEmail()
+    {
+        return true;
+    }
+
     @Override
     public void check() throws EnvironmentFailureException, ConfigurationFailureException
     {
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java
index 08db566b516..e626810b6e7 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/FileBasedLineStore.java
@@ -50,6 +50,8 @@ final class FileBasedLineStore implements ILineStore
     private final File newFile;
 
     private final String fileDescription;
+    
+    private long lastReadTimestamp;
 
     FileBasedLineStore(File file, String fileDescription)
     {
@@ -135,7 +137,9 @@ final class FileBasedLineStore implements ILineStore
         }
         try
         {
-            return primReadLines(file);
+            final List<String> lines = primReadLines(file);
+            lastReadTimestamp = file.lastModified();
+            return lines;
         } catch (IOException ex)
         {
             final String msg =
@@ -182,6 +186,13 @@ final class FileBasedLineStore implements ILineStore
         oldFile.delete();
         file.renameTo(oldFile);
         newFile.renameTo(file);
+        lastReadTimestamp = file.lastModified();
+    }
+
+    @Override
+    public boolean hasChanged()
+    {
+        return file.lastModified() != lastReadTimestamp;
     }
 
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java
index de6f1494861..30076e20d42 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/ILineStore.java
@@ -53,4 +53,10 @@ interface ILineStore
      * Writes the <var>lines</var> to the store.
      */
     void writeLines(List<String> lines) throws EnvironmentFailureException;
+
+    /**
+     * Returns <code>true</code> if the lines store has changed on disk since the last time it was
+     * read or written.
+     */
+    boolean hasChanged();
 }
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java
index 3bc5fb5b7d5..df400c927b2 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/IUserStore.java
@@ -34,10 +34,16 @@ interface IUserStore extends ISelfTestable
     String getId();
 
     /**
-     * Returns the {@link UserEntry} of <var>user</var>, or <code>null</code>, if this user does
+     * Returns the {@link UserEntry} of <var>userId</var>, or <code>null</code>, if this user does
      * not exist.
      */
-    UserEntry tryGetUser(String user) throws EnvironmentFailureException;
+    UserEntry tryGetUserById(String userId) throws EnvironmentFailureException;
+    
+    /**
+     * Returns the {@link UserEntry} of <var>email</var>, or <code>null</code>, if this user does
+     * not exist.
+     */
+    UserEntry tryGetUserByEmail(String email) throws EnvironmentFailureException;
     
     /**
      * Adds the <var>user</var> if it exists, otherwise updates (replaces) the entry with the given
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java b/authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java
index 505f86c7c82..270f2d92cef 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/LineBasedUserStore.java
@@ -17,9 +17,14 @@
 package ch.systemsx.cisd.authentication.file;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
+import ch.systemsx.cisd.common.exceptions.EnvironmentFailureException;
+import ch.systemsx.cisd.common.shared.basic.string.StringUtils;
 
 /**
  * A class to read and write {@link UserEntry}.
@@ -28,28 +33,69 @@ import ch.systemsx.cisd.common.exceptions.ConfigurationFailureException;
  */
 final class LineBasedUserStore implements IUserStore
 {
-
     private final ILineStore lineStore;
-    
+
+    private Map<String, UserEntry> idToEntryMap;
+
+    private Map<String, UserEntry> emailToEntryMap;
+
     LineBasedUserStore(final ILineStore lineStore)
     {
         this.lineStore = lineStore;
+        this.idToEntryMap = new LinkedHashMap<String, UserEntry>();
+        this.emailToEntryMap = new LinkedHashMap<String, UserEntry>();
     }
 
-    private UserEntry tryFindUserEntry(String user, List<String> passwordLines)
+    private synchronized Map<String, UserEntry> getIdToEntryMap()
     {
-        assert user != null;
-        assert passwordLines != null;
+        return idToEntryMap;
+    }
 
-        for (String line : passwordLines)
+    private synchronized Map<String, UserEntry> getEmailToEntryMap()
+    {
+        return emailToEntryMap;
+    }
+
+    synchronized void setEntryMaps(Map<String, UserEntry> idToEntryMap,
+            Map<String, UserEntry> emailToEntryMap)
+    {
+        this.idToEntryMap = idToEntryMap;
+        this.emailToEntryMap = emailToEntryMap;
+    }
+
+    private void updateMaps()
+    {
+        if (lineStore.hasChanged())
         {
-            final UserEntry entry = new UserEntry(line);
-            if (user.equals(entry.getUserId()))
+            final Map<String, UserEntry> newIdToEntryMap = new LinkedHashMap<String, UserEntry>();
+            final Map<String, UserEntry> newEmailToEntryMap =
+                    new LinkedHashMap<String, UserEntry>();
+            for (String line : lineStore.readLines())
             {
-                return entry;
+                final UserEntry entry = new UserEntry(line);
+                newIdToEntryMap.put(entry.getUserId(), entry);
+                if (StringUtils.isNotBlank(entry.getEmail()))
+                {
+                    if (newEmailToEntryMap.put(entry.getEmail().toLowerCase(), entry) != null)
+                    {
+                        // Multiple users with the same email
+                        emailToEntryMap.remove(entry.getEmail().toLowerCase());
+                    }
+                }
             }
+            setEntryMaps(newIdToEntryMap, newEmailToEntryMap);
         }
-        return null;
+    }
+
+    private List<String> asPasswordLines()
+    {
+        final Collection<UserEntry> users = getIdToEntryMap().values();
+        final List<String> lines = new ArrayList<String>(users.size());
+        for (UserEntry user : users)
+        {
+            lines.add(user.asPasswordLine());
+        }
+        return lines;
     }
 
     @Override
@@ -59,59 +105,54 @@ final class LineBasedUserStore implements IUserStore
     }
 
     @Override
-    public UserEntry tryGetUser(String user)
+    public UserEntry tryGetUserById(String user)
+    {
+        updateMaps();
+        return getIdToEntryMap().get(user);
+    }
+
+    @Override
+    public UserEntry tryGetUserByEmail(String email) throws EnvironmentFailureException
     {
-        return tryFindUserEntry(user, lineStore.readLines());
+        updateMaps();
+        return getEmailToEntryMap().get(email.toLowerCase());
     }
 
     @Override
-    public void addOrUpdateUser(UserEntry user)
+    public synchronized void addOrUpdateUser(UserEntry user)
     {
         assert user != null;
 
-        final List<String> passwordLines = lineStore.readLines();
-        boolean found = false;
-        for (int i = 0; i < passwordLines.size(); ++i)
+        updateMaps();
+        idToEntryMap.put(user.getUserId(), user);
+        if (StringUtils.isNotBlank(user.getEmail()))
         {
-            final String line = passwordLines.get(i);
-            final UserEntry entry = new UserEntry(line);
-            if (entry.getUserId().equals(user.getUserId()))
+            if (emailToEntryMap.put(user.getEmail().toLowerCase(), user) != null)
             {
-                passwordLines.set(i, user.asPasswordLine());
-                found = true;
-                break;
+                // Multiple users with the same email
+                emailToEntryMap.remove(user.getEmail().toLowerCase());
             }
         }
-        if (found == false)
-        {
-            passwordLines.add(user.asPasswordLine());
-        }
-        lineStore.writeLines(passwordLines);
+        lineStore.writeLines(asPasswordLines());
     }
 
     @Override
-    public boolean removeUser(String userId)
+    public synchronized boolean removeUser(String userId)
     {
         assert userId != null;
 
-        final List<String> passwordLines = lineStore.readLines();
-        boolean found = false;
-        for (int i = 0; i < passwordLines.size(); ++i)
+        updateMaps();
+        final UserEntry oldEntryOrNull = idToEntryMap.remove(userId);
+        if (oldEntryOrNull != null)
         {
-            final String line = passwordLines.get(i);
-            final UserEntry entry = new UserEntry(line);
-            if (userId.equals(entry.getUserId()))
+            if (StringUtils.isNotBlank(oldEntryOrNull.getEmail()))
             {
-                passwordLines.remove(i);
-                found = true;
-                break;
+                emailToEntryMap.remove(oldEntryOrNull.getEmail().toLowerCase());
             }
+            lineStore.writeLines(asPasswordLines());
+            return true;
         }
-        if (found)
-        {
-            lineStore.writeLines(passwordLines);
-        }
-        return found;
+        return false;
     }
 
     @Override
@@ -120,7 +161,7 @@ final class LineBasedUserStore implements IUserStore
         assert user != null;
         assert password != null;
 
-        final UserEntry userEntryOrNull = tryFindUserEntry(user, lineStore.readLines());
+        final UserEntry userEntryOrNull = tryGetUserById(user);
         if (userEntryOrNull == null)
         {
             return false;
@@ -131,13 +172,8 @@ final class LineBasedUserStore implements IUserStore
     @Override
     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;
+        updateMaps();
+        return new ArrayList<UserEntry>(getIdToEntryMap().values());
     }
 
     /**
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 71f0fe5a659..7d8f7082b19 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordEditorCommand.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/file/PasswordEditorCommand.java
@@ -106,7 +106,7 @@ public class PasswordEditorCommand
                 case ADD:
                 {
                     final String userId = params.getUserId();
-                    final UserEntry userOrNull = userStore.tryGetUser(userId);
+                    final UserEntry userOrNull = userStore.tryGetUserById(userId);
                     if (userOrNull != null)
                     {
                         System.err.printf("User '%s' already exists.\n", userId);
@@ -129,7 +129,7 @@ public class PasswordEditorCommand
                 case CHANGE:
                 {
                     final String userId = params.getUserId();
-                    final UserEntry userOrNull = userStore.tryGetUser(userId);
+                    final UserEntry userOrNull = userStore.tryGetUserById(userId);
                     if (userOrNull == null)
                     {
                         System.err.printf("User '%s' does not exist.\n", userId);
@@ -180,7 +180,7 @@ public class PasswordEditorCommand
                 case SHOW:
                 {
                     final String userId = params.getUserId();
-                    final UserEntry userOrNull = userStore.tryGetUser(userId);
+                    final UserEntry userOrNull = userStore.tryGetUserById(userId);
                     if (userOrNull == null)
                     {
                         System.err.printf("User '%s' does not exist.\n", userId);
@@ -194,7 +194,7 @@ public class PasswordEditorCommand
                 case TEST:
                 {
                     final String userId = params.getUserId();
-                    final UserEntry userOrNull = userStore.tryGetUser(userId);
+                    final UserEntry userOrNull = userStore.tryGetUserById(userId);
                     if (userOrNull == null)
                     {
                         System.err.printf("User '%s' does not exist.\n", userId);
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
index 158e248f2bb..aee60a5a933 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/ldap/LDAPAuthenticationService.java
@@ -157,6 +157,12 @@ public class LDAPAuthenticationService implements IAuthenticationService
         return true;
     }
 
+    @Override
+    public boolean supportsAuthenticatingByEmail()
+    {
+        return true;
+    }
+
     @Override
     public void check() throws EnvironmentFailureException, ConfigurationFailureException
     {
diff --git a/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java b/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java
index 14a0600152e..4a7ec39f6a6 100644
--- a/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java
+++ b/authentication/source/java/ch/systemsx/cisd/authentication/stacked/StackedAuthenticationService.java
@@ -41,6 +41,8 @@ public class StackedAuthenticationService implements IAuthenticationService
     private final boolean supportsListingByEmail;
 
     private final boolean supportsListingByLastName;
+    
+    private final boolean supportsAuthenticatingByEmail;
 
     public StackedAuthenticationService(List<IAuthenticationService> authenticationServices)
     {
@@ -49,17 +51,20 @@ public class StackedAuthenticationService implements IAuthenticationService
         boolean foundSupportsListingByUserId = false;
         boolean foundSupportsListingByEmail = false;
         boolean foundSupportsListingByLastName = false;
+        boolean foundSupportsAuthenticateByEmail = false;
         for (IAuthenticationService service : delegates)
         {
             foundRemote |= service.isRemote();
             foundSupportsListingByUserId |= service.supportsListingByUserId();
             foundSupportsListingByEmail |= service.supportsListingByEmail();
             foundSupportsListingByLastName |= service.supportsListingByLastName();
+            foundSupportsAuthenticateByEmail |= service.supportsAuthenticatingByEmail();
         }
         this.remote = foundRemote;
         this.supportsListingByUserId = foundSupportsListingByUserId;
         this.supportsListingByEmail = foundSupportsListingByEmail;
         this.supportsListingByLastName = foundSupportsListingByLastName;
+        this.supportsAuthenticatingByEmail = foundSupportsAuthenticateByEmail;
     }
 
     @Override
@@ -234,6 +239,12 @@ public class StackedAuthenticationService implements IAuthenticationService
         return supportsListingByUserId;
     }
 
+    @Override
+    public boolean supportsAuthenticatingByEmail()
+    {
+        return supportsAuthenticatingByEmail;
+    }
+
     @Override
     public void check() throws EnvironmentFailureException, ConfigurationFailureException
     {
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileAuthenticationServiceTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileAuthenticationServiceTest.java
index cf365373799..8559772c229 100644
--- a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileAuthenticationServiceTest.java
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileAuthenticationServiceTest.java
@@ -92,7 +92,7 @@ public class FileAuthenticationServiceTest
         context.checking(new Expectations()
         {
             {
-                one(userStore).tryGetUser(uid);
+                one(userStore).tryGetUserById(uid);
                 will(returnValue(user));
             }
         });
@@ -107,7 +107,7 @@ public class FileAuthenticationServiceTest
         context.checking(new Expectations()
         {
             {
-                one(userStore).tryGetUser(uid);
+                one(userStore).tryGetUserById(uid);
                 will(returnValue(null));
             }
         });
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileBasedLineStoreTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileBasedLineStoreTest.java
index a0647c61453..52728ef9604 100644
--- a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileBasedLineStoreTest.java
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/FileBasedLineStoreTest.java
@@ -82,11 +82,16 @@ public class FileBasedLineStoreTest
         file.delete();
         assertFalse(file.exists());
 
+        assertFalse(store.hasChanged());
         final List<String> lines1 = Arrays.asList("1", "2", "3"); 
         store.writeLines(lines1);
         assertTrue(file.exists());
+        assertFalse(store.hasChanged());
+        file.setLastModified(System.currentTimeMillis() + 1000);
+        assertTrue(store.hasChanged());
         assertEquals(StringUtils.join(lines1, '\n') + "\n", FileUtils.readFileToString(file));
         final List<String> linesRead1 = store.readLines();
+        assertFalse(store.hasChanged());
         assertEquals(lines1, linesRead1);
         assertTrue(svFile.exists());
         assertEquals(0, svFile.length());
diff --git a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java
index 585a4434323..35ad13bf17b 100644
--- a/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java
+++ b/authentication/sourceTest/java/ch/systemsx/cisd/authentication/file/LineBasedUserStoreTest.java
@@ -119,6 +119,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                 }
@@ -138,11 +140,27 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                 }
             });
-        assertNull(userStore.tryGetUser("uid"));
+        assertNull(userStore.tryGetUserById("uid"));
+        context.assertIsSatisfied();
+    }
+
+    @Test
+    public void testTryGetUserFailedNoStoreFile()
+    {
+        context.checking(new Expectations()
+            {
+                {
+                    one(lineStore).hasChanged();
+                    will(returnValue(false));
+                }
+            });
+        assertNull(userStore.tryGetUserById("uid"));
         context.assertIsSatisfied();
     }
 
@@ -157,11 +175,13 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                 }
             });
-        assertNull(userStore.tryGetUser("non-existent"));
+        assertNull(userStore.tryGetUserById("non-existent"));
         context.assertIsSatisfied();
     }
 
@@ -176,11 +196,13 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                 }
             });
-        assertEquals(u1, userStore.tryGetUser("uid1"));
+        assertEquals(u1, userStore.tryGetUserById("uid1"));
         context.assertIsSatisfied();
     }
 
@@ -197,6 +219,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                 }
@@ -218,6 +242,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                 }
@@ -234,6 +260,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(new ArrayList<String>()));
                     one(lineStore).writeLines(Collections.singletonList(userLine));
@@ -253,10 +281,10 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
-                    final List<String> lines = new ArrayList<String>();
-                    lines.add(oldUserLine);
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
-                    will(returnValue(lines));
+                    will(returnValue(Arrays.asList(oldUserLine)));
                     one(lineStore).writeLines(Arrays.asList(oldUserLine, newUserLine));
                 }
             });
@@ -280,6 +308,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                     one(lineStore).writeLines(linesUpdated);
@@ -289,6 +319,40 @@ public class LineBasedUserStoreTest
         context.assertIsSatisfied();
     }
 
+    @Test
+    public void testUpdateUserStoreChanged()
+    {
+        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> linesOld =
+                Arrays.asList(u1.asPasswordLine(), u3.asPasswordLine());
+        final List<String> linesNew =
+                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).hasChanged();
+                    will(returnValue(true));
+                    one(lineStore).readLines();
+                    will(returnValue(linesOld));
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
+                    one(lineStore).readLines();
+                    will(returnValue(linesNew));
+                    one(lineStore).writeLines(linesUpdated);
+                }
+            });
+        assertEquals(Arrays.asList(u1, u3), userStore.listUsers());
+        userStore.addOrUpdateUser(u3Updated);
+        context.assertIsSatisfied();
+    }
+
     @Test
     public void testRemoveUser()
     {
@@ -305,6 +369,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                     one(lineStore).writeLines(linesUpdated);
@@ -314,6 +380,40 @@ public class LineBasedUserStoreTest
         context.assertIsSatisfied();
     }
 
+    @Test
+    public void testRemoveUserStoreChanged()
+    {
+        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> linesOld =
+                new ArrayList<String>(Arrays.asList(u1.asPasswordLine(), u3.asPasswordLine()));
+        final List<String> linesNew =
+                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).hasChanged();
+                    will(returnValue(true));
+                    one(lineStore).readLines();
+                    will(returnValue(linesOld));
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
+                    one(lineStore).readLines();
+                    will(returnValue(linesNew));
+                    one(lineStore).writeLines(linesUpdated);
+                }
+            });
+        assertEquals(Arrays.asList(u1, u3), userStore.listUsers());
+        userStore.removeUser(uid1);
+        context.assertIsSatisfied();
+    }
+
     @Test
     public void testRemoveNonExistingUser()
     {
@@ -326,6 +426,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                 }
@@ -344,6 +446,8 @@ public class LineBasedUserStoreTest
         context.checking(new Expectations()
             {
                 {
+                    one(lineStore).hasChanged();
+                    will(returnValue(true));
                     one(lineStore).readLines();
                     will(returnValue(lines));
                     one(lineStore).writeLines(Collections.<String> emptyList());
-- 
GitLab