java/syncbase: Fix CollectionRowPattern to escape string patterns

We need to escape the strings because % and _  have special meaning for
CollectionRowPattern. It uses SQL LIKE syntax, and we use \ as an
escape character.

Similarly, we needed to rename the internal prefixes of userdata.

Change-Id: I3778d8239ad49500842e1b9090384e08c056371f
diff --git a/syncbase/build.gradle b/syncbase/build.gradle
index 484f3bd..1b90633 100644
--- a/syncbase/build.gradle
+++ b/syncbase/build.gradle
@@ -26,7 +26,7 @@
 
 // You should update this after releasing a new version of the Syncbase API. See the
 // list of published versions at: https://repo1.maven.org/maven2/io/v/syncbase
-version = '0.1.8'
+version = '0.1.9'
 group = 'io.v'
 
 def siteUrl = 'https://github.com/vanadium/java/syncbase'
diff --git a/syncbase/src/main/java/io/v/syncbase/Database.java b/syncbase/src/main/java/io/v/syncbase/Database.java
index 6232319..47b1034 100644
--- a/syncbase/src/main/java/io/v/syncbase/Database.java
+++ b/syncbase/src/main/java/io/v/syncbase/Database.java
@@ -396,14 +396,14 @@
         final String name;
         final String blessing;
         final String row;
-        final boolean showUserdataCollectionRow;
+        final boolean showUserdataInternalRow;
 
         AddWatchChangeHandlerOptions(Builder builder) {
             resumeMarker = builder.resumeMarker;
             name = builder.name;
             blessing = builder.blessing;
             row = builder.row;
-            showUserdataCollectionRow = builder.showUserdataCollectionRow;
+            showUserdataInternalRow = builder.showUserdataCollectionRow;
         }
 
         CollectionRowPattern getCollectionRowPattern() {
@@ -425,27 +425,23 @@
             }
 
             public Builder setCollectionNamePrefix(String prefix) {
-                // TODO(alexfandrianto): Unsafe. The prefix was not escaped. Incorrect if it has a
-                // trailing backslash.
-                name = prefix + WILDCARD;
+                name = escapePattern(prefix) + WILDCARD;
                 return this;
             }
 
             public Builder setCollectionId(Id id) {
-                name = id.getName();
-                blessing = id.getBlessing();
+                name = escapePattern(id.getName());
+                blessing = escapePattern(id.getBlessing());
                 return this;
             }
 
             public Builder setRowKeyPrefix(String prefix) {
-                // TODO(alexfandrianto): Unsafe. The prefix was not escaped. Incorrect if it has a
-                // trailing backslash.
-                row = prefix + WILDCARD;
+                row = escapePattern(prefix) + WILDCARD;
                 return this;
             }
 
             public Builder setRowKey(String rowKey) {
-                row = rowKey;
+                row = escapePattern(rowKey);
                 return this;
             }
 
@@ -454,6 +450,12 @@
                 return this;
             }
 
+            private String escapePattern(String s) {
+                // Replace '\', '%', and '_' literals with a '\\', '\%', and '\_' respectively.
+                // The reason is that we use SQL LIKE syntax in Go.
+                return s.replaceAll("[\\\\%_]", "\\\\$0");
+            }
+
             public AddWatchChangeHandlerOptions build() {
                 return new AddWatchChangeHandlerOptions(this);
             }
