io.v.todos.persistence.syncbase.SyncbasePersistence.java Source code

Java tutorial

Introduction

Here is the source code for io.v.todos.persistence.syncbase.SyncbasePersistence.java

Source

// 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.todos.persistence.syncbase;

import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import android.util.Log;

import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;

import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;

import io.v.android.inspectors.RemoteInspectors;
import io.v.android.ManagedVAndroidContext;
import io.v.android.VAndroidContext;
import io.v.android.VAndroidContexts;
import io.v.android.error.ErrorReporter;
import io.v.android.error.ToastingErrorReporter;
import io.v.android.security.BlessingsManager;
import io.v.android.v23.V;
import io.v.impl.google.services.syncbase.SyncbaseServer;
import io.v.todos.R;
import io.v.todos.persistence.Persistence;
import io.v.todos.sharing.NeighborhoodFragment;
import io.v.todos.sharing.Sharing;
import io.v.v23.InputChannel;
import io.v.v23.InputChannelCallback;
import io.v.v23.InputChannels;
import io.v.v23.VFutures;
import io.v.v23.context.VContext;
import io.v.v23.naming.Endpoint;
import io.v.v23.rpc.ListenSpec;
import io.v.v23.rpc.Server;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.Blessings;
import io.v.v23.security.access.AccessList;
import io.v.v23.security.access.Constants;
import io.v.v23.security.access.Permissions;
import io.v.v23.services.syncbase.Id;
import io.v.v23.services.syncbase.SyncgroupJoinFailedException;
import io.v.v23.services.syncbase.SyncgroupMemberInfo;
import io.v.v23.services.syncbase.SyncgroupSpec;
import io.v.v23.syncbase.Collection;
import io.v.v23.syncbase.Database;
import io.v.v23.syncbase.Syncbase;
import io.v.v23.syncbase.SyncbaseService;
import io.v.v23.syncbase.Syncgroup;
import io.v.v23.syncbase.WatchChange;
import io.v.v23.syncbase.util.Util;
import io.v.v23.vdl.VdlStruct;
import io.v.v23.verror.ExistException;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;
import io.v.x.ref.lib.discovery.BadAdvertisementException;

/**
 * TODO(rosswang): Move most of this to vanadium-android.
 */
public class SyncbasePersistence implements Persistence {
    public static final String LISTS_PREFIX = "lists_";
    public static final String LIST_COLLECTION_SYNCGROUP_PREFIX = "list_";

    private static final String TAG = "SyncbasePersistence", FILENAME = "syncbase", PROXY = "proxy",
            DATABASE = "db", BLESSINGS_KEY = "blessings", USER_COLLECTION_SYNCGROUP_SUFFIX = "sg_",
            DEFAULT_APP_BLESSING_STRING = "dev.v" + ".io:o:608941808256-43vtfndets79kf5hac8ieujto8837660"
                    + ".apps.googleusercontent.com";
    protected static final long SHORT_TIMEOUT = 2500, MEMBER_TIMER_DELAY = 100, MEMBER_TIMER_PERIOD = 1000;
    public static final String USER_COLLECTION_NAME = "userdata", MOUNTPOINT = "/ns.dev.v.io:8101/tmp/todos/users/",
            CLOUD_NAME = null, // MOUNTPOINT + "cloud",
            // TODO(alexfandrianto): Restore the cloud once we can rely on it again.
            // TODO(alexfandrianto): This shouldn't be me running the cloud.
            CLOUD_BLESSING = "dev.v.io:u:alexfandrianto@google.com";

    // BlessingPattern initialization has to be deferred until after V23 init due to native binding.
    private static final Supplier<AccessList> OPEN_ACL = Suppliers.memoize(new Supplier<AccessList>() {
        @Override
        public AccessList get() {
            return new AccessList(ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of());
        }
    });

    protected static final ListeningScheduledExecutorService sExecutor = MoreExecutors
            .listeningDecorator(Executors.newScheduledThreadPool(10));

