java/syncbase: Join Or Create Userdata. UUID collection+syncgroup

Userdata will avoid the syncgroup merge issue by using a join or
create pattern. This collection should be used if one wants to
have a user-only collection.

db.collection(name) => db.createCollection()

Every non-userdata collection name will be assigned a UUID.
Syncgroups will use the same name, and thus avoid the merge
problem.

Drawbacks:
- Watch is a little harder since you can't expect any particular name,
  only a prefix (if you specify one).
- Collection creation in a batch is also a little harder to use since
  we are no longer idempotent when calling db.collection.

Change-Id: I35bc93519667110914ea7c52c95cb6c006ee3bb8
diff --git a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
index 81b7f1a..b60b504 100644
--- a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
+++ b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
@@ -4,6 +4,8 @@
 
 package io.v.syncbase;
 
+import java.util.UUID;
+
 import io.v.syncbase.core.VError;
 import io.v.syncbase.exception.SyncbaseException;
 
@@ -25,7 +27,8 @@
      * @throws IllegalArgumentException if opts.withoutSyncgroup false
      */
     @Override
-    public Collection collection(String name, CollectionOptions opts) throws SyncbaseException {
+    public Collection createCollection(CollectionOptions opts) throws SyncbaseException {
+        String name = opts.prefix + "_" + UUID.randomUUID().toString().replaceAll("-", "");
         if (!opts.withoutSyncgroup) {
             throw new IllegalArgumentException("Cannot create syncgroup in a batch");
         }
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index eda76d9..e86141f 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -12,6 +12,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 
 import io.v.syncbase.core.CollectionRowPattern;
@@ -50,7 +51,12 @@
     }
 
     @Override
-    public Collection collection(String name, CollectionOptions opts) throws SyncbaseException {
+    public Collection createCollection(CollectionOptions opts) throws SyncbaseException {
+        String name = opts.prefix + "_" + UUID.randomUUID().toString().replaceAll("-", "");
+        return createNamedCollection(name, opts);
+    }
+
+    Collection createNamedCollection(String name, CollectionOptions opts) throws SyncbaseException {
         Collection res = getCollection(new Id(Syncbase.getPersonalBlessingString(), name));
         res.createIfMissing();
         // TODO(sadovsky): Unwind collection creation on syncgroup creation failure? It would be
@@ -61,6 +67,15 @@
         return res;
     }
 
+
+    /**
+     * Returns a reference to the userdata collection. Returns null if the user is not currently
+     * logged in.
+     */
+    public Collection getUserdataCollection() throws SyncbaseException {
+        return getCollection(new Id(Syncbase.getPersonalBlessingString(), Syncbase.USERDATA_NAME));
+    }
+
     /**
      * FOR ADVANCED USERS. Options for syncgroup creation.
      */
@@ -95,7 +110,7 @@
         // Remember this syncgroup in the userdata collection. The value doesn't matter, but since
         // VOM won't accept null, use a boolean.
         // Note: We may eventually want to use the value to deal with rejected invitations.
-        Syncbase.sUserdataCollection.put(id.encode(), true);
+        Syncbase.addToUserdata(id);
         return syncgroup;
     }
 
@@ -258,8 +273,9 @@
                         expectedBlessings.add(Syncbase.sOpts.getCloudBlessingString());
                     }
                     coreSyncgroup.join(publishName, expectedBlessings, new SyncgroupMemberInfo());
-                } catch (VError vError) {
-                    cb.onFailure(vError);
+                    Syncbase.addToUserdata(invite.getId());
+                } catch (VError | SyncbaseException e) {
+                    cb.onFailure(e);
                     return;
                 }
                 cb.onSuccess(new Syncgroup(coreSyncgroup, database));