@@ -511,12 +513,12 @@
                     public void onChange(io.v.syncbase.core.WatchChange coreWatchChange) {
                         boolean isRoot = coreWatchChange.entityType ==
                                 io.v.syncbase.core.WatchChange.EntityType.ROOT;
-                        boolean isUserdataCollectionRow =
+                        boolean isUserdataInternalRow =
                                 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 && (opts.showUserdataCollectionRow || !isUserdataCollectionRow)) {
+                                coreWatchChange.row.startsWith(Syncbase.USERDATA_INTERNAL_PREFIX);
+                        if (!isRoot && (opts.showUserdataInternalRow || !isUserdataInternalRow)) {
                             mBatch.add(new WatchChange(coreWatchChange));
                         }
                         if (!coreWatchChange.continued) {
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncbase.java b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
index 89d27f8..c8a586f 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncbase.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncbase.java
@@ -36,8 +36,8 @@
 /**
  * The "userdata" collection is a per-user collection (and associated syncgroup) for data that
  * should automatically get synced across a given user's devices. It has the following schema:
- * - /syncgroups/{encodedSyncgroupId} -> null
- * - /ignoredInvites/{encodedSyncgroupId} -> null
+ * - internal__/syncgroups/{encodedSyncgroupId} -> null
+ * - internal__/ignoredInvites/{encodedSyncgroupId} -> null
  */
 
 /**
@@ -219,11 +219,11 @@
     static final String
             TAG = "syncbase",
             DIR_NAME = "syncbase",
-            DB_NAME = "db";
+            DB_NAME = "db",
+            USERDATA_INTERNAL_PREFIX = "internal__/",
+            USERDATA_INTERNAL_SYNCGROUP_PREFIX = USERDATA_INTERNAL_PREFIX + "syncgroups";
 
-    public static final String
-            USERDATA_NAME = "userdata__",
-            USERDATA_COLLECTION_PREFIX = "__collections/";
+    public static final String USERDATA_NAME = "userdata__";
 
     private static Map selfAndCloud() throws SyncbaseException {
         List<String> inList = sOpts.mCloudAdmin == null
@@ -401,10 +401,10 @@
                 if (watchChange.getCollectionId().getName().equals(USERDATA_NAME) &&
                         watchChange.getEntityType() == WatchChange.EntityType.ROW &&
                         watchChange.getChangeType() == WatchChange.ChangeType.PUT &&
-                        watchChange.getRowKey().startsWith(USERDATA_COLLECTION_PREFIX)) {
+                        watchChange.getRowKey().startsWith(USERDATA_INTERNAL_SYNCGROUP_PREFIX)) {
                     try {
                         String encodedId = watchChange.getRowKey().
-                                substring(Syncbase.USERDATA_COLLECTION_PREFIX.length());
+                                substring(Syncbase.USERDATA_INTERNAL_SYNCGROUP_PREFIX.length());
                         sDatabase.getSyncgroup(Id.decode(encodedId)).join();
                     } catch (VError vError) {
                         vError.printStackTrace();
@@ -417,7 +417,7 @@
 
     static void addToUserdata(Id id) throws SyncbaseException {
         sDatabase.getUserdataCollection().
-                put(Syncbase.USERDATA_COLLECTION_PREFIX + id.encode(), true);
+                put(Syncbase.USERDATA_INTERNAL_SYNCGROUP_PREFIX + id.encode(), true);
     }
 
     /**
diff --git a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
index ea9ee01..a906059 100644
--- a/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
+++ b/syncbase/src/main/java/io/v/syncbase/Syncgroup.java
@@ -167,25 +167,25 @@
     public void updateAccessList(final AccessList delta, UpdateAccessListOptions opts)
             throws SyncbaseException {
         try {
-
             // TODO(sadovsky): Make it so SyncgroupSpec can be updated as part of a batch?
             VersionedSyncgroupSpec versionedSyncgroupSpec = mCoreSyncgroup.getSpec();
             versionedSyncgroupSpec.syncgroupSpec.permissions = AccessList.applyDeltaForSyncgroup(
                     versionedSyncgroupSpec.syncgroupSpec.permissions, delta);
             mCoreSyncgroup.setSpec(versionedSyncgroupSpec);
-            // TODO(sadovsky): There's a race here - it's possible for a collection to get destroyed
-            // after getSpec() but before db.getCollection().
-            final List<io.v.syncbase.core.Id> collectionsIds =
-                    versionedSyncgroupSpec.syncgroupSpec.collections;
-            mDatabase.runInBatch(new Database.BatchOperation() {
-                @Override
-                public void run(BatchDatabase db) throws SyncbaseException {
-                    for (io.v.syncbase.core.Id id : collectionsIds) {
-                        db.getCollection(new Id(id)).updateAccessList(delta);
+            if (!opts.syncgroupOnly) {
+                // TODO(sadovsky): There's a race here - it's possible for a collection to get
+                // destroyed after getSpec() but before db.getCollection().
+                final List<io.v.syncbase.core.Id> collectionsIds =
+                        versionedSyncgroupSpec.syncgroupSpec.collections;
+                mDatabase.runInBatch(new Database.BatchOperation() {
+                    @Override
+                    public void run(BatchDatabase db) throws SyncbaseException {
+                        for (io.v.syncbase.core.Id id : collectionsIds) {
+                            db.getCollection(new Id(id)).updateAccessList(delta);
+                        }
                     }
-                }
-            });
-
+                });
+            }
         } catch (VError e) {
             chainThrow("updating access list of syncgroup", getId().getName(), e);
         }
diff --git a/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java b/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java
index ef21baa..b9b1490 100644
--- a/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java
+++ b/syncbase/src/test/java/io/v/syncbase/DatabaseTest.java
@@ -18,6 +18,7 @@
 import io.v.syncbase.exception.SyncbaseException;
 
 import static io.v.syncbase.TestUtil.setUpDatabase;
+import static org.junit.Assert.assertEquals;
 
 public class DatabaseTest {
     @Rule
@@ -45,4 +46,73 @@
         Syncbase.database().syncgroup("aSyncgroup", new ArrayList<Collection>());
     }
 
+    @Test
+    public void testWatchChangeHandlerOptionsBuilder() {
+        Database.AddWatchChangeHandlerOptions opts;
+
+        // Wildcard and prefix tests.
+        opts = new Database.AddWatchChangeHandlerOptions.Builder()
+                        .build();
+        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, "%");
+
+        opts = new Database.AddWatchChangeHandlerOptions.Builder()
+                .setCollectionNamePrefix("c")
+                .build();
+        assertEquals(opts.blessing, "%");
+        assertEquals(opts.name, "c%");
+        assertEquals(opts.row, "%");
+
+        opts = new Database.AddWatchChangeHandlerOptions.Builder()
+                .setRowKey("d")
+                .build();
+        assertEquals(opts.blessing, "%");
+        assertEquals(opts.name, "%");
+        assertEquals(opts.row, "d");
+
+        opts = new Database.AddWatchChangeHandlerOptions.Builder()
+                .setRowKeyPrefix("e")
+                .build();
+        assertEquals(opts.blessing, "%");
+        assertEquals(opts.name, "%");
+        assertEquals(opts.row, "e%");
+
+        // Escaping tests. %, _ and \ are special characters.
+        opts = new Database.AddWatchChangeHandlerOptions.Builder()
+                .setCollectionId(new Id("%", "_"))
+                .build();
+        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, "%");
+
+        opts = new Database.AddWatchChangeHandlerOptions.Builder()
+                .setRowKey("%%")
+                .build();
+        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, "\\_\\\\\\_%");
+    }
+
 }