    private static final Object sVContextMutex = new Object();
    private static VAndroidContext<Context> sVAndroidContext;

    private static final Object sSyncbaseMutex = new Object();
    private static SyncbaseService sSyncbase;
    private static RemoteInspectors sRemoteInspectors;

    private static void appVInit(Context appContext) {
        synchronized (sVContextMutex) {
            if (sVAndroidContext == null) {
                sVAndroidContext = new ManagedVAndroidContext<>(appContext, new ToastingErrorReporter(appContext));
            }
        }
    }

    public static Context getAppContext() {
        return sVAndroidContext.getAndroidContext();
    }

    public static VContext getAppVContext() {
        return sVAndroidContext.getVContext();
    }

    public static ErrorReporter getAppErrorReporter() {
        return sVAndroidContext.getErrorReporter();
    }

    private static String startSyncbaseServer(VContext vContext, Context appContext, Permissions serverPermissions)
            throws SyncbaseServer.StartException {
        try {
            ListenSpec ls = V.getListenSpec(vContext);
            ListenSpec.Address[] addresses = ls.getAddresses();
            addresses = Arrays.copyOf(addresses, addresses.length + 1);
            addresses[addresses.length - 1] = new ListenSpec.Address("bt", "/0");
            ListenSpec newLs = new ListenSpec(addresses, PROXY, ls.getChooser());
            vContext = V.withListenSpec(vContext, newLs);
        } catch (VException e) {
            Log.w(TAG, "Unable to set up Vanadium proxy for Syncbase");
        }

        File storageRoot = new File(appContext.getFilesDir(), FILENAME);
        storageRoot.mkdirs();

        Log.i(TAG, "Starting Syncbase");
        SyncbaseServer.Params params = new SyncbaseServer.Params().withPermissions(serverPermissions)
                .withStorageRootDir(storageRoot.getAbsolutePath());

        VContext serverContext = SyncbaseServer.withNewServer(vContext, params);

        Server server = V.getServer(serverContext);
        try {
            // TODO(ashankar): For initial debugging it is proving useful to allow remote
            // inspection to browse through syncbase data. But this should be removed at
            // some point?
            sRemoteInspectors = new RemoteInspectors(serverContext, Constants.READ);
        } catch (VException e) {
            Log.w(TAG, "Unable to start remote inspection service:" + e);
        }
        // TODO(ashankar): This is not a good idea. For one, endpoints of a service may change
        // as the device changes networks. But I believe in a few weeks (mid June 2016) we'll
        // switch to a mode where there are no "local RPCs" between the syncbase client and the
        // server, so this will hopefully go away before it matters.
        // TODO(suharshs): Although in a few weeks (mid June 2016) the new mode mentioned above
        // will solve this issue, there is currently still an bug where initialization can hang due
        // to the wrong endpoint getting returned here. In particular, it is important that the
        // endpoint returned is locally accessible for the "local RPC" to succeed.
        // So for now, we make sure to return an locally accessible endpoint.
        Endpoint[] endpoints = server.getStatus().getEndpoints();
        for (Endpoint ep : endpoints) {
            try {
                String[] hostPort = ep.address().address().split(":");
                if (hostPort.length != 2) {
                    continue;
                }
                InetAddress addr = InetAddress.getByName(hostPort[0]);
                if (addr.isLoopbackAddress()) {
                    return ep.name();
                }
            } catch (UnknownHostException e) {
                // Try the next address.
            }
        }
        String errString = "";
        for (Endpoint ep : endpoints) {
            errString += " " + ep.name();
        }
        Log.e(TAG, "No locally accessible addresses in" + errString);
        return endpoints[0].name();
    }

