java/syncbase: Improve AccessList inspectability and Watch Cancel

We improve usability of the AccessList and Watch.
The former can be inspected more easily now.
And the latter can be canceled without throwing.

Note: We will want to actually implement watch cancel correctly.
Change-Id: Ib8f4f7a2516342c537e9cda6ad96f5a33e147121
diff --git a/syncbase/src/main/java/io/v/syncbase/AccessList.java b/syncbase/src/main/java/io/v/syncbase/AccessList.java
index 075ad1c..2494529 100644
--- a/syncbase/src/main/java/io/v/syncbase/AccessList.java
+++ b/syncbase/src/main/java/io/v/syncbase/AccessList.java
@@ -48,10 +48,19 @@
         this.users = new HashMap<>();
     }
 
+    /**
+     * Gets the user's access level.
+     */
     public AccessLevel getAccessLevelForUser(User user) {
         return users.get(user.getAlias());
     }
 
+    /**
+     * Changes the user's access level and returns their previous access level.
+     *
+     * @param user The user whose access level is changing.
+     * @param newLevel The user's new access level.
+     */
     public AccessLevel setAccessLevel(User user, AccessLevel newLevel) {
         if (newLevel == null) {
             return removeAccessLevel(user);
@@ -61,11 +70,31 @@
         return oldLevel;
     }
 
+    /**
+     * Removes access from the given user.
+     */
     public AccessLevel removeAccessLevel(User user) {
         return users.remove(user);
     }
 
     /**
+     * Obtains the users that match the given access level.
+     * Filters out the cloud, which always has READ_WRITE_ADMIN access.
+     */
+    public java.util.Collection<User> getByAccessLevel(AccessLevel level) {
+        Set<User> matchingUsers = new HashSet<>();
+        for (Map.Entry<String, AccessLevel> entry : users.entrySet()) {
+            String alias = entry.getKey();
+            boolean isCloud = Syncbase.sOpts.mCloudAdmin != null &&
+                    alias.equals(Syncbase.getAliasFromBlessingPattern(Syncbase.sOpts.mCloudAdmin));
+            if (!isCloud && entry.getValue().equals(level)) {
+                matchingUsers.add(new User(alias));
+            }
+        }
+        return matchingUsers;
+    }
+
+    /**
      * TODO(alexfandrianto): Vary implementation if this constructor needs to be called with non-
      * collection permissions. The current simplification allows us to know that read/write/admin
      * are available for parsing.
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index 47b1034..83134b0 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -14,6 +14,7 @@
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import io.v.syncbase.core.CollectionRowPattern;
 import io.v.syncbase.core.SyncgroupMemberInfo;
@@ -504,6 +505,8 @@
             throw new UnsupportedOperationException("Specifying resumeMarker is not yet supported");
         }
 
+        final AtomicBoolean canceled = new AtomicBoolean(false);
+
         mCoreDatabase.watch(null, ImmutableList.of(opts.getCollectionRowPattern()),
                 new io.v.syncbase.core.Database.WatchPatternsCallbacks() {
                     private boolean mGotFirstBatch = false;
@@ -511,6 +514,10 @@
 
                     @Override
                     public void onChange(io.v.syncbase.core.WatchChange coreWatchChange) {
+                        if (canceled.get()) {
+                            return;
+                        }
+
                         boolean isRoot = coreWatchChange.entityType ==
                                 io.v.syncbase.core.WatchChange.EntityType.ROOT;
                         boolean isUserdataInternalRow =
@@ -564,7 +571,8 @@
             mWatchChangeHandlers.put(h, new Runnable() {
                 @Override
                 public void run() {
-                    throw new UnsupportedOperationException("Not implemented");
+                    // TODO(alexfandrianto): Implement properly. AtomicBoolean is only a patch.
+                    canceled.set(true);
                 }
             });
         }
diff --git a/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java b/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java
index b9b1490..667c7a8 100644
--- a/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java
@@ -53,66 +53,66 @@
         // Wildcard and prefix tests.
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                         .build();
-        assertEquals(opts.blessing, "%");
-        assertEquals(opts.name, "%");
-        assertEquals(opts.row, "%");
+        assertEquals("%", opts.blessing);
+        assertEquals("%", opts.name);
+        assertEquals("%", opts.row);
 
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                         .setCollectionId(new Id("a", "b"))
                         .build();
-        assertEquals(opts.blessing, "a");
-        assertEquals(opts.name, "b");
-        assertEquals(opts.row, "%");
+        assertEquals("a", opts.blessing);
+        assertEquals("b", opts.name);
+        assertEquals("%", opts.row);
 
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                 .setCollectionNamePrefix("c")
                 .build();
-        assertEquals(opts.blessing, "%");
-        assertEquals(opts.name, "c%");
-        assertEquals(opts.row, "%");
+        assertEquals("%", opts.blessing);
+        assertEquals("c%", opts.name);
+        assertEquals("%", opts.row);
 
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                 .setRowKey("d")
                 .build();
-        assertEquals(opts.blessing, "%");
-        assertEquals(opts.name, "%");
-        assertEquals(opts.row, "d");
+        assertEquals("%", opts.blessing);
+        assertEquals("%", opts.name);
+        assertEquals("d", opts.row);
 
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                 .setRowKeyPrefix("e")
                 .build();
-        assertEquals(opts.blessing, "%");
-        assertEquals(opts.name, "%");
-        assertEquals(opts.row, "e%");
+        assertEquals("%", opts.blessing);
+        assertEquals("%", opts.name);
+        assertEquals("e%", opts.row);
 
         // Escaping tests. %, _ and \ are special characters.
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                 .setCollectionId(new Id("%", "_"))
                 .build();
-        assertEquals(opts.blessing, "\\%");
-        assertEquals(opts.name, "\\_");
-        assertEquals(opts.row, "%");
+        assertEquals("\\%", opts.blessing);
+        assertEquals("\\_", opts.name);
+        assertEquals("%", opts.row);
 
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                 .setCollectionNamePrefix("\\")
                 .build();
-        assertEquals(opts.blessing, "%");
-        assertEquals(opts.name, "\\\\%");
-        assertEquals(opts.row, "%");
+        assertEquals("%", opts.blessing);
+        assertEquals("\\\\%", opts.name);
+        assertEquals("%", opts.row);
 
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                 .setRowKey("%%")
                 .build();
-        assertEquals(opts.blessing, "%");
-        assertEquals(opts.name, "%");
-        assertEquals(opts.row, "\\%\\%");
+        assertEquals("%", opts.blessing);
+        assertEquals("%", opts.name);
+        assertEquals("\\%\\%", opts.row);
 
         opts = new Database.AddWatchChangeHandlerOptions.Builder()
                 .setRowKeyPrefix("_\\_")
                 .build();
-        assertEquals(opts.blessing, "%");
-        assertEquals(opts.name, "%");
-        assertEquals(opts.row, "\\_\\\\\\_%");
+        assertEquals("%", opts.blessing);
+        assertEquals("%", opts.name);
+        assertEquals("\\_\\\\\\_%", opts.row);
     }
 
 }
diff --git a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
index 66a63a6..d2b7679 100644
--- a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -124,18 +124,18 @@
         Collection cx = db.createCollection();
         assertNotNull(cx);
         assertFalse(cx.exists("foo"));
-        assertEquals(cx.get("foo", String.class), null);
+        assertEquals(null, cx.get("foo", String.class));
         cx.put("foo", "bar");
         assertTrue(cx.exists("foo"));
-        assertEquals(cx.get("foo", String.class), "bar");
+        assertEquals("bar", cx.get("foo", String.class));
         cx.put("foo", "baz");
         assertTrue(cx.exists("foo"));
-        assertEquals(cx.get("foo", String.class), "baz");
+        assertEquals("baz", cx.get("foo", String.class));
         cx.delete("foo");
         assertFalse(cx.exists("foo"));
-        assertEquals(cx.get("foo", String.class), null);
+        assertEquals(null, cx.get("foo", String.class));
         cx.put("foo", 5);
-        assertEquals(cx.get("foo", Integer.class), Integer.valueOf(5));
+        assertEquals(Integer.valueOf(5), cx.get("foo", Integer.class));
 
         // TODO(razvanm): Figure out a way to get the POJOs to work.
 //        // This time, with a POJO.
@@ -395,7 +395,7 @@
 
         TestOperation op = new TestOperation();
         db.runInBatch(op);
-        assertEquals(db.getCollection(op.id).get("foo", Integer.class), Integer.valueOf(10));
+        assertEquals(Integer.valueOf(10), db.getCollection(op.id).get("foo", Integer.class));
     }
 
     @Test
@@ -413,6 +413,10 @@
         assertNull(acl0.getAccessLevelForUser(alice));
         assertNull(acl0.getAccessLevelForUser(bob));
         assertNull(acl0.getAccessLevelForUser(carol));
+        assertEquals(0, acl0.getByAccessLevel(AccessList.AccessLevel.READ).size());
+        assertEquals(0, acl0.getByAccessLevel(AccessList.AccessLevel.READ_WRITE).size());
+        // Note: This is 1 because the creator of the collection starts with RWA access.
+        assertEquals(1, acl0.getByAccessLevel(AccessList.AccessLevel.READ_WRITE_ADMIN).size());
 
         // Alice can read now.
         sg.inviteUser(new User("alice"), AccessList.AccessLevel.READ);
@@ -420,6 +424,9 @@
         assertEquals(acl1.getAccessLevelForUser(alice), AccessList.AccessLevel.READ);
         assertNull(acl1.getAccessLevelForUser(bob));
         assertNull(acl1.getAccessLevelForUser(carol));
+        assertEquals(1, acl1.getByAccessLevel(AccessList.AccessLevel.READ).size());
+        assertEquals(0, acl1.getByAccessLevel(AccessList.AccessLevel.READ_WRITE).size());
+        assertEquals(1, acl1.getByAccessLevel(AccessList.AccessLevel.READ_WRITE_ADMIN).size());
 
         // Bob can both read and write now.
         sg.inviteUser(new User("bob"), AccessList.AccessLevel.READ_WRITE);
@@ -427,6 +434,9 @@
         assertEquals(acl2.getAccessLevelForUser(alice), AccessList.AccessLevel.READ);
         assertEquals(acl2.getAccessLevelForUser(bob), AccessList.AccessLevel.READ_WRITE);
         assertNull(acl2.getAccessLevelForUser(carol));
+        assertEquals(1, acl2.getByAccessLevel(AccessList.AccessLevel.READ).size());
+        assertEquals(1, acl2.getByAccessLevel(AccessList.AccessLevel.READ_WRITE).size());
+        assertEquals(1, acl2.getByAccessLevel(AccessList.AccessLevel.READ_WRITE_ADMIN).size());
 
         // Alice and Carol get full access now. (Tests overwrite and multiple invites.)
         sg.inviteUsers(ImmutableList.of(new User("alice"), new User("carol")),
@@ -435,6 +445,9 @@
         assertEquals(acl3.getAccessLevelForUser(alice), AccessList.AccessLevel.READ_WRITE_ADMIN);
         assertEquals(acl3.getAccessLevelForUser(bob), AccessList.AccessLevel.READ_WRITE);
         assertEquals(acl3.getAccessLevelForUser(carol), AccessList.AccessLevel.READ_WRITE_ADMIN);
+        assertEquals(0, acl3.getByAccessLevel(AccessList.AccessLevel.READ).size());
+        assertEquals(1, acl3.getByAccessLevel(AccessList.AccessLevel.READ_WRITE).size());
+        assertEquals(3, acl3.getByAccessLevel(AccessList.AccessLevel.READ_WRITE_ADMIN).size());
     }
 
     @Test