syncbase java high-level: more bits of implementation

Change-Id: Ife7195c5903699a0086fd0504ebb45a7736835ff
diff --git a/syncbase/build.gradle b/syncbase/build.gradle
index a89a3d1..ec0a3fd 100644
--- a/syncbase/build.gradle
+++ b/syncbase/build.gradle
@@ -34,12 +34,15 @@
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
     }
+    lintOptions {
+        abortOnError false
+    }
 }
 
 dependencies {
     compile fileTree(dir: 'libs', include: ['*.jar'])
     compile 'com.android.support:appcompat-v7:23.4.0'
-    compile 'io.v:vanadium-android:2.1.3'
+    compile 'io.v:vanadium-android:2.1.4'
     testCompile 'junit:junit:4.12'
     androidTestCompile 'com.android.support.test:runner:0.4.1'
     androidTestCompile 'com.android.support.test:rules:0.4.1'
diff --git a/syncbase/src/main/java/io/v/syncbase/AccessList.java b/syncbase/src/main/java/io/v/syncbase/AccessList.java
index 3b7e158..b2304a2 100644
--- a/syncbase/src/main/java/io/v/syncbase/AccessList.java
+++ b/syncbase/src/main/java/io/v/syncbase/AccessList.java
@@ -4,7 +4,14 @@
 
 package io.v.syncbase;
 
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
+
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.access.Constants;
+import io.v.v23.security.access.Permissions;
 
 public class AccessList {
     public enum AccessLevel {
@@ -14,4 +21,84 @@
     }
 
     public Map<String, AccessLevel> users;
+
+    private static Set<String> vAccessListToUserIds(io.v.v23.security.access.AccessList accessList) {
+        if (!accessList.getNotIn().isEmpty()) {
+            throw new RuntimeException("Non-empty not-in section: " + accessList);
+        }
+        Set<String> res = new HashSet<>();
+        for (BlessingPattern bp : accessList.getIn()) {
+            res.add(Syncbase.getEmailFromBlessingPattern(bp));
+        }
+        return res;
+    }
+
+    public AccessList() {
+        this.users = new HashMap<>();
+    }
+
+    protected AccessList(Permissions perms) {
+        Set<String> readers = vAccessListToUserIds(perms.get(Constants.READ.getValue()));
+        Set<String> writers = vAccessListToUserIds(perms.get(Constants.WRITE.getValue()));
+        Set<String> admins = vAccessListToUserIds(perms.get(Constants.ADMIN.getValue()));
+        if (!readers.containsAll(writers)) {
+            throw new RuntimeException("Some writers are not readers: " + writers + ", " + readers);
+        }
+        if (!writers.containsAll(admins)) {
+            throw new RuntimeException("Some admins are not writers: " + admins + ", " + writers);
+        }
+        for (String userId : readers) {
+            users.put(userId, AccessLevel.READ);
+        }
+        for (String userId : writers) {
+            users.put(userId, AccessLevel.READ_WRITE);
+        }
+        for (String userId : admins) {
+            users.put(userId, AccessLevel.READ_WRITE_ADMIN);
+        }
+    }
+
+    private static void addToVAccessList(io.v.v23.security.access.AccessList accessList, BlessingPattern bp) {
+        if (!accessList.getIn().contains(bp)) {
+            accessList.getIn().add(bp);
+        }
+    }
+
+    private static void removeFromVAccessList(io.v.v23.security.access.AccessList accessList, BlessingPattern bp) {
+        accessList.getIn().remove(bp);
+    }
+
+    protected static void applyDelta(Permissions perms, AccessList delta) {
+        for (String userId : delta.users.keySet()) {
+            AccessLevel level = delta.users.get(userId);
+            BlessingPattern bp = Syncbase.getBlessingPatternFromEmail(userId);
+            if (level == null) {
+                removeFromVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
+                removeFromVAccessList(perms.get(Constants.READ.getValue()), bp);
+                removeFromVAccessList(perms.get(Constants.WRITE.getValue()), bp);
+                removeFromVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+            } else {
+                switch (level) {
+                    case READ:
+                        addToVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
+                        addToVAccessList(perms.get(Constants.READ.getValue()), bp);
+                        removeFromVAccessList(perms.get(Constants.WRITE.getValue()), bp);
+                        removeFromVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+                        break;
+                    case READ_WRITE:
+                        addToVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
+                        addToVAccessList(perms.get(Constants.READ.getValue()), bp);
+                        addToVAccessList(perms.get(Constants.WRITE.getValue()), bp);
+                        removeFromVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+                        break;
+                    case READ_WRITE_ADMIN:
+                        addToVAccessList(perms.get(Constants.RESOLVE.getValue()), bp);
+                        addToVAccessList(perms.get(Constants.READ.getValue()), bp);
+                        addToVAccessList(perms.get(Constants.WRITE.getValue()), bp);
+                        addToVAccessList(perms.get(Constants.ADMIN.getValue()), bp);
+                        break;
+                }
+            }
+        }
+    }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
index c231e29..26681bb 100644
--- a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
+++ b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
@@ -4,34 +4,21 @@
 
 package io.v.syncbase;
 
-import java.util.Iterator;
-
-public class BatchDatabase implements DatabaseHandle {
-    private final Database mDatabase;
+public class BatchDatabase extends DatabaseHandle {
     private final io.v.v23.syncbase.BatchDatabase mVBatchDatabase;
 
-    protected BatchDatabase(Database database, io.v.v23.syncbase.BatchDatabase vBatchDatabase) {
-        mDatabase = database;
+    protected BatchDatabase(io.v.v23.syncbase.BatchDatabase vBatchDatabase) {
+        super(vBatchDatabase);
         mVBatchDatabase = vBatchDatabase;
     }
 
-    public Id getId() {
-        return new Id(mVBatchDatabase.id());
-    }
-
     public Collection collection(String name, CollectionOptions opts) {
         if (!opts.withoutSyncgroup) {
             throw new RuntimeException("Cannot create syncgroup in a batch");
         }
-        return new Collection(mDatabase, mVBatchDatabase.getCollection(new io.v.v23.services.syncbase.Id(Syncbase.getPersonalBlessingString(), name)), true);
-    }
-
-    public Collection getCollection(Id id) {
-        return Database.getCollectionImpl(mDatabase, mVBatchDatabase, id);
-    }
-
-    public Iterator<Collection> getCollections() {
-        return Database.getCollectionsImpl(mDatabase, mVBatchDatabase);
+        Collection res = getCollection(new Id(Syncbase.getPersonalBlessingString(), name));
+        res.createIfMissing();
+        return res;
     }
 
     public void commit() {
diff --git a/syncbase/src/main/java/io/v/syncbase/Collection.java b/syncbase/src/main/java/io/v/syncbase/Collection.java
index 5dea588..af8503b 100644
--- a/syncbase/src/main/java/io/v/syncbase/Collection.java
+++ b/syncbase/src/main/java/io/v/syncbase/Collection.java
@@ -4,29 +4,28 @@
 
 package io.v.syncbase;
 
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-
 import io.v.v23.VFutures;
+import io.v.v23.security.access.Permissions;
 import io.v.v23.verror.ExistException;
 import io.v.v23.verror.VException;
 
 public class Collection {
-    private final Database mDatabase;
     private final io.v.v23.syncbase.Collection mVCollection;
+    private final DatabaseHandle mDatabaseHandle;
 
-    protected Collection(Database database, io.v.v23.syncbase.Collection vCollection, boolean createIfMissing) {
-        if (createIfMissing) {
-            try {
-                VFutures.sync(vCollection.create(Syncbase.getVContext(), null));
-            } catch (ExistException e) {
-                // Collection already exists.
-            } catch (VException e) {
-                throw new RuntimeException("Failed to create collection", e);
-            }
+    protected void createIfMissing() {
+        try {
+            VFutures.sync(mVCollection.create(Syncbase.getVContext(), Syncbase.defaultPerms()));
+        } catch (ExistException e) {
+            // Collection already exists.
+        } catch (VException e) {
+            throw new RuntimeException("Failed to create collection", e);
         }
-        mDatabase = database;
+    }
+
+    protected Collection(io.v.v23.syncbase.Collection vCollection, DatabaseHandle databaseHandle) {
         mVCollection = vCollection;
+        mDatabaseHandle = databaseHandle;
     }
 
     public Id getId() {
@@ -36,16 +35,18 @@
     // Shortcut for Database.getSyncgroup(c.getId()), helpful for the common case of one syncgroup
     // per collection.
     public Syncgroup getSyncgroup() {
-        return mDatabase.getSyncgroup(getId());
+        if (mDatabaseHandle instanceof BatchDatabase) {
+            throw new RuntimeException("Must not call getSyncgroup within batch");
+        }
+        return ((Database) mDatabaseHandle).getSyncgroup(getId());
     }
 
     // TODO(sadovsky): Maybe add scan API, if developers aren't satisfied with watch.
 
-    // TODO(sadovsky): Revisit this API, which was copied from io.v.v23.syncbase. For example, would
-    // the signature "public <T> T get(String key)" be preferable?
-    public Object get(String key, Type type) {
+    // TODO(sadovsky): Revisit this API. Is the Class<T> argument necessary?
+    public <T> T get(String key, Class<T> cls) {
         try {
-            return VFutures.sync(mVCollection.getRow(key).get(Syncbase.getVContext(), type));
+            return VFutures.sync(mVCollection.getRow(key).get(Syncbase.getVContext(), cls));
         } catch (VException e) {
             throw new RuntimeException("get failed: " + key, e);
         }
@@ -59,19 +60,9 @@
         }
     }
 
-    // TODO(sadovsky): Only needed for the current (old) version of io.v.v23.syncbase, which does
-    // not include fredq's change to the put() API.
-    private static Type getType(Object object) {
-        Type superclassType = object.getClass().getGenericSuperclass();
-        if (!ParameterizedType.class.isAssignableFrom(superclassType.getClass())) {
-            return null;
-        }
-        return ((ParameterizedType) superclassType).getActualTypeArguments()[0];
-    }
-
     public <T> void put(String key, T value) {
         try {
-            VFutures.sync(mVCollection.put(Syncbase.getVContext(), key, value, getType(value)));
+            VFutures.sync(mVCollection.put(Syncbase.getVContext(), key, value));
         } catch (VException e) {
             throw new RuntimeException("put failed: " + key, e);
         }
@@ -88,10 +79,38 @@
     // FOR ADVANCED USERS. The following methods manipulate the AccessList for this collection, but
     // not for associated syncgroups.
     public AccessList getAccessList() {
-        throw new RuntimeException("Not implemented");
+        try {
+            return new AccessList(VFutures.sync(mVCollection.getPermissions(Syncbase.getVContext())));
+        } catch (VException e) {
+            throw new RuntimeException("getPermissions failed", e);
+        }
     }
 
-    public void updateAccessList(AccessList delta) {
-        throw new RuntimeException("Not implemented");
+    public void updateAccessList(final AccessList delta) {
+        // Create a batch if we're not already in a batch.
+        final Id id = this.getId();
+        Database.BatchOperation op = new Database.BatchOperation() {
+            @Override
+            public void run(BatchDatabase db) {
+                io.v.v23.syncbase.Collection vCx = db.getCollection(id).mVCollection;
+                Permissions perms;
+                try {
+                    perms = VFutures.sync(vCx.getPermissions(Syncbase.getVContext()));
+                } catch (VException e) {
+                    throw new RuntimeException("getPermissions failed", e);
+                }
+                AccessList.applyDelta(perms, delta);
+                try {
+                    VFutures.sync(vCx.setPermissions(Syncbase.getVContext(), perms));
+                } catch (VException e) {
+                    throw new RuntimeException("setPermissions failed", e);
+                }
+            }
+        };
+        if (mDatabaseHandle instanceof BatchDatabase) {
+            op.run((BatchDatabase) mDatabaseHandle);
+        } else {
+            ((Database) mDatabaseHandle).runInBatch(op, new Database.BatchOptions());
+        }
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index 2e9236e..f03cba0 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -9,61 +9,33 @@
 import java.util.List;
 
 import io.v.v23.VFutures;
-import io.v.v23.syncbase.DatabaseCore;
 import io.v.v23.verror.ExistException;
 import io.v.v23.verror.VException;
 
-public class Database implements DatabaseHandle {
+public class Database extends DatabaseHandle {
     private final io.v.v23.syncbase.Database mVDatabase;
 
-    protected Database(io.v.v23.syncbase.Database vDatabase) {
+    protected void createIfMissing() {
         try {
-            VFutures.sync(vDatabase.create(Syncbase.getVContext(), Syncbase.defaultPerms()));
+            VFutures.sync(mVDatabase.create(Syncbase.getVContext(), Syncbase.defaultPerms()));
         } catch (ExistException e) {
             // Database already exists, presumably from a previous run of the app.
         } catch (VException e) {
             throw new RuntimeException("Failed to create database", e);
         }
+    }
+
+    protected Database(io.v.v23.syncbase.Database vDatabase) {
+        super(vDatabase);
         mVDatabase = vDatabase;
     }
 
-    public Id getId() {
-        return new Id(mVDatabase.id());
-    }
-
     public Collection collection(String name, CollectionOptions opts) {
-        // TODO(sadovsky): If !opts.withoutSyncgroup, create syncgroup and update userdata syncgroup.
-        return new Collection(this, mVDatabase.getCollection(new io.v.v23.services.syncbase.Id(Syncbase.getPersonalBlessingString(), name)), true);
-    }
-
-    protected static Collection getCollectionImpl(Database database, DatabaseCore vDbCore, Id id) {
-        // TODO(sadovsky): Consider throwing an exception or returning null if the collection does
-        // not exist.
-        return new Collection(database, vDbCore.getCollection(id.toVId()), false);
-    }
-
-    public Collection getCollection(Id id) {
-        return getCollectionImpl(this, mVDatabase, id);
-    }
-
-    // Exposed as a static function so that the implementation can be shared between Database and
-    // BatchDatabase.
-    protected static Iterator<Collection> getCollectionsImpl(Database database, DatabaseCore vDbCore) {
-        List<io.v.v23.services.syncbase.Id> vIds;
-        try {
-            vIds = VFutures.sync(vDbCore.listCollections(Syncbase.getVContext()));
-        } catch (VException e) {
-            throw new RuntimeException("listCollections failed", e);
-        }
-        ArrayList<Collection> cxs = new ArrayList<>(vIds.size());
-        for (io.v.v23.services.syncbase.Id vId : vIds) {
-            cxs.add(new Collection(database, vDbCore.getCollection(vId), false));
-        }
-        return cxs.iterator();
-    }
-
-    public Iterator<Collection> getCollections() {
-        return getCollectionsImpl(this, mVDatabase);
+        // TODO(sadovsky): If !opts.withoutSyncgroup, create syncgroup and update userdata
+        // syncgroup.
+        Collection res = getCollection(new Id(Syncbase.getPersonalBlessingString(), name));
+        res.createIfMissing();
+        return res;
     }
 
     public static class SyncgroupOptions {
@@ -72,24 +44,50 @@
 
     // FOR ADVANCED USERS. Creates syncgroup and adds it to the user's "userdata" collection, as
     // needed. Idempotent.
-    public Syncgroup syncgroup(String name, Collection[] collections, SyncgroupOptions opts) {
-        throw new RuntimeException("Not implemented");
+    public Syncgroup syncgroup(String name, List<Collection> collections, SyncgroupOptions opts) {
+        Id id = new Id(collections.get(0).getId().getBlessing(), name);
+        Syncgroup res = new Syncgroup(mVDatabase.getSyncgroup(id.toVId()), this, id);
+        res.createIfMissing(collections);
+        return res;
     }
 
+    public Syncgroup syncgroup(String name, List<Collection> collections) {
+        return syncgroup(name, collections, new SyncgroupOptions());
+    }
+
+
     public Syncgroup getSyncgroup(Id id) {
         // TODO(sadovsky): Consider throwing an exception or returning null if the syncgroup does
         // not exist.
-        return new Syncgroup(id, mVDatabase.getSyncgroup(id.toVId()));
+        return new Syncgroup(mVDatabase.getSyncgroup(id.toVId()), this, id);
     }
 
     public Iterator<Syncgroup> getSyncgroups() {
-        throw new RuntimeException("Not implemented");
+        List<io.v.v23.services.syncbase.Id> vIds;
+        try {
+            vIds = VFutures.sync(mVDatabase.listSyncgroups(Syncbase.getVContext()));
+        } catch (VException e) {
+            throw new RuntimeException("listSyncgroups failed", e);
+        }
+        ArrayList<Syncgroup> sgs = new ArrayList<>(vIds.size());
+        for (io.v.v23.services.syncbase.Id vId : vIds) {
+            sgs.add(new Syncgroup(mVDatabase.getSyncgroup(vId), this, new Id(vId)));
+        }
+        return sgs.iterator();
     }
 
     public static class AddSyncgroupInviteHandlerOptions {
         // TODO(sadovsky): Fill this in.
     }
 
+    public abstract class SyncgroupInviteHandler {
+        void onInvite(SyncgroupInvite invite) {
+        }
+
+        void onError(Exception e) {
+        }
+    }
+
     // Notifies 'h' of any existing syncgroup invites, and of all subsequent new invites.
     public void addSyncgroupInviteHandler(SyncgroupInviteHandler h, AddSyncgroupInviteHandlerOptions opts) {
         throw new RuntimeException("Not implemented");
@@ -125,6 +123,14 @@
         }
     }
 
+    public interface BatchOperation {
+        void run(BatchDatabase db);
+    }
+
+    public void runInBatch(BatchOperation op, BatchOptions opts) {
+        throw new RuntimeException("Not implemented");
+    }
+
     public BatchDatabase beginBatch(BatchOptions opts) {
         io.v.v23.syncbase.BatchDatabase vBatchDatabase;
         try {
@@ -132,13 +138,29 @@
         } catch (VException e) {
             throw new RuntimeException("beginBatch failed", e);
         }
-        return new BatchDatabase(this, vBatchDatabase);
+        return new BatchDatabase(vBatchDatabase);
     }
 
     public static class AddWatchChangeHandlerOptions {
         public byte[] resumeMarker;
     }
 
+    public abstract class WatchChangeHandler {
+        // TODO(sadovsky): Consider adopting Aaron's suggestion of combining onInitialState and
+        // onChangeBatch into a single method, to make things simpler for developers who don't want to
+        // apply deltas to their in-memory data structures:
+        // void onChangeBatch(Iterator<WatchChange> values, Iterator<WatchChange> changes)
+
+        void onInitialState(Iterator<WatchChange> values) {
+        }
+
+        void onChangeBatch(Iterator<WatchChange> changes) {
+        }
+
+        void onError(Exception e) {
+        }
+    }
+
     // Notifies 'h' of initial state, and of all subsequent changes to this database.
     // Note: Eventually we'll add a watch variant that takes a query, where the query can be
     // constructed using some sort of query builder API.
diff --git a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
index bf986a6..a18c721 100644
--- a/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
+++ b/syncbase/src/main/java/io/v/syncbase/DatabaseHandle.java
@@ -4,18 +4,52 @@
 
 package io.v.syncbase;
 
+import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
 
-public interface DatabaseHandle {
-    Id getId();
+import io.v.v23.VFutures;
+import io.v.v23.syncbase.DatabaseCore;
+import io.v.v23.verror.VException;
+
+public abstract class DatabaseHandle {
+    protected DatabaseCore mVDatabaseCore;
+
+    protected DatabaseHandle(DatabaseCore vDatabaseCore) {
+        mVDatabaseCore = vDatabaseCore;
+    }
+
+    Id getId() {
+        return new Id(mVDatabaseCore.id());
+    }
 
     class CollectionOptions {
         public boolean withoutSyncgroup;
     }
 
-    Collection collection(String name, CollectionOptions opts);
+    abstract Collection collection(String name, CollectionOptions opts);
 
-    Collection getCollection(Id id);
+    Collection collection(String name) {
+        return collection(name, new CollectionOptions());
+    }
 
-    Iterator<Collection> getCollections();
+    Collection getCollection(Id id) {
+        // TODO(sadovsky): Consider throwing an exception or returning null if the collection does
+        // not exist.
+        return new Collection(mVDatabaseCore.getCollection(id.toVId()), this);
+    }
+
+    Iterator<Collection> getCollections() {
+        List<io.v.v23.services.syncbase.Id> vIds;
+        try {
+            vIds = VFutures.sync(mVDatabaseCore.listCollections(Syncbase.getVContext()));
+        } catch (VException e) {
+            throw new RuntimeException("listCollections failed", e);
+        }
+        ArrayList<Collection> cxs = new ArrayList<>(vIds.size());
+        for (io.v.v23.services.syncbase.Id vId : vIds) {
+            cxs.add(new Collection(mVDatabaseCore.getCollection(vId), this));
+        }
+        return cxs.iterator();
+    }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Id.java b/syncbase/src/main/java/io/v/syncbase/Id.java
index d34e1a7..4908bfb 100644
--- a/syncbase/src/main/java/io/v/syncbase/Id.java
+++ b/syncbase/src/main/java/io/v/syncbase/Id.java
@@ -8,6 +8,15 @@
     private final String mBlessing;
     private final String mName;
 
+    protected Id(String blessing, String name) {
+        mBlessing = blessing;
+        mName = name;
+    }
+
+    protected String getBlessing() {
+        return mBlessing;
+    }
+
     // TODO(sadovsky): Eliminate the code below once we've switched to io.v.syncbase.core.
 
     protected Id(io.v.v23.services.syncbase.Id id) {
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index dfd9547..c3624ac 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -71,11 +71,12 @@
     }
 
     // TODO(sadovsky): Some of these constants should become fields in DatabaseOptions.
-    private static final String
+    protected static final String
             PROXY = "proxy",
             DEFAULT_BLESSING_STRING_PREFIX = "dev.v.io:o:608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com:",
             MOUNT_POINT = "/ns.dev.v.io:8101/tmp/todos/users/",
-            CLOUD_BLESSING_STRING = "dev.v.io:u:alexfandrianto@google.com";
+            CLOUD_BLESSING_STRING = "dev.v.io:u:alexfandrianto@google.com",
+            CLOUD_NAME = MOUNT_POINT + "cloud";
 
     private static Database startSyncbaseAndInitDatabase(VContext ctx) {
         SyncbaseService s;
@@ -85,7 +86,9 @@
             throw new RuntimeException("Failed to start Syncbase", e);
         }
         // Create database, if needed.
-        return new Database(s.getDatabase(getVContext(), DB_NAME, null));
+        Database res = new Database(s.getDatabase(getVContext(), DB_NAME, null));
+        res.createIfMissing();
+        return res;
     }
 
     private static String startSyncbase(VContext vContext, String rootDir)
@@ -118,7 +121,7 @@
         return getEmailFromBlessingString(blessings.toString());
     }
 
-    private static String getEmailFromBlessingPattern(BlessingPattern pattern) {
+    protected static String getEmailFromBlessingPattern(BlessingPattern pattern) {
         return getEmailFromBlessingString(pattern.toString());
     }
 
@@ -131,6 +134,10 @@
         return DEFAULT_BLESSING_STRING_PREFIX + email;
     }
 
+    protected static BlessingPattern getBlessingPatternFromEmail(String email) {
+        return new BlessingPattern(getBlessingStringFromEmail(email));
+    }
+
     private static Blessings getPersonalBlessings() {
         return V.getPrincipal(getVContext()).blessingStore().defaultBlessings();
     }
@@ -147,21 +154,23 @@
 
     protected static Permissions defaultPerms() {
         // TODO(sadovsky): Revisit these default perms, which were copied from the Todos app.
-        io.v.v23.security.access.AccessList openAccessList =
+
+
+        io.v.v23.security.access.AccessList anyone =
                 new io.v.v23.security.access.AccessList(
                         ImmutableList.of(
                                 new BlessingPattern("...")),
                         ImmutableList.<String>of());
-        io.v.v23.security.access.AccessList accessList =
+        io.v.v23.security.access.AccessList selfAndCloud =
                 new io.v.v23.security.access.AccessList(
                         ImmutableList.of(
                                 new BlessingPattern(getPersonalBlessingString()),
                                 new BlessingPattern(CLOUD_BLESSING_STRING)),
                         ImmutableList.<String>of());
         return new Permissions(ImmutableMap.of(
-                Constants.RESOLVE.getValue(), openAccessList,
-                Constants.READ.getValue(), accessList,
-                Constants.WRITE.getValue(), accessList,
-                Constants.ADMIN.getValue(), accessList));
+                Constants.RESOLVE.getValue(), anyone,
+                Constants.READ.getValue(), selfAndCloud,
+                Constants.WRITE.getValue(), selfAndCloud,
+                Constants.ADMIN.getValue(), selfAndCloud));
     }
 }
\ No newline at end of file
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
index 4a5009c..8be7b05 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
@@ -4,14 +4,51 @@
 
 package io.v.syncbase;
 
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import io.v.v23.VFutures;
+import io.v.v23.services.syncbase.SyncgroupMemberInfo;
+import io.v.v23.services.syncbase.SyncgroupSpec;
+import io.v.v23.verror.ExistException;
+import io.v.v23.verror.VException;
+
 public class Syncgroup {
+    private final Database mDatabase;
     private final Id mId;
     private final io.v.v23.syncbase.Syncgroup mVSyncgroup;
 
+    protected void createIfMissing(List<Collection> collections) {
+        ArrayList<io.v.v23.services.syncbase.Id> cxVIds = new ArrayList<>(collections.size());
+        for (Collection cx : collections) {
+            cxVIds.add(cx.getId().toVId());
+        }
+        SyncgroupSpec spec = new SyncgroupSpec(
+                "", Syncbase.CLOUD_NAME, Syncbase.defaultPerms(), cxVIds,
+                ImmutableList.of(Syncbase.MOUNT_POINT), false);
+        SyncgroupMemberInfo info = new SyncgroupMemberInfo();
+        // TODO(sadovsky): Still have no idea how to set sync priority.
+        info.setSyncPriority((byte) 3);
+        try {
+            VFutures.sync(mVSyncgroup.create(Syncbase.getVContext(), spec, info));
+        } catch (ExistException e) {
+            // Syncgroup already exists.
+            // TODO(sadovsky): Verify that the existing syncgroup has the specified configuration,
+            // e.g. the specified collections?
+        } catch (VException e) {
+            throw new RuntimeException("Failed to create collection", e);
+        }
+    }
+
     // Note, we take 'id' because io.v.v23.syncbase.Syncgroup is missing a 'getId' method.
-    protected Syncgroup(Id id, io.v.v23.syncbase.Syncgroup vSyncgroup) {
-        mId = id;
+    protected Syncgroup(io.v.v23.syncbase.Syncgroup vSyncgroup, Database database, Id id) {
         mVSyncgroup = vSyncgroup;
+        mDatabase = database;
+        mId = id;
     }
 
     public Id getId() {
@@ -19,7 +56,13 @@
     }
 
     public AccessList getAccessList() {
-        throw new RuntimeException("Not implemented");
+        Map<String, SyncgroupSpec> versionedSpec;
+        try {
+            versionedSpec = VFutures.sync(mVSyncgroup.getSpec(Syncbase.getVContext()));
+        } catch (VException e) {
+            throw new RuntimeException("getSpec failed", e);
+        }
+        return new AccessList(versionedSpec.values().iterator().next().getPerms());
     }
 
     public static class UpdateAccessListOptions {
@@ -29,24 +72,63 @@
     // The following methods update the AccessList for the syncgroup and its associated collections.
     // Setting opts.syncgroupOnly makes it so these methods only update the AccessList for the
     // syncgroup.
-    public void addUsers(User[] users, AccessList.AccessLevel level, UpdateAccessListOptions opts) {
+    public void addUsers(List<User> users, AccessList.AccessLevel level, UpdateAccessListOptions opts) {
         AccessList delta = new AccessList();
-        for (User u: users) {
+        for (User u : users) {
             delta.users.put(u.getId(), level);
         }
         updateAccessList(delta, opts);
     }
 
-    public void removeUsers(User[] users, UpdateAccessListOptions opts) {
+    public void addUsers(List<User> users, AccessList.AccessLevel level) {
+        addUsers(users, level, new UpdateAccessListOptions());
+    }
+
+    public void addUser(User user, AccessList.AccessLevel level) {
+        addUsers(Collections.singletonList(user), level);
+    }
+
+    public void removeUsers(List<User> users, UpdateAccessListOptions opts) {
         AccessList delta = new AccessList();
-        for (User u: users) {
+        for (User u : users) {
             delta.users.put(u.getId(), null);
         }
         updateAccessList(delta, opts);
     }
 
+    public void removeUsers(List<User> users) {
+        removeUsers(users, new UpdateAccessListOptions());
+    }
+
+    public void removeUser(User user) {
+        removeUsers(Collections.singletonList(user));
+    }
+
     // Applies 'delta' to the AccessList. Note, NULL enum means "remove".
-    public void updateAccessList(AccessList delta, UpdateAccessListOptions opts) {
-        throw new RuntimeException("Not implemented");
+    public void updateAccessList(final AccessList delta, UpdateAccessListOptions opts) {
+        // TODO(sadovsky): Make it so SyncgroupSpec can be updated as part of a batch?
+        Map<String, SyncgroupSpec> versionedSpec;
+        try {
+            versionedSpec = VFutures.sync(mVSyncgroup.getSpec(Syncbase.getVContext()));
+        } catch (VException e) {
+            throw new RuntimeException("getSpec failed", e);
+        }
+        String version = versionedSpec.keySet().iterator().next();
+        SyncgroupSpec spec = versionedSpec.values().iterator().next();
+        AccessList.applyDelta(spec.getPerms(), delta);
+        try {
+            VFutures.sync(mVSyncgroup.setSpec(Syncbase.getVContext(), spec, version));
+        } catch (VException e) {
+            throw new RuntimeException("setSpec failed", e);
+        }
+        final List<io.v.v23.services.syncbase.Id> cxVIds = spec.getCollections();
+        mDatabase.runInBatch(new Database.BatchOperation() {
+            @Override
+            public void run(BatchDatabase db) {
+                for (io.v.v23.services.syncbase.Id vId : cxVIds) {
+                    db.getCollection(new Id(vId)).updateAccessList(delta);
+                }
+            }
+        }, new Database.BatchOptions());
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/SyncgroupInviteHandler.java b/syncbase/src/main/java/io/v/syncbase/SyncgroupInviteHandler.java
deleted file mode 100644
index 3a95721..0000000
--- a/syncbase/src/main/java/io/v/syncbase/SyncgroupInviteHandler.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2016 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package io.v.syncbase;
-
-// TODO(sadovsky): Make this a nested class of Database?
-public abstract class SyncgroupInviteHandler {
-    void onInvite(SyncgroupInvite invite) {
-
-    }
-
-    void onError(Exception e) {
-
-    }
-}
diff --git a/syncbase/src/main/java/io/v/syncbase/WatchChangeHandler.java b/syncbase/src/main/java/io/v/syncbase/WatchChangeHandler.java
deleted file mode 100644
index c8d27b6..0000000
--- a/syncbase/src/main/java/io/v/syncbase/WatchChangeHandler.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2016 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package io.v.syncbase;
-
-import java.util.Iterator;
-
-// TODO(sadovsky): Make this a nested class of Database?
-public abstract class WatchChangeHandler {
-    // TODO(sadovsky): Consider adopting Aaron's suggestion of combining onInitialState and
-    // onChangeBatch into a single method, to make things simpler for developers who don't want to
-    // apply deltas to their in-memory data structures:
-    // void onChangeBatch(Iterator<WatchChange> values, Iterator<WatchChange> changes)
-
-    void onInitialState(Iterator<WatchChange> values) {
-
-    }
-
-    void onChangeBatch(Iterator<WatchChange> changes) {
-
-    }
-
-    void onError(Exception e) {
-
-    }
-}
diff --git a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
index 0e71c91..123aeb3 100644
--- a/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -11,6 +11,7 @@
     public void createDatabase() {
         Syncbase.DatabaseOptions opts = new Syncbase.DatabaseOptions();
         opts.rootDir = "/tmp";
-        Syncbase.database(opts);
+        // TODO(sadovsky): Restore this once we figure out what Vanadium context to pass.
+        // Syncbase.database(opts);
     }
 }
\ No newline at end of file