    /**
     * Ensures that Syncbase is running. This should not be called until after the Vanadium
     * principal has assumed blessings. The Syncbase server will run until the process is killed.
     *
     * @throws IllegalStateException                                            if blessings were
     *                                                                          not attached to
     *                                                                          the principal
     *                                                                          beforehand
     * @throws io.v.impl.google.services.syncbase.SyncbaseServer.StartException if there was an
     *                                                                          error starting
     *                                                                          the syncbase service
     */
    private static void ensureSyncbaseStarted(Context androidContext)
            throws SyncbaseServer.StartException, VException {
        synchronized (sSyncbaseMutex) {
            if (sSyncbase == null) {
                VContext serverRun = getAppVContext().withCancel();
                try {
                    // Retrieve this context's personal permissions to set ACLs on the server.
                    Blessings personalBlessings = getPersonalBlessings();
                    if (personalBlessings == null) {
                        throw new IllegalStateException("Blessings must be attached to the "
                                + "Vanadium principal before Syncbase initialization.");
                    }
                    Permissions permissions = computePermissionsFromBlessings(personalBlessings);

                    sSyncbase = Syncbase.newService(startSyncbaseServer(serverRun, getAppContext(), permissions));
                } catch (SyncbaseServer.StartException | RuntimeException e) {
                    serverRun.cancel();
                    throw e;
                }
            }
        }
    }

    // TODO(rosswang): Factor into v23
    public static Blessings getPersonalBlessings() {
        return V.getPrincipal(getAppVContext()).blessingStore().defaultBlessings();
    }

    public static String getPersonalBlessingsString() {
        return getPersonalBlessings().toString();
    }

    public static String getEmailFromBlessings(Blessings blessings) {
        // TODO(alexfandrianto): This should be in v23, but it should also verify that the app
        // component is the right one in the blessing.
        return getEmailFromBlessingsString(blessings.toString());
    }

    public static String getEmailFromPattern(BlessingPattern pattern) {
        return getEmailFromBlessingsString(pattern.toString());
    }

    public static String getEmailFromBlessingsString(String blessingsStr) {
        String[] split = blessingsStr.split(io.v.v23.security.Constants.CHAIN_SEPARATOR);
        return split[split.length - 1];
    }

    public static String getPersonalEmail() {
        return getEmailFromBlessings(getPersonalBlessings());
    }

    public static String blessingsStringFromEmail(String email) {
        // TODO(alexfandrianto): We may need a more sophisticated method for producing this
        // blessings string. Currently, the app's id is fixed to the anonymous Android app.
        return DEFAULT_APP_BLESSING_STRING + io.v.v23.security.Constants.CHAIN_SEPARATOR + email;
    }

    protected static Permissions computePermissionsFromBlessings(Blessings blessings) {
        AccessList clientAcl = new AccessList(
                ImmutableList.of(new BlessingPattern(blessings.toString()), new BlessingPattern(CLOUD_BLESSING)),
                ImmutableList.<String>of());

        return new Permissions(
                ImmutableMap.of(Constants.RESOLVE.getValue(), OPEN_ACL.get(), Constants.READ.getValue(), clientAcl,
                        Constants.WRITE.getValue(), clientAcl, Constants.ADMIN.getValue(), clientAcl));
    }

    private static final Object sDatabaseMutex = new Object();
    private static Database sDatabase;

    private static void ensureDatabaseExists() throws VException {
        synchronized (sDatabaseMutex) {
            if (sDatabase == null) {
                final Database db = sSyncbase.getDatabase(new Id(DEFAULT_APP_BLESSING_STRING, DATABASE), null);
                Permissions permissions = computePermissionsFromBlessings(getPersonalBlessings());

                try {
                    VFutures.sync(db.create(getAppVContext(), permissions));
                } catch (ExistException e) {
                    // This is fine.
                }
                sDatabase = db;
            }
        }
    }

    private static final Object sUserCollectionMutex = new Object();
    private static Collection sUserCollection;