@@ -428,9 +444,14 @@
 
                     @Override
                     public void onChange(io.v.syncbase.core.WatchChange coreWatchChange) {
-                        // TODO(razvanm): Ignore changes to userdata collection.
-                        if (coreWatchChange.entityType !=
-                                io.v.syncbase.core.WatchChange.EntityType.ROOT) {
+                        boolean isRoot = coreWatchChange.entityType ==
+                                io.v.syncbase.core.WatchChange.EntityType.ROOT;
+                        boolean isUserdataCollectionRow =
+                                coreWatchChange.entityType ==
+                                        io.v.syncbase.core.WatchChange.EntityType.ROW &&
+                                coreWatchChange.collection.name.equals(Syncbase.USERDATA_NAME) &&
+                                coreWatchChange.row.startsWith(Syncbase.USERDATA_COLLECTION_PREFIX);
+                        if (!isRoot && !isUserdataCollectionRow) {
                             mBatch.add(new WatchChange(coreWatchChange));
                         }
                         if (!coreWatchChange.continued) {
diff --git a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
index b31f96b..90a8761 100644
--- a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
+++ b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
@@ -19,6 +19,8 @@
 public abstract class DatabaseHandle {
     private final io.v.syncbase.core.DatabaseHandle mCoreDatabaseHandle;
 
+    protected static final String DEFAULT_COLLECTION_PREFIX = "cx";
+
     DatabaseHandle(io.v.syncbase.core.DatabaseHandle coreDatabaseHandle) {
         mCoreDatabaseHandle = coreDatabaseHandle;
     }
@@ -35,32 +37,38 @@
      */
     public static class CollectionOptions {
         public boolean withoutSyncgroup;
+        public String prefix = DEFAULT_COLLECTION_PREFIX;
 
         public CollectionOptions setWithoutSyncgroup(boolean value) {
             withoutSyncgroup = value;
             return this;
         }
+
+        public CollectionOptions setPrefix(String value) {
+            prefix = value;
+            return this;
+        }
     }
 
     /**
-     * Creates a collection and an associated syncgroup, as needed. Idempotent. The id of the new
-     * collection will include the creator's user id and the given collection name. Upon creation,
-     * both the collection and syncgroup are {@code READ_WRITE} for the creator. Setting
-     * {@code opts.withoutSyncgroup} prevents syncgroup creation. May only be called within a batch
-     * if {@code opts.withoutSyncgroup} is set.
+     * Creates a new collection and an associated syncgroup.
+     * The id of the collection will include the creator's user id and its name will be a UUID.
+     * Upon creation, both the collection and syncgroup are {@code READ_WRITE} for the creator.
+     * Setting {@code opts.withoutSyncgroup} prevents syncgroup creation.
+     * Setting {@code opts.prefix} will assign a UUID name starting with that prefix.
+     * May only be called within a batch if {@code opts.withoutSyncgroup} is set.
      *
-     * @param name name of the collection
      * @param opts options for collection creation
      * @return the collection handle
      */
-    public abstract Collection collection(String name, CollectionOptions opts)
+    public abstract Collection createCollection(CollectionOptions opts)
             throws SyncbaseException;
 
     /**
      * Calls {@code collection(name, opts)} with default {@code CollectionOptions}.
      */
-    public Collection collection(String name) throws SyncbaseException {
-        return collection(name, new CollectionOptions());
+    public Collection createCollection() throws SyncbaseException {
+        return createCollection(new CollectionOptions());
     }
 
     /**
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index be4981f..0f7caab 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -117,8 +117,11 @@
     static final String
             TAG = "syncbase",
             DIR_NAME = "syncbase",
-            DB_NAME = "db",
-            USERDATA_SYNCGROUP_NAME = "userdata__";
+            DB_NAME = "db";
+
+    public static final String
+            USERDATA_NAME = "userdata__",
+            USERDATA_COLLECTION_PREFIX = "__collections/";
 
     private static Map selfAndCloud() throws SyncbaseException {
         return ImmutableMap.of(Permissions.IN,
@@ -150,7 +153,6 @@
      */
     public static Database database() throws SyncbaseException {
         try {
-
             if (!isLoggedIn()) {
                 return null;
             }
@@ -186,7 +188,11 @@
      * Returns the currently logged in user.
      */
     public static User getLoggedInUser() {
-        throw new UnsupportedOperationException("Not implemented");
+        try {
+            return new User(getAliasFromBlessingPattern(getPersonalBlessingString()));
+        } catch (SyncbaseException e) {
+            return null;
+        }
     }
 
     public interface LoginCallback {
@@ -247,14 +253,19 @@
                         return;
                     }
                     sDatabase.createIfMissing();
-                    sUserdataCollection = sDatabase.collection(
-                            USERDATA_SYNCGROUP_NAME,
+                    sUserdataCollection = sDatabase.createNamedCollection(
+                            USERDATA_NAME,
                             new DatabaseHandle.CollectionOptions().setWithoutSyncgroup(true));
                     if (!sOpts.disableUserdataSyncgroup) {
                         Syncgroup syncgroup = sUserdataCollection.getSyncgroup();
-                        // TODO(razvanm): First we need to try to join, then we need to try to
-                        // create the syncgroup.
-                        syncgroup.createIfMissing(ImmutableList.of(sUserdataCollection));
+                        // Join-Or-Create pattern. If join fails, create the syncgroup instead.
+                        // Note: Syncgroup merge does not exist yet, so this may potentially lead
+                        // to split-brain syncgroups. This is exacerbated by lack of cloud instance.
+                        try {
+                            syncgroup.join();
+                        } catch(VError e) {
+                            syncgroup.createIfMissing(ImmutableList.of(sUserdataCollection));
+                        }
                         sDatabase.addWatchChangeHandler(new UserdataWatchHandler());
                     }
                     sOpts.callbackExecutor.execute(new Runnable() {
@@ -288,11 +299,14 @@
 
         private void onWatchChange(Iterator<WatchChange> changes) {
             WatchChange watchChange = changes.next();
-            if (watchChange.getCollectionId().getName().equals(USERDATA_SYNCGROUP_NAME) &&
+            if (watchChange.getCollectionId().getName().equals(USERDATA_NAME) &&
                     watchChange.getEntityType() == WatchChange.EntityType.ROW &&
-                    watchChange.getChangeType() == WatchChange.ChangeType.PUT) {
+                    watchChange.getChangeType() == WatchChange.ChangeType.PUT &&
+                    watchChange.getRowKey().startsWith(USERDATA_COLLECTION_PREFIX)) {
                 try {
-                    sDatabase.getSyncgroup(Id.decode(watchChange.getRowKey())).join();
+                    String encodedId = watchChange.getRowKey().
+                            substring(Syncbase.USERDATA_COLLECTION_PREFIX.length());
+                    sDatabase.getSyncgroup(Id.decode(encodedId)).join();
                 } catch (VError vError) {
                     vError.printStackTrace();
                     System.err.println(vError.toString());
@@ -301,6 +315,10 @@
         }
     }
 
+    static void addToUserdata(Id id) throws SyncbaseException {
+        sUserdataCollection.put(Syncbase.USERDATA_COLLECTION_PREFIX + id.encode(), true);
+    }
+
     /**
      * Scans the neighborhood for nearby users.
      *
diff --git a/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java b/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java
index c598c1a..1569d10 100644
--- a/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/BatchDatabaseTest.java
@@ -44,7 +44,7 @@
         thrown.expect(IllegalArgumentException.class);
         thrown.expectMessage("Cannot create syncgroup in a batch");
 
-        batch.collection("aCollection", new CollectionOptions());
+        batch.createCollection(new CollectionOptions());
     }
 
 }
diff --git a/syncbase/src/test/java/io/v/syncbase/CollectionTest.java b/syncbase/src/test/java/io/v/syncbase/CollectionTest.java
index 76312e7..6be3f76 100644
--- a/syncbase/src/test/java/io/v/syncbase/CollectionTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/CollectionTest.java
@@ -50,7 +50,7 @@
         thrown.expectMessage("invalid name");
 
         // Create with invalid name
-        Syncbase.database().collection("name with spaces", new CollectionOptions());
+        Syncbase.database().createNamedCollection("name with spaces", new CollectionOptions());
     }
 
     @Test
@@ -59,7 +59,7 @@
         BatchDatabase batch = Syncbase.database().beginBatch(new Database.BatchOptions());
 
         // Create collection without a syncgroup
-        Collection collection = batch.collection("collectionName",
+        Collection collection = batch.createCollection(
                 new CollectionOptions().setWithoutSyncgroup(true));
 
         thrown.expect(IllegalArgumentException.class);
diff --git a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
index 82f410e..f8fd471 100644
--- a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -60,6 +60,21 @@
         return res;
     }
 
+    private static boolean idsMatch(Iterable<Id> ids, String blessing, List<String> prefixes) {
+        int i = 0;
+        for (Id id : ids) {
+            if (!idMatch(id, blessing, prefixes.get(i))) {
+                return false;
+            }
+            i++;
+        }
+        return prefixes.size() == i; // Every id matches, and all prefixes were used.
+    }
+
+    private static boolean idMatch(Id id, String blessing, String prefix) {
+        return id.getName().startsWith(prefix) && id.getBlessing().equals(blessing);
+    }
+
     private static Iterable<Id> getSyncgroupIds(Database db) throws SyncbaseException {
         List<Id> res = new ArrayList<>();
         for (Iterator<Syncgroup> it = db.getSyncgroups(); it.hasNext(); ) {
@@ -77,35 +92,35 @@
     public void testCreateAndGetCollections() throws Exception {
         Database db = createDatabase();
         assertNotNull(db);
-        DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
-        opts.withoutSyncgroup = true;
-        Collection cxA = db.collection("a", opts);
+        DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions().
+                setWithoutSyncgroup(true).setPrefix("a");
+
+        Collection cxA = db.createCollection(opts);
         assertNotNull(cxA);
-        // TODO(sadovsky): Should we omit the userdata collection?
-        assertThat(getCollectionIds(db)).containsExactly(
-                new Id(Syncbase.getPersonalBlessingString(), "a"),
-                new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
-        db.collection("b", opts);
-        assertThat(getCollectionIds(db)).containsExactly(
-                new Id(Syncbase.getPersonalBlessingString(), "a"),
-                new Id(Syncbase.getPersonalBlessingString(), "b"),
-                new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
-        // Note, createDatabase() sets disableSyncgroupPublishing to true, so db.collection(name) is
-        // a purely local operation.
-        db.collection("c");
-        assertThat(getCollectionIds(db)).containsExactly(
-                new Id(Syncbase.getPersonalBlessingString(), "a"),
-                new Id(Syncbase.getPersonalBlessingString(), "b"),
-                new Id(Syncbase.getPersonalBlessingString(), "c"),
-                new Id(Syncbase.getPersonalBlessingString(), "userdata__"));
-        Collection secondCxA = db.collection("a", opts);
-        assertEquals(cxA.getId(), secondCxA.getId());
+        assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+                ImmutableList.of("a", Syncbase.USERDATA_NAME)));
+
+        db.createCollection(opts.setPrefix("b"));
+        assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+                ImmutableList.of("a", "b", Syncbase.USERDATA_NAME)));
+
+        // Note, createDatabase() sets disableSyncgroupPublishing to true, so
+        // db.createCollection(opts) is still a purely local operation.
+        opts = new DatabaseHandle.CollectionOptions();
+        db.createCollection(opts.setPrefix("c"));
+        assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+                ImmutableList.of("a", "b", "c", Syncbase.USERDATA_NAME)));
+
+        Collection secondCxA = db.createCollection(opts.setPrefix("a"));
+        assertFalse(cxA.getId().equals(secondCxA.getId()));
+        assertTrue(idsMatch(getCollectionIds(db), Syncbase.getPersonalBlessingString(),
+                ImmutableList.of("a", "a", "b", "c", Syncbase.USERDATA_NAME)));
     }
 
     @Test
     public void testRowCrudMethods() throws Exception {
         Database db = createDatabase();
-        Collection cx = db.collection("cx");
+        Collection cx = db.createCollection();
         assertNotNull(cx);
         assertFalse(cx.exists("foo"));
         assertEquals(cx.get("foo", String.class), null);
@@ -141,17 +156,17 @@
         Database db = createDatabase();
         DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions();
         opts.withoutSyncgroup = true;
-        Collection cxA = db.collection("a", opts);
-        Collection cxB = db.collection("b", opts);
-        Collection cxC = db.collection("c");
+        Collection cxA = db.createCollection(opts.setPrefix("a"));
+        Collection cxB = db.createCollection(opts.setPrefix("b"));
+        Collection cxC = db.createCollection(opts.setWithoutSyncgroup(false).setPrefix("c"));
         assertNotNull(cxA);
         // Note, there's no userdata syncgroup since we set disableUserdataSyncgroup to true.
-        assertThat(getSyncgroupIds(db)).containsExactly(
-                new Id(Syncbase.getPersonalBlessingString(), "c"));
+        assertTrue(idsMatch(getSyncgroupIds(db), Syncbase.getPersonalBlessingString(),
+                ImmutableList.of("c")));
         db.syncgroup("sg1", ImmutableList.of(cxA));
         db.syncgroup("sg2", ImmutableList.of(cxA, cxB, cxC));
         assertThat(getSyncgroupIds(db)).containsExactly(
-                new Id(Syncbase.getPersonalBlessingString(), "c"),
+                new Id(Syncbase.getPersonalBlessingString(), cxC.getSyncgroup().getId().getName()),
                 new Id(Syncbase.getPersonalBlessingString(), "sg1"),
                 new Id(Syncbase.getPersonalBlessingString(), "sg2"));
     }
@@ -161,8 +176,9 @@
         Database db = createDatabase();
         final SettableFuture<Void> waitOnInitialState = SettableFuture.create();
         final SettableFuture<Void> waitOnChangeBatch = SettableFuture.create();
-        Collection collection = db.collection("c");
+        Collection collection = db.createCollection();
         collection.put("foo", 1);
+        final String collectionName = collection.getId().getName();
         db.addWatchChangeHandler(new Database.WatchChangeHandler() {
             @Override
             public void onInitialState(Iterator<WatchChange> values) {
@@ -172,13 +188,13 @@
                 WatchChange watchChange = (WatchChange) values.next();
                 assertEquals(WatchChange.EntityType.COLLECTION, watchChange.getEntityType());
                 assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
-                assertEquals("c", watchChange.getCollectionId().getName());
+                assertTrue(watchChange.getCollectionId().getName().equals(collectionName));
                 // 2nd change: the row for the "foo" key.
                 assertTrue(values.hasNext());
                 watchChange = (WatchChange) values.next();
                 assertEquals(WatchChange.EntityType.ROW, watchChange.getEntityType());
                 assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
-                assertEquals("c", watchChange.getCollectionId().getName());
+                assertTrue(watchChange.getCollectionId().getName().equals(collectionName));
                 assertEquals("foo", watchChange.getRowKey());
                 // TODO(razvanm): Uncomment after the POJO start working.
                 //assertEquals(1, watchChange.getValue());
@@ -187,13 +203,9 @@
                 watchChange = (WatchChange) values.next();
                 assertEquals(WatchChange.EntityType.COLLECTION, watchChange.getEntityType());
                 assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
-                // 4th change: the userdata collection has a row for "c"'s syncgroup.
-                assertTrue(values.hasNext());
-                watchChange = (WatchChange) values.next();
-                assertEquals(WatchChange.EntityType.ROW, watchChange.getEntityType());
-                assertEquals(WatchChange.ChangeType.PUT, watchChange.getChangeType());
-                assertEquals("userdata__", watchChange.getCollectionId().getName());
-                assertTrue(watchChange.getRowKey().endsWith("c"));
+                assertTrue(watchChange.getCollectionId().getName().equals(Syncbase.USERDATA_NAME));
+                // No more changes.
+                assertFalse(values.hasNext());
                 waitOnInitialState.set(null);
             }
 
@@ -202,6 +214,7 @@
                 assertTrue(changes.hasNext());
                 WatchChange watchChange = changes.next();
                 assertEquals(WatchChange.ChangeType.DELETE, watchChange.getChangeType());
+                assertTrue(watchChange.getCollectionId().getName().startsWith("c"));
                 // TODO(razvanm): Uncomment after the POJO start working.
                 //assertEquals(1, watchChange.getValue());
                 assertFalse(changes.hasNext());
@@ -224,26 +237,36 @@
     @Test
     public void testRunInBatch() throws Exception {
         Database db = createDatabase();
-        db.runInBatch(new Database.BatchOperation() {
+
+        // We need a box class to store the id of the created collection since we want to refer to
+        // it after the batch is over.
+        class TestOperation implements Database.BatchOperation {
+            private Id id;
+
             @Override
             public void run(BatchDatabase db) {
                 try {
                     DatabaseHandle.CollectionOptions opts = new DatabaseHandle.CollectionOptions()
                             .setWithoutSyncgroup(true);
-                    db.collection("c", opts).put("foo", 10);
+                    Collection c = db.createCollection(opts);
+                    c.put("foo", 10);
+                    id = c.getId();
                 } catch (SyncbaseException e) {
                     e.printStackTrace();
                     fail(e.toString());
                 }
             }
-        });
-        assertEquals(db.collection("c").get("foo", Integer.class), Integer.valueOf(10));
+        }
+
+        TestOperation op = new TestOperation();
+        db.runInBatch(op);
+        assertEquals(db.getCollection(op.id).get("foo", Integer.class), Integer.valueOf(10));
     }
 
     @Test
     public void testSyncgroupInviteUsers() throws Exception {
         Database db = createDatabase();
-        Collection collection = db.collection("c");
+        Collection collection = db.createCollection();
         Syncgroup sg = collection.getSyncgroup();
 
         User alice = new User("alice");