syncbase java: start implementing high-level API in terms of low-level

Change-Id: I9eb753bfdf363ccd8056c322c27514a47fad83a7
diff --git a/syncbase/build.gradle b/syncbase/build.gradle
index 5130e92..33c0281 100644
--- a/syncbase/build.gradle
+++ b/syncbase/build.gradle
@@ -13,11 +13,31 @@
     }
 }
 
-apply plugin: 'java'
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion "23.0.3"
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 23
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
 
 dependencies {
     compile fileTree(dir: 'libs', include: ['*.jar'])
-    compile 'junit:junit:4.12'
+    testCompile 'junit:junit:4.12'
+    compile 'com.android.support:appcompat-v7:23.4.0'
+    compile 'io.v:vanadium-android:2.1.3'
 }
 
 task clean(type: Delete) {
diff --git a/syncbase/src/main/AndroidManifest.xml b/syncbase/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..36846f6
--- /dev/null
+++ b/syncbase/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest package="io.v.syncbase">
+
+</manifest>
diff --git a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
index 9237191..910a32a 100644
--- a/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
+++ b/syncbase/src/main/java/io/v/syncbase/BatchDatabase.java
@@ -8,26 +8,26 @@
 
 public class BatchDatabase implements DatabaseHandle {
     public Id getId() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public Collection collection(String name, CollectionOptions opts) {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public Collection getCollection(Id id) {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public Iterator<Collection> getCollections() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public void commit() {
-
+        throw new RuntimeException("Not implemented");
     }
 
     public void abort() {
-
+        throw new RuntimeException("Not implemented");
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Collection.java b/syncbase/src/main/java/io/v/syncbase/Collection.java
index 9f9062e..b68e295 100644
--- a/syncbase/src/main/java/io/v/syncbase/Collection.java
+++ b/syncbase/src/main/java/io/v/syncbase/Collection.java
@@ -5,40 +5,47 @@
 package io.v.syncbase;
 
 public class Collection {
+    private final io.v.v23.syncbase.Collection mCxImpl;
+
+    protected Collection(io.v.v23.syncbase.Collection cxImpl) {
+        // TODO(sadovsky): Add create-if-not-exists code.
+        mCxImpl = cxImpl;
+    }
+
     public Id getId() {
-        return null;
+        return new Id(mCxImpl.id());
     }
 
     // Shortcut for Database.getSyncgroup(c.getId()), helpful for the common case of one syncgroup
     // per collection.
     public Syncgroup getSyncgroup() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     // TODO(sadovsky): Maybe add scan API, if developers aren't satisfied with watch.
     public <T> T get(String key) {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public boolean exists(String key) {
-        return false;
+        throw new RuntimeException("Not implemented");
     }
 
     public <T> void put(String key, T value) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     public void delete(String key) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     // FOR ADVANCED USERS. The following methods manipulate the AccessList for this collection, but
     // not for associated syncgroups.
     public AccessList getAccessList() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public void updateAccessList(AccessList delta) {
-
+        throw new RuntimeException("Not implemented");
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index 880b123..c83492e5 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -7,20 +7,28 @@
 import java.util.Iterator;
 
 public class Database implements DatabaseHandle {
+    private final io.v.v23.syncbase.Database mDbImpl;
+
+    protected Database(io.v.v23.syncbase.Database dbImpl) {
+        mDbImpl = dbImpl;
+    }
+
     public Id getId() {
-        return null;
+        return new Id(mDbImpl.id());
     }
 
     public Collection collection(String name, CollectionOptions opts) {
-        return null;
+        return new Collection(mDbImpl.getCollection(new io.v.v23.services.syncbase.Id(Syncbase.getPersonalBlessingString(), name)));
     }
 
     public Collection getCollection(Id id) {
-        return null;
+        return new Collection(mDbImpl.getCollection(id.toVId()));
     }
 
     public Iterator<Collection> getCollections() {
-        return null;
+        // FIXME: Convert ListenableFuture<List<Id>> to Iterator<Collection>.
+        mDbImpl.listCollections(Syncbase.getVContext());
+        throw new RuntimeException("Not implemented");
     }
 
     class SyncgroupOptions {
@@ -30,15 +38,15 @@
     // 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) {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public Syncgroup getSyncgroup(Id id) {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public Iterator<Syncgroup> getSyncgroups() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public class AddSyncgroupInviteHandlerOptions {
@@ -47,15 +55,15 @@
 
     // 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");
     }
 
     public void removeSyncgroupInviteHandler(SyncgroupInviteHandler h) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     public void removeSyncgroupInviteHandlers() {
-
+        throw new RuntimeException("Not implemented");
     }
 
     // Joins syncgroup and adds it to the user's "userdata" collection, as needed.
@@ -63,17 +71,17 @@
     // "ignore" methods to the SyncgroupInvite class, or should we treat it as a POJO?
     // TODO(sadovsky): Make this method async.
     public Syncgroup acceptSyncgroupInvite(SyncgroupInvite invite) {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     // Records that the user has ignored this invite, such that it's never surfaced again.
     // Note: This will be one of the last things we implement.
     public void ignoreSyncgroupInvite(SyncgroupInvite invite) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     public BatchDatabase beginBatch() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public class AddWatchChangeHandlerOptions {
@@ -85,14 +93,14 @@
     // 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.
     public void addWatchChangeHandler(WatchChangeHandler h, AddWatchChangeHandlerOptions opts) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     public void removeWatchChangeHandler(WatchChangeHandler h) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     public void removeAllWatchChangeHandlers() {
-
+        throw new RuntimeException("Not implemented");
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Id.java b/syncbase/src/main/java/io/v/syncbase/Id.java
index 2cae7cb..9213824 100644
--- a/syncbase/src/main/java/io/v/syncbase/Id.java
+++ b/syncbase/src/main/java/io/v/syncbase/Id.java
@@ -5,5 +5,18 @@
 package io.v.syncbase;
 
 public class Id {
-    // TODO(sadovsky): Fill this in.
+    // TODO(sadovsky): Fill this in further.
+    private String mBlessing;
+    private String mName;
+
+    // TODO(sadovsky): Eliminate the code below once we've switched to io.v.syncbase.core.
+
+    protected Id(io.v.v23.services.syncbase.Id id) {
+        mBlessing = id.getBlessing();
+        mName = id.getName();
+    }
+
+    protected io.v.v23.services.syncbase.Id toVId() {
+        return new io.v.v23.services.syncbase.Id(mBlessing, mName);
+    }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index e5e23ef..f3340f1 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -4,19 +4,179 @@
 
 package io.v.syncbase;
 
+import android.util.Log;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import java.io.File;
+
+import io.v.android.VAndroidContext;
+import io.v.android.v23.V;
+import io.v.impl.google.services.syncbase.SyncbaseServer;
+import io.v.v23.VFutures;
+import io.v.v23.context.VContext;
+import io.v.v23.rpc.Server;
+import io.v.v23.security.BlessingPattern;
+import io.v.v23.security.Blessings;
+import io.v.v23.security.access.Constants;
+import io.v.v23.security.access.Permissions;
+import io.v.v23.syncbase.SyncbaseService;
+import io.v.v23.verror.ExistException;
+import io.v.v23.verror.VException;
+
 public class Syncbase {
-    public class DatabaseOptions {
-        // TODO(sadovsky): Fill this in.
+    public static class DatabaseOptions {
+        // TODO(sadovsky): Fill this in further.
+        public String rootDir;
+        // TODO(sadovsky): Drop this once we switch from io.v.v23.syncbase to io.v.syncbase.core.
+        public VAndroidContext vAndroidContext;
     }
 
+    private static DatabaseOptions sOpts;
+    private static Database sDatabase;
+
+    private static final String
+            TAG = "syncbase",
+            DIR_NAME = "syncbase",
+            DB_NAME = "db";
+
     // Starts Syncbase if needed; creates default database if needed; reads config (e.g. cloud
     // syncbase name) from options struct; performs create-or-join for "userdata" syncgroup if
-    // needed; returns database handle. Async, and can fail.
-    // TODO(sadovsky): The create-or-join will force this method to be async, which is annoying
-    // since create-or-join will no longer be necessary once syncgroup merge is supported. Maybe
-    // move create-or-join to a separate, temporarily-necessary init function?
-    // TODO(sadovsky): Make this method async.
+    // needed; returns database handle.
+    // TODO(sadovsky): The create-or-join forces this method to be async, which is annoying since
+    // create-or-join will no longer be necessary once syncgroup merge is supported.
     public static Database database(DatabaseOptions opts) {
-        return null;
+        if (sDatabase != null) {
+            // TODO(sadovsky): Check that opts matches original opts (sOpts)?
+            return sDatabase;
+        }
+        sOpts = opts;
+        // TODO(sadovsky): Call ctx.cancel in sDatabase destructor?
+        VContext ctx = getVContext().withCancel();
+        try {
+            sDatabase = startSyncbaseAndInitDatabase(ctx);
+        } catch (Exception e) {
+            ctx.cancel();
+            throw e;
+        }
+        // TODO(sadovsky): Add create-or-join of userdata syncgroup, and make this method async.
+        return sDatabase;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    // TODO(sadovsky): Remove much of the code below once we switch from io.v.v23.syncbase to
+    // io.v.syncbase.core. Note, much of this code was copied from the Todos app.
+
+    protected static VContext getVContext() {
+        return sOpts.vAndroidContext.getVContext();
+    }
+
+    // TODO(sadovsky): Some of these constants should become fields in DatabaseOptions.
+    private 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 = "dev.v.io:u:alexfandrianto@google.com";
+
+    private static Database startSyncbaseAndInitDatabase(VContext ctx) {
+        SyncbaseService s;
+        Blessings personalBlessings = getPersonalBlessings();
+        if (personalBlessings == null) {
+            throw new RuntimeException("No blessings");
+        }
+        Permissions perms = permissionsFromBlessings(personalBlessings);
+        try {
+            s = io.v.v23.syncbase.Syncbase.newService(startSyncbase(ctx, sOpts.rootDir));
+        } catch (SyncbaseServer.StartException e) {
+            throw new RuntimeException("Failed to start Syncbase", e);
+        }
+        // Create database, if needed.
+        io.v.v23.syncbase.Database d = s.getDatabase(getVContext(), DB_NAME, null);
+        try {
+            VFutures.sync(d.create(getVContext(), null));
+        } 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);
+        }
+        return new Database(d);
+    }
+
+    private static String startSyncbase(VContext vContext, String rootDir)
+            throws SyncbaseServer.StartException {
+        try {
+            vContext = V.withListenSpec(vContext, V.getListenSpec(vContext).withProxy(PROXY));
+        } catch (VException e) {
+            Log.w(TAG, "Failed to set up Vanadium proxy", e);
+        }
+        File dir = new File(rootDir, DIR_NAME);
+        dir.mkdirs();
+        SyncbaseServer.Params params = new SyncbaseServer.Params()
+                .withStorageRootDir(dir.getAbsolutePath());
+        VContext serverContext = SyncbaseServer.withNewServer(vContext, params);
+        Server server = V.getServer(serverContext);
+        String name = server.getStatus().getEndpoints()[0].name();
+        Log.i(TAG, "Started Syncbase: " + name);
+        return name;
+    }
+
+    private static void checkHasOneBlessing(Blessings blessings) {
+        int n = blessings.getCertificateChains().size();
+        if (n != 1) {
+            throw new RuntimeException("Expected one blessing, got " + n);
+        }
+    }
+
+    private static String getEmailFromBlessings(Blessings blessings) {
+        checkHasOneBlessing(blessings);
+        return getEmailFromBlessingString(blessings.toString());
+    }
+
+    private static String getEmailFromBlessingPattern(BlessingPattern pattern) {
+        return getEmailFromBlessingString(pattern.toString());
+    }
+
+    private static String getEmailFromBlessingString(String blessingStr) {
+        String[] parts = blessingStr.split(":");
+        return parts[parts.length - 1];
+    }
+
+    private static String getBlessingStringFromEmail(String email) {
+        return DEFAULT_BLESSING_STRING_PREFIX + email;
+    }
+
+    private static Blessings getPersonalBlessings() {
+        return V.getPrincipal(getVContext()).blessingStore().defaultBlessings();
+    }
+
+    protected static String getPersonalBlessingString() {
+        Blessings blessings = getPersonalBlessings();
+        checkHasOneBlessing(blessings);
+        return blessings.toString();
+    }
+
+    private static String getPersonalEmail() {
+        return getEmailFromBlessings(getPersonalBlessings());
+    }
+
+    protected static Permissions permissionsFromBlessings(Blessings blessings) {
+        // TODO(sadovsky): Revisit these default perms, which were copied from the Todos app.
+        io.v.v23.security.access.AccessList openAccessList =
+                new io.v.v23.security.access.AccessList(
+                        ImmutableList.of(
+                                new BlessingPattern("...")),
+                        ImmutableList.<String>of());
+        io.v.v23.security.access.AccessList accessList =
+                new io.v.v23.security.access.AccessList(
+                        ImmutableList.of(
+                                new BlessingPattern(blessings.toString()),
+                                new BlessingPattern(CLOUD_BLESSING)),
+                        ImmutableList.<String>of());
+        return new Permissions(ImmutableMap.of(
+                Constants.RESOLVE.getValue(), openAccessList,
+                Constants.READ.getValue(), accessList,
+                Constants.WRITE.getValue(), accessList,
+                Constants.ADMIN.getValue(), accessList));
     }
 }
\ 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 99494f3..4abc126 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
@@ -6,14 +6,14 @@
 
 public class Syncgroup {
     public Id getId() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
     public AccessList getAccessList() {
-        return null;
+        throw new RuntimeException("Not implemented");
     }
 
-    public class UpdateAccessListOptions {
+    public static class UpdateAccessListOptions {
         // TODO(sadovsky): Fill this in.
     }
 
@@ -21,15 +21,15 @@
     // Setting opts.syncgroupOnly makes it so these methods only update the AccessList for the
     // syncgroup.
     public void addUsers(User[] users, AccessLevel level, UpdateAccessListOptions opts) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     public void removeUsers(User[] users, UpdateAccessListOptions opts) {
-
+        throw new RuntimeException("Not implemented");
     }
 
     // Applies 'delta' to the AccessList. Note, NULL enum means "remove".
     public void updateAccessList(AccessList delta, UpdateAccessListOptions opts) {
-
+        throw new RuntimeException("Not implemented");
     }
 }
diff --git a/syncbase/src/main/java/io/v/syncbase/WatchChange.java b/syncbase/src/main/java/io/v/syncbase/WatchChange.java
index e728f3a..0269413 100644
--- a/syncbase/src/main/java/io/v/syncbase/WatchChange.java
+++ b/syncbase/src/main/java/io/v/syncbase/WatchChange.java
@@ -4,6 +4,18 @@
 
 package io.v.syncbase;
 
+import io.v.v23.services.watch.Change;
+
 public class WatchChange {
-    // TODO(sadovsky): Fill this in.
+    // TODO(sadovsky): Fill this in further.
+
+    // TODO(sadovsky): Eliminate the code below once we've switched to io.v.syncbase.core.
+
+    protected WatchChange(Change c) {
+        throw new RuntimeException("Not implemented");
+    }
+
+    protected Change toVChange() {
+        throw new RuntimeException("Not implemented");
+    }
 }
\ No newline at end of file
diff --git a/syncbase/src/main/java/io/v/syncbase/WatchChangeHandler.java b/syncbase/src/main/java/io/v/syncbase/WatchChangeHandler.java
index b7ef9d9..c8d27b6 100644
--- a/syncbase/src/main/java/io/v/syncbase/WatchChangeHandler.java
+++ b/syncbase/src/main/java/io/v/syncbase/WatchChangeHandler.java
@@ -14,11 +14,14 @@
     // 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
new file mode 100644
index 0000000..473e742
--- /dev/null
+++ b/syncbase/src/test/java/io/v/syncbase/SyncbaseTest.java
@@ -0,0 +1,16 @@
+// 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 org.junit.Test;
+
+public class SyncbaseTest {
+    @Test
+    public void createDatabase() {
+        Syncbase.DatabaseOptions opts = new Syncbase.DatabaseOptions();
+        opts.mRootDir = "/tmp";
+        Syncbase.database(opts);
+    }
+}
\ No newline at end of file