    private static void ensureUserCollectionExists() throws VException {
        synchronized (sUserCollectionMutex) {
            if (sUserCollection == null) {
                Collection userCollection = sDatabase
                        .getCollection(new Id(getPersonalBlessingsString(), USER_COLLECTION_NAME));

                Permissions permissions = Util.filterPermissionsByTags(
                        computePermissionsFromBlessings(getPersonalBlessings()),
                        io.v.v23.services.syncbase.Constants.ALL_COLLECTION_TAGS);
                try {
                    VFutures.sync(userCollection.create(getAppVContext(), permissions));
                } catch (ExistException e) {
                    // This is fine.
                }
                sUserCollection = userCollection;
            }
        }
    }

    private static final Object sCloudDatabaseMutex = new Object();
    private static Database sCloudDatabase;

    // TODO(alexfandrianto): Remove. Cloud database creation shouldn't be done in the app.
    private static void ensureCloudDatabaseExists() {
        synchronized (sCloudDatabaseMutex) {
            if (sCloudDatabase == null) {
                SyncbaseService cloudService = Syncbase.newService(CLOUD_NAME);
                Database db = cloudService.getDatabase(getAppVContext(), DATABASE, null);

                try {
                    VFutures.sync(db.create(getAppVContext().withTimeout(Duration.millis(SHORT_TIMEOUT)), null));
                } catch (ExistException e) {
                    // This is acceptable. No need to do it again.
                } catch (Exception e) {
                    Log.w(TAG, "Could not ensure cloud database exists: " + e.getMessage());
                }
                sCloudDatabase = db;
            }
        }
    }

    private static final Object sUserSyncgroupMutex = new Object();
    private static Syncgroup sUserSyncgroup;

    private static void ensureUserSyncgroupExists() throws VException {
        synchronized (sUserSyncgroupMutex) {
            if (sUserSyncgroup == null) {
                Blessings clientBlessings = getPersonalBlessings();
                String email = getEmailFromBlessings(clientBlessings);
                Log.d(TAG, email);

                Permissions permissions = Util.filterPermissionsByTags(
                        computePermissionsFromBlessings(clientBlessings),
                        io.v.v23.services.syncbase.Constants.ALL_SYNCGROUP_TAGS);

                String sgName = USER_COLLECTION_SYNCGROUP_SUFFIX + Math.abs(email.hashCode());

                final SyncgroupMemberInfo memberInfo = getDefaultMemberInfo();
                final Syncgroup sgHandle = sDatabase.getSyncgroup(new Id(clientBlessings.toString(), sgName));

                try {
                    Log.d(TAG, "Trying to join the syncgroup: " + sgName);
                    VFutures.sync(
                            sgHandle.join(getAppVContext(), CLOUD_NAME, Arrays.asList(CLOUD_BLESSING), memberInfo));
                    Log.d(TAG, "JOINED the syncgroup: " + sgName);
                } catch (SyncgroupJoinFailedException e) {
                    Log.w(TAG, "Failed join. Trying to create the syncgroup: " + sgName, e);
                    SyncgroupSpec spec = new SyncgroupSpec("TODOs User Data Collection", CLOUD_NAME, permissions,
                            ImmutableList.of(sUserCollection.id()), ImmutableList.of(MOUNTPOINT), false);
                    try {
                        VFutures.sync(sgHandle.create(getAppVContext(), spec, memberInfo));
                    } catch (BadAdvertisementException e2) {
                        Log.d(TAG, "Bad advertisement exception. Can we fix this?");
                    }
                    Log.d(TAG, "CREATED the syncgroup: " + sgName);
                } catch (Exception e) {
                    Log.d(TAG, "Failed to join or create the syncgroup: " + sgName);
                    if (!(e instanceof BadAdvertisementException)) { // joined, I guess
                        throw e;
                    }
                }
                sUserSyncgroup = sgHandle;
            }
        }
    }

    protected static SyncgroupMemberInfo getDefaultMemberInfo() {
        SyncgroupMemberInfo memberInfo = new SyncgroupMemberInfo();
        memberInfo.setSyncPriority((byte) 3);
        return memberInfo;

    }

    protected static String computeListSyncgroupName(String listId) {
        return LIST_COLLECTION_SYNCGROUP_PREFIX + listId;
    }

    private static String BLESSING_NAME_SEPARATOR = "___";

    public static String convertIdToString(Id id) {
        // Put the name first since it has a useful prefix for watch to switch on.
        return id.getName() + BLESSING_NAME_SEPARATOR + id.getBlessing();
    }

    public static Id convertStringToId(String idString) {
        String[] parts = idString.split(BLESSING_NAME_SEPARATOR);
        return new Id(parts[1], parts[0]);
    }

    private static volatile boolean sInitialized;

    public static boolean isInitialized() {
        return sInitialized;
    }

    /**
     * Extracts the value from a watch change or scan stream.
     * TODO(rosswang): This method is a temporary hack, awaiting resolution of the following issues:
     * <ul>
     * <li><a href="https://github.com/vanadium/issues/issues/1305">#1305</a>
     * <li><a href="https://github.com/vanadium/issues/issues/1310">#1310</a>
     * </ul>
     */
    @SuppressWarnings("unchecked")
    public static <T> T castFromSyncbase(Object watchValue, Class<T> type) {
        if (type.isInstance(watchValue)) {
            return (T) watchValue;
        }

        try {
            return (T) VomUtil.decode(VomUtil.encode((VdlStruct) watchValue), type);
        } catch (VException e) {
            Log.e(TAG, Throwables.getStackTraceAsString(e));
            throw new ClassCastException("Could not cast " + watchValue + " to " + type);
        }
    }

    protected class SyncTrappingCallback<T> extends TrappingCallback<T> {
        public SyncTrappingCallback() {
            super(R.string.err_sync, TAG, getErrorReporter());
        }
    }

    private final VAndroidContext<Activity> mVAndroidContext;

    public VContext getVContext() {
        return mVAndroidContext.getVContext();
    }

    public ErrorReporter getErrorReporter() {
        return mVAndroidContext.getErrorReporter();
    }

    @Override
    public void close() {
        mVAndroidContext.close();
    }

    protected Database getDatabase() {
        return sDatabase;
    }

    protected Collection getUserCollection() {
        return sUserCollection;
    }

    /**
     * @see TrappingCallback
     */
    protected void trap(ListenableFuture<?> future) {
        Futures.addCallback(future, new SyncTrappingCallback<>());
    }

    /**
     * Hook to insert or rebind fragments.
     *
     * @param manager
     * @param transaction the fragment transaction to use to add fragments, or null if fragments are
     *                    being restored by the system.
     */
    @CallSuper
    protected void addFeatureFragments(FragmentManager manager, @Nullable FragmentTransaction transaction) {
        if (transaction != null) {
            transaction.add(new NeighborhoodFragment(), NeighborhoodFragment.FRAGMENT_TAG);
        }
    }

    /**
     * This constructor is blocking for simplicity.
     */
    public SyncbasePersistence(final Activity activity, Bundle savedInstanceState)
            throws VException, SyncbaseServer.StartException {
        mVAndroidContext = VAndroidContexts.withDefaults(activity, savedInstanceState);

        FragmentManager mgr = activity.getFragmentManager();
        if (savedInstanceState == null) {
            FragmentTransaction t = mgr.beginTransaction();
            addFeatureFragments(mgr, t);
            t.commit();
        } else {
            addFeatureFragments(mgr, null);
        }

        // We might not actually have to seek blessings each time, but getBlessings does not
        // block if we already have blessings and this has better-behaved lifecycle
        // implications than trying to seek blessings in the static code.
        final SettableFuture<ListenableFuture<Blessings>> blessings = SettableFuture.create();
        if (activity.getMainLooper().getThread() == Thread.currentThread()) {
            blessings.set(BlessingsManager.getBlessings(getVContext(), activity, BLESSINGS_KEY, true));
        } else {
            new Handler(activity.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    blessings.set(BlessingsManager.getBlessings(getVContext(), activity, BLESSINGS_KEY, true));
                }
            });
        }

        VFutures.sync(Futures.dereference(blessings));
        appVInit(activity.getApplicationContext());
        final SyncbasePersistence self = this;
        /*final Future<?> ensureCloudDatabaseExists = sExecutor.submit(new Runnable() {
        @Override
        public void run() {
            ensureCloudDatabaseExists();
        }
        });*/
        ensureSyncbaseStarted(activity);
        ensureDatabaseExists();
        ensureUserCollectionExists();
        // TODO(alexfandrianto): If the cloud is dependent on me, then we must do this too.
        // VFutures.sync(ensureCloudDatabaseExists); // must finish before syncgroup setup
        ensureUserSyncgroupExists();
        Sharing.initDiscovery(sDatabase); // requires that db and collection exist
        sInitialized = true;
    }

    @Override
    public String debugDetails() {
        synchronized (sSyncbaseMutex) {
            if (sRemoteInspectors == null) {
                return "Syncbase has not been initialized";
            }
            final String timestamp = DateTimeFormat.forPattern("yyyy-MM-dd").print(new DateTime());
            try {
                return sRemoteInspectors.invite("invited-on-" + timestamp, Duration.standardDays(1));
            } catch (VException e) {
                return "Unable to setup remote inspection: " + e;
            }
        }
    }

    public static void acceptSharedTodoList(final Id listId) {
        sExecutor.submit(new Callable<Void>() {
            @Override
            public Void call() throws VException {
                Boolean exists = VFutures
                        .sync(sUserCollection.getRow(convertIdToString(listId)).exists(getAppVContext()));
                if (!exists) {
                    VFutures.sync(rememberTodoList(listId));
                }
                return null;
            }
        });
    }

    protected static ListenableFuture<Void> rememberTodoList(Id listId) {
        return sUserCollection.put(getAppVContext(), convertIdToString(listId), "");
    }

    public static ListenableFuture<Void> watchUserCollection(InputChannelCallback<WatchChange> callback) {
        InputChannel<WatchChange> watch = sDatabase.watch(getAppVContext(),
                ImmutableList.of(Util.rowPrefixPattern(sUserCollection.id(), LISTS_PREFIX)));
        return InputChannels.withCallback(watch, callback);
    }

    public static Timer watchSharedTo(final Id listId, final Function<List<BlessingPattern>, Void> callback) {
        final Syncgroup sgHandle = sDatabase
                .getSyncgroup(new Id(listId.getBlessing(), computeListSyncgroupName(listId.getName())));

        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            private SyncgroupSpec lastSpec;

            @Override
            public void run() {
                Map<String, SyncgroupSpec> specMap;
                try {
                    // Ok to block; we don't want to try parallel polls.
                    specMap = VFutures.sync(sgHandle.getSpec(getAppVContext()));
                } catch (Exception e) {
                    Log.w(TAG, "Failed to get syncgroup spec for list: " + listId, e);
                    return;
                }

                String version = Iterables.getOnlyElement(specMap.keySet());
                SyncgroupSpec spec = specMap.get(version);

                if (spec.equals(lastSpec)) {
                    return; // no changes, so no event should fire.
                }
                Log.d(TAG, "Spec changed for list: " + listId);
                lastSpec = spec;

                Permissions perms = spec.getPerms();
                AccessList acl = perms.get(Constants.READ.getValue());
                List<BlessingPattern> patterns = acl.getIn();

                callback.apply(patterns);
            }
        }, MEMBER_TIMER_DELAY, MEMBER_TIMER_PERIOD);
        return timer;
    }
}