com.couchbase.lite.LiteTestCaseWithDB.java Source code

Java tutorial

Introduction

Here is the source code for com.couchbase.lite.LiteTestCaseWithDB.java

Source

//
// Copyright (c) 2016 Couchbase, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
// except in compliance with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the
// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing permissions
// and limitations under the License.
//
package com.couchbase.lite;

import com.couchbase.lite.internal.Body;
import com.couchbase.lite.internal.RevisionInternal;
import com.couchbase.lite.mockserver.MockDispatcher;
import com.couchbase.lite.mockserver.MockDocumentGet;
import com.couchbase.lite.mockserver.MockHelper;
import com.couchbase.lite.replicator.Replication;
import com.couchbase.lite.replicator.ReplicationState;
import com.couchbase.lite.router.Router;
import com.couchbase.lite.router.RouterCallbackBlock;
import com.couchbase.lite.router.URLConnection;
import com.couchbase.lite.router.URLStreamHandlerFactory;
import com.couchbase.lite.storage.SQLiteNativeLibrary;
import com.couchbase.lite.store.SQLiteStore;
import com.couchbase.lite.support.FileDirUtils;
import com.couchbase.lite.support.Version;
import com.couchbase.lite.support.security.SymmetricKey;
import com.couchbase.lite.util.Log;
import com.couchbase.lite.util.Utils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import junit.framework.Assert;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import javax.crypto.Cipher;

import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;

public class LiteTestCaseWithDB extends LiteTestCase {
    public static final String TAG = "LiteTestCaseWithDB";

    private static boolean initializedUrlHandler = false;

    protected ObjectMapper mapper = new ObjectMapper();

    protected Manager manager = null;
    protected Database database = null;
    protected static final String DEFAULT_TEST_DB = "cblite-test";
    protected static final String DEFAULT_TEST_DIR_NAME = "test";

    protected boolean useForestDB = false;

    private boolean encryptedAttachmentStore = false;

    protected boolean isSQLiteDB() {
        return SQLiteStore.class.equals(database.getStore().getClass());
    }

    protected boolean isUseForestDB() {
        return useForestDB;
    }

    @Override
    public void runBare() throws Throwable {
        long start = System.currentTimeMillis();

        loadCustomProperties();

        // Run Unit Test with SQLiteStore
        if (getTestStorageType() != 2) {
            useForestDB = false;
            super.runBare();
        }

        // Run Unit Test with ForestDBStore
        if (getTestStorageType() != 1) {
            useForestDB = true;
            super.runBare();
        }

        long end = System.currentTimeMillis();
        String name = getName();
        long duration = (end - start) / 1000;
        Log.e(TAG, "DURATION: %s: %d sec%s", name, duration, duration >= 6 ? " - [SLOW]" : "");
    }

    public void runBare(boolean forestDB) throws Throwable {
        long start = System.currentTimeMillis();

        useForestDB = forestDB;
        super.runBare();

        long end = System.currentTimeMillis();
        String name = getName();
        long duration = (end - start) / 1000;
        Log.e(TAG, "DURATION: %s: %d sec%s", name, duration, duration >= 6 ? " - [SLOW]" : "");
    }

    @Override
    protected void setUp() throws Exception {
        Log.v(TAG, "setUp");
        super.setUp();

        loadCustomProperties();

        if (!useForestDB)
            setupSQLiteNativeLibrary();

        //for some reason a traditional static initializer causes junit to die
        if (!initializedUrlHandler) {
            URLStreamHandlerFactory.registerSelfIgnoreError();
            initializedUrlHandler = true;
        }

        startCBLite();
        startDatabase();
    }

    protected void setupSQLiteNativeLibrary() {
        int library = getSQLiteLibrary();
        if (library == 1)
            SQLiteNativeLibrary.TEST_NATIVE_LIBRARY_NAME = SQLiteNativeLibrary.JNI_SQLITE_CUSTOM_LIBRARY;
        else if (library == 2)
            SQLiteNativeLibrary.TEST_NATIVE_LIBRARY_NAME = SQLiteNativeLibrary.JNI_SQLCIPHER_LIBRARY;
        else
            throw new IllegalArgumentException("Invalid Native Library : " + library);
    }

    protected static boolean syncgatewayTestsEnabled() {
        return Boolean.parseBoolean(System.getProperty("syncgatewayTestsEnabled"));
    }

    protected static boolean multithreadsTestsEnabled() {
        return Boolean.parseBoolean(System.getProperty("multithreadsTestsEnabled"));
    }

    protected static int getSQLiteLibrary() {
        return Integer.parseInt(System.getProperty("sqliteLibrary"));
    }

    protected static int getTestStorageType() {
        return Integer.parseInt(System.getProperty("storageType"));
    }

    protected boolean isEncryptionTestEnabled() {
        if (!isAndriod()) {
            boolean support256Key = false;
            try {
                support256Key = Cipher.getMaxAllowedKeyLength("AES") >= 256;
            } catch (NoSuchAlgorithmException e) {
            }
            if (!support256Key)
                return false;
        }

        return !isSQLiteDB() || (isSQLiteDB()
                && SQLiteNativeLibrary.TEST_NATIVE_LIBRARY_NAME == SQLiteNativeLibrary.JNI_SQLCIPHER_LIBRARY);
    }

    protected InputStream getAsset(String name) {
        return this.getClass().getResourceAsStream("/assets/" + name);
    }

    protected Context getDefaultTestContext(boolean deleteContent) {
        return getTestContext(DEFAULT_TEST_DIR_NAME, deleteContent);
    }

    protected Context getTestContext(String dirName, boolean deleteContent) {
        Context context = getTestContext(dirName);
        if (deleteContent && context.getFilesDir().exists()) {
            // NOTE: retry and sleep is only for Windows. Other platforms never fail.
            int counter = 0;
            while (!FileDirUtils.cleanDirectory(context.getFilesDir()) && counter < 10) {
                // NOTE: forestdb releses resources in Object.destroy()
                System.gc();
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                }
                counter++;
            }
            assertTrue(counter < 10);
        }
        return context;
    }

    protected void startCBLite() throws IOException {
        Manager.enableLogging(TAG, Log.VERBOSE);
        Manager.enableLogging(Log.TAG, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_SYNC, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_SYNC_ASYNC_TASK, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_BATCHER, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_QUERY, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_VIEW, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_CHANGE_TRACKER, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_BLOB_STORE, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_DATABASE, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_LISTENER, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_MULTI_STREAM_WRITER, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_REMOTE_REQUEST, Log.VERBOSE);
        Manager.enableLogging(Log.TAG_ROUTER, Log.VERBOSE);

        Context context = getDefaultTestContext(true);
        manager = createManager(context);
    }

    protected Manager createManager(Context context) throws IOException {
        ManagerOptions options = new ManagerOptions();
        Manager manager = new Manager(context, options);
        if (useForestDB)
            manager.setStorageType(Manager.FORESTDB_STORAGE);
        return manager;
    }

    protected void stopCBLite() {
        int DEFAULT_VALUE = Utils.DEFAULT_TIME_TO_WAIT_4_SHUTDOWN;
        Utils.DEFAULT_TIME_TO_WAIT_4_SHUTDOWN = 0;
        try {
            if (manager != null) {
                manager.close();
            }
        } finally {
            Utils.DEFAULT_TIME_TO_WAIT_4_SHUTDOWN = DEFAULT_VALUE;
        }
    }

    protected Database startDatabase() throws CouchbaseLiteException {
        database = ensureEmptyDatabase(DEFAULT_TEST_DB);
        return database;
    }

    protected void stopDatabase() {
        if (database != null) {
            database.close();
        }
    }

    protected Database ensureEmptyDatabase(String dbName) throws CouchbaseLiteException {
        Database db = manager.getExistingDatabase(dbName);
        if (db != null) {
            db.delete();
        }
        db = manager.getDatabase(dbName);
        return db;
    }

    protected void loadCustomProperties() throws IOException {
        Properties systemProperties = System.getProperties();
        InputStream mainProperties = getAsset("test.properties");
        if (mainProperties != null) {
            systemProperties.load(new InputStreamReader(mainProperties, "UTF-8"));
            mainProperties.close();
        }
        try {
            InputStream localProperties = getAsset("local-test.properties");
            if (localProperties != null) {
                systemProperties.load(new InputStreamReader(localProperties, "UTF-8"));
                localProperties.close();
            }
        } catch (IOException e) {
            Log.w(TAG, "Error trying to read from local-test.properties, does this file exist?");
        }
    }

    protected URL getReplicationURL() throws MalformedURLException {
        return new URL(System.getProperty("replicationUrl"));
    }

    protected URL getSyncGatewayURL() throws MalformedURLException {
        return new URL(System.getProperty("syncgatewayUrl"));
    }

    protected String getSyncGatewayHost() throws MalformedURLException {
        return System.getProperty("syncgatewayHost");
    }

    protected URL getRemoteTestDBURL(String dbName) throws MalformedURLException {
        URL url = getSyncGatewayURL();
        return new URL(url.getProtocol(), url.getHost(), url.getPort(), dbName);
    }

    protected URL getAdminReplicationURL() throws MalformedURLException {
        return new URL(System.getProperty("adminReplicationUrl"));
    }

    protected boolean isTestingAgainstSyncGateway() {
        try {
            URL url = getReplicationURL();
            return url.getPort() == 4984;
        } catch (MalformedURLException e) {
            return false;
        }
    }

    @Override
    protected void tearDown() throws Exception {
        Log.v(TAG, "tearDown");
        super.tearDown();
        stopDatabase();
        stopCBLite();
    }

    protected Map<String, Object> userProperties(Map<String, Object> properties) {
        Map<String, Object> result = new HashMap<String, Object>();

        for (String key : properties.keySet()) {
            if (!key.startsWith("_")) {
                result.put(key, properties.get(key));
            }
        }

        return result;
    }

    public boolean isEncryptedAttachmentStore() {
        return encryptedAttachmentStore;
    }

    public void setEncryptedAttachmentStore(boolean encrypted) throws Exception {
        SymmetricKey key = encrypted ? new SymmetricKey() : null;
        database.getAttachmentStore().changeEncryptionKey(key);
        encryptedAttachmentStore = encrypted;
    }

    public Map<String, Object> getReplicationAuthParsedJson() throws IOException {
        String authJson = "{\n" + "    \"facebook\" : {\n" + "        \"email\" : \"jchris@couchbase.com\"\n"
                + "     }\n" + "   }\n";
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> authProperties = mapper.readValue(authJson,
                new TypeReference<HashMap<String, Object>>() {
                });
        return authProperties;

    }

    public Map<String, Object> getPushReplicationProperties(URL url) throws IOException {
        Map<String, Object> targetProperties = new HashMap<String, Object>();
        targetProperties.put("url", url.toExternalForm());

        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("source", DEFAULT_TEST_DB);
        properties.put("target", targetProperties);
        return properties;
    }

    public Map<String, Object> getPullReplicationProperties(URL url) throws IOException {
        Map<String, Object> sourceProperties = new HashMap<String, Object>();
        sourceProperties.put("url", url.toExternalForm());

        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("source", sourceProperties);
        properties.put("target", DEFAULT_TEST_DB);
        return properties;
    }

    protected URLConnection sendRequest(String method, String path, Map<String, String> headers, Object bodyObj) {
        try {
            URL url = new URL("cblite://" + path);
            URLConnection conn = (URLConnection) url.openConnection();
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setDoOutput(true);
            conn.setRequestMethod(method);
            if (headers != null) {
                for (String header : headers.keySet()) {
                    conn.setRequestProperty(header, headers.get(header));
                }
            }

            if (bodyObj != null) {
                conn.setDoInput(true);
                ByteArrayInputStream bais = new ByteArrayInputStream(mapper.writeValueAsBytes(bodyObj));
                conn.setRequestInputStream(bais);
            }

            Router router = new com.couchbase.lite.router.Router(manager, conn);

            final CountDownLatch latch = new CountDownLatch(1);
            router.setCallbackBlock(new RouterCallbackBlock() {
                @Override
                public void onResponseReady() {
                    latch.countDown();
                }
            });

            router.start();

            boolean success = false;
            try {
                // NOTE: latch.await() should be fine. 60 sec for just in case.
                success = latch.await(60, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            assertTrue(success);

            return conn;
        } catch (MalformedURLException e) {
            fail();
        } catch (IOException e) {
            fail();
        }
        return null;
    }

    protected Object parseJSONResponse(URLConnection conn) {
        Object result = null;
        try {
            byte[] bytes = IOUtils.toByteArray(conn.getResponseInputStream());
            result = Manager.getObjectMapper().readValue(bytes, Object.class);
        } catch (IOException e) {
            Log.e(TAG, "Cannot get data from connection", e);
            fail();
        }
        return result;
    }

    protected Object sendBody(String method, String path, Object bodyObj, int expectedStatus,
            Object expectedResult) {
        return sendBody(method, path, bodyObj, null, expectedStatus, expectedResult);
    }

    protected Object sendBody(String method, String path, Object bodyObj, Map<String, String> headers,
            int expectedStatus, Object expectedResult) {
        URLConnection conn = sendRequest(method, path, headers, bodyObj);
        conn.setRequestProperty("Content-Type", "application/json");
        Object result = parseJSONResponse(conn);
        Log.v(TAG, "%s %s --> %d", method, path, conn.getResponseCode());
        Assert.assertEquals(expectedStatus, conn.getResponseCode());
        if (expectedResult != null)
            Assert.assertEquals(expectedResult, result);
        return result;
    }

    protected Object send(String method, String path, int expectedStatus, Object expectedResult) {
        return send(method, path, null, expectedStatus, expectedResult);
    }

    protected Object send(String method, String path, Map<String, String> headers, int expectedStatus,
            Object expectedResult) {
        return sendBody(method, path, null, headers, expectedStatus, expectedResult);
    }

    public static void createDocuments(final Database db, final int n) {
        db.runInTransaction(new TransactionalTask() {
            @Override
            public boolean run() {
                createDocuments(db, n, false);
                return true;
            }
        });
    }

    public static void createDocuments(final Database db, final int n, boolean transaction) {
        if (transaction) {
            createDocuments(db, n);
        } else {
            for (int i = 0; i < n; i++) {
                Map<String, Object> properties = new HashMap<String, Object>();
                properties.put("testName", "testDatabase");
                properties.put("sequence", i);
                createDocumentWithProperties(db, properties);
            }
        }
    }

    static Future createDocumentsAsync(final Database db, final int n) {
        return db.runAsync(new AsyncTask() {
            @Override
            public void run(Database database) {
                db.runInTransaction(new TransactionalTask() {
                    @Override
                    public boolean run() {
                        createDocuments(db, n);
                        return true;
                    }
                });
            }
        });
    }

    public static Document createDocumentWithProperties(Database db, Map<String, Object> properties) {
        Document doc = db.createDocument();
        Assert.assertNotNull(doc);
        Assert.assertNull(doc.getCurrentRevisionId());
        Assert.assertNull(doc.getCurrentRevision());
        Assert.assertNotNull("Document has no ID", doc.getId());
        // 'untitled' docs are no longer untitled (8/10/12)
        try {
            doc.putProperties(properties);
        } catch (Exception e) {
            Log.e(TAG, "Error creating document", e);
            assertTrue(
                    "can't create new document in db:" + db.getName() + " with properties:" + properties.toString(),
                    false);
        }
        Assert.assertNotNull(doc.getId());
        Assert.assertNotNull(doc.getCurrentRevisionId());
        Assert.assertNotNull(doc.getUserProperties());

        // should be same doc instance, since there should only ever be a single Document
        // instance for a given document
        Assert.assertEquals(db.getDocument(doc.getId()), doc);

        Assert.assertEquals(db.getDocument(doc.getId()).getId(), doc.getId());

        return doc;
    }

    public static Document createDocWithAttachment(Database database, String attachmentName, String content,
            Map<String, Object> properties) throws Exception {

        Document doc = createDocumentWithProperties(database, properties);
        SavedRevision rev = doc.getCurrentRevision();

        assertEquals(rev.getAttachments().size(), 0);
        assertEquals(rev.getAttachmentNames().size(), 0);
        assertNull(rev.getAttachment(attachmentName));

        ByteArrayInputStream body = new ByteArrayInputStream(content.getBytes());

        UnsavedRevision rev2 = doc.createRevision();
        rev2.setAttachment(attachmentName, "text/plain; charset=utf-8", body);

        SavedRevision rev3 = rev2.save();
        assertNotNull(rev3);
        assertEquals(rev3.getAttachments().size(), 1);
        assertEquals(rev3.getAttachmentNames().size(), 1);

        Attachment attach = rev3.getAttachment(attachmentName);
        assertNotNull(attach);
        assertEquals(doc, attach.getDocument());
        assertEquals(attachmentName, attach.getName());
        List<String> attNames = new ArrayList<String>();
        attNames.add(attachmentName);
        assertEquals(rev3.getAttachmentNames(), attNames);

        assertEquals("text/plain; charset=utf-8", attach.getContentType());
        InputStream attachInputStream = attach.getContent();
        assertEquals(IOUtils.toString(attachInputStream, "UTF-8"), content);
        attachInputStream.close();
        assertEquals(content.getBytes().length, attach.getLength());

        return doc;
    }

    public static Document createDocWithAttachment(Database database, String attachmentName, String content)
            throws Exception {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("foo", "bar");
        return createDocWithAttachment(database, attachmentName, content, properties);
    }

    public void stopReplication(Replication replication) throws Exception {
        final CountDownLatch replicationDoneSignal = new CountDownLatch(1);
        replication.addChangeListener(new ReplicationFinishedObserver(replicationDoneSignal));

        replication.stop();

        boolean success = replicationDoneSignal.await(30, TimeUnit.SECONDS);
        assertTrue(success);
    }

    protected String createDocumentsForPushReplication(String docIdTimestamp) throws CouchbaseLiteException {
        return createDocumentsForPushReplication(docIdTimestamp, "png");
    }

    protected Document createDocumentForPushReplication(String docId, String attachmentFileName,
            String attachmentContentType) throws CouchbaseLiteException {

        Map<String, Object> docJsonMap = MockHelper.generateRandomJsonMap();
        Map<String, Object> docProperties = new HashMap<String, Object>();
        docProperties.put("_id", docId);
        docProperties.putAll(docJsonMap);
        Document document = database.getDocument(docId);
        UnsavedRevision revision = document.createRevision();
        revision.setProperties(docProperties);

        if (attachmentFileName != null) {
            revision.setAttachment(attachmentFileName, attachmentContentType, getAsset(attachmentFileName));
        }

        revision.save();
        return document;

    }

    protected String createDocumentsForPushReplication(String docIdTimestamp, String attachmentType)
            throws CouchbaseLiteException {
        String doc1Id;
        String doc2Id;// Create some documents:
        Map<String, Object> doc1Properties = new HashMap<String, Object>();
        doc1Id = String.format(Locale.ENGLISH, "doc1-%s", docIdTimestamp);
        doc1Properties.put("_id", doc1Id);
        doc1Properties.put("foo", 1);
        doc1Properties.put("bar", false);

        Body body = new Body(doc1Properties);
        RevisionInternal rev1 = new RevisionInternal(body);

        Status status = new Status();
        rev1 = database.putRevision(rev1, null, false, status);
        assertEquals(Status.CREATED, status.getCode());

        doc1Properties.put("_rev", rev1.getRevID());
        doc1Properties.put("UPDATED", true);

        @SuppressWarnings("unused")
        RevisionInternal rev2 = database.putRevision(new RevisionInternal(doc1Properties), rev1.getRevID(), false,
                status);
        assertEquals(Status.CREATED, status.getCode());

        Map<String, Object> doc2Properties = new HashMap<String, Object>();
        doc2Id = String.format(Locale.ENGLISH, "doc2-%s", docIdTimestamp);
        doc2Properties.put("_id", doc2Id);
        doc2Properties.put("baz", 666);
        doc2Properties.put("fnord", true);

        database.putRevision(new RevisionInternal(doc2Properties), null, false, status);
        assertEquals(Status.CREATED, status.getCode());

        Document doc2 = database.getDocument(doc2Id);
        UnsavedRevision doc2UnsavedRev = doc2.createRevision();
        if (attachmentType.equals("png")) {
            InputStream attachmentStream = getAsset("attachment.png");
            doc2UnsavedRev.setAttachment("attachment.png", "image/png", attachmentStream);
        } else if (attachmentType.equals("txt")) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 1000; i++) {
                sb.append("This is a large attachemnt.");
            }
            ByteArrayInputStream attachmentStream = new ByteArrayInputStream(sb.toString().getBytes());
            doc2UnsavedRev.setAttachment("attachment.txt", "text/plain", attachmentStream);
        } else {
            throw new RuntimeException("invalid attachment type: " + attachmentType);
        }
        SavedRevision doc2Rev = doc2UnsavedRev.save();
        assertNotNull(doc2Rev);

        return doc1Id;
    }

    // timeout - second
    public void runReplication(Replication replication) throws Exception {
        runReplication(replication, 120);
    }

    // timeout - second
    public void runReplication(Replication replication, long timeout) throws Exception {
        final CountDownLatch replicationDoneSignal = new CountDownLatch(1);
        replication.addChangeListener(new ReplicationFinishedObserver(replicationDoneSignal));
        replication.start();
        boolean success = replicationDoneSignal.await(timeout, TimeUnit.SECONDS);
        assertTrue(success);
    }

    public void waitForPutCheckpointRequestWithSeq(MockDispatcher dispatcher, int seq) throws TimeoutException {
        while (true) {
            RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT);
            checkUserAgent(request);
            if (request.getMethod().equals("PUT")) {
                Buffer clone = request.getBody().clone();
                String body = clone.readUtf8();
                if (body.indexOf(Integer.toString(seq)) != -1) {
                    // block until response returned
                    dispatcher.takeRecordedResponseBlocking(request);
                    return;
                }
            }
        }
    }

    protected void checkUserAgent(RecordedRequest request) {
        assertNotNull(request);
        String userAgent = request.getHeader("User-Agent");
        assertNotNull(userAgent);
        Log.v(TAG, "[checkUserAgent(RecordedRequest()] UserAgent: " + userAgent);
        assertTrue(userAgent.indexOf(Manager.PRODUCT_NAME + "/" + Version.SYNC_PROTOCOL_VERSION) != -1);
    }

    protected List<RecordedRequest> waitForPutCheckpointRequestWithSequence(MockDispatcher dispatcher,
            int expectedLastSequence) throws IOException, TimeoutException {

        Log.d(TAG, "Wait for PUT checkpoint request with lastSequence: %s", expectedLastSequence);

        List<RecordedRequest> recordedRequests = new ArrayList<RecordedRequest>();

        // wait until mock server gets a checkpoint PUT request with expected lastSequence
        boolean foundExpectedLastSeq = false;
        String expectedLastSequenceStr = String.format(Locale.ENGLISH, "%s", expectedLastSequence);

        while (!foundExpectedLastSeq) {

            RecordedRequest request = dispatcher.takeRequestBlocking(MockHelper.PATH_REGEX_CHECKPOINT);
            if (request.getMethod().equals("PUT")) {

                recordedRequests.add(request);
                Buffer clone = request.getBody().clone();
                Map<String, Object> jsonMap = Manager.getObjectMapper().readValue(clone.readByteArray(), Map.class);
                Log.i(TAG, "lastSequence=" + jsonMap.get("lastSequence"));
                Log.i(TAG, "checkpoint request=" + jsonMap);
                if (jsonMap.containsKey("lastSequence")
                        && ((String) jsonMap.get("lastSequence")).equals(expectedLastSequenceStr)) {
                    foundExpectedLastSeq = true;
                }

                // wait until mock server responds to the checkpoint PUT request.
                // not sure if this is strictly necessary, but might prevent race conditions.
                dispatcher.takeRecordedResponseBlocking(request);

            }
        }

        return recordedRequests;
    }

    protected void validateCheckpointRequestsRevisions(List<RecordedRequest> checkpointRequests) {
        try {
            int i = 0;
            for (RecordedRequest request : checkpointRequests) {
                Buffer clone = request.getBody().clone();
                Map<String, Object> jsonMap = Manager.getObjectMapper().readValue(clone.readByteArray(), Map.class);
                if (i == 0) {
                    // the first request is not expected to have a _rev field
                    assertFalse(jsonMap.containsKey("_rev"));
                } else {
                    assertTrue(jsonMap.containsKey("_rev"));
                    // TODO: make sure that each _rev is in sequential order, eg: "0-1", "0-2", etc..
                }
                i += 1;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    protected Document createDocWithProperties(Map<String, Object> props) throws CouchbaseLiteException {
        return createDocWithProperties(props, database);
    }

    protected Document createDocWithProperties(Map<String, Object> props, Database db)
            throws CouchbaseLiteException {
        Document doc = db.createDocument();
        UnsavedRevision revUnsaved = doc.createRevision();
        revUnsaved.setUserProperties(props);
        SavedRevision rev = revUnsaved.save();
        assertNotNull(rev);
        return doc;
    }

    protected void attachmentAsserts(String docAttachName, Document doc)
            throws IOException, CouchbaseLiteException {
        Attachment attachment = doc.getCurrentRevision().getAttachment(docAttachName);
        assertNotNull(attachment);
        byte[] testAttachBytes = MockDocumentGet.getAssetByteArray(docAttachName);
        int attachLength = testAttachBytes.length;
        assertEquals(attachLength, attachment.getLength());
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        InputStream is = attachment.getContent();
        baos.write(is);
        is.close();
        byte[] actualAttachBytes = baos.toByteArray();
        assertEquals(testAttachBytes.length, actualAttachBytes.length);
        for (int i = 0; i < actualAttachBytes.length; i++) {
            boolean ithByteEqual = actualAttachBytes[i] == testAttachBytes[i];
            if (!ithByteEqual) {
                Log.d(Log.TAG, "mismatch");
            }
            assertTrue(ithByteEqual);
        }
    }

    public static SavedRevision createRevisionWithRandomProps(SavedRevision createRevFrom, boolean allowConflict)
            throws CouchbaseLiteException {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(UUID.randomUUID().toString(), "val");
        UnsavedRevision unsavedRevision = createRevFrom.createRevision();
        unsavedRevision.setUserProperties(properties);
        return unsavedRevision.save(allowConflict);
    }

    /*
    Assert that the bulk docs json in request contains given doc.
        
    Example bulk docs json.
        
     {
       "docs":[
     {
       "_id":"b7f5664c-7f84-4ddf-9abc-de4e3f376ae4",
       ..
     }
       ]
     }
     */
    protected void assertBulkDocJsonContainsDoc(RecordedRequest request, Document doc) throws Exception {

        Buffer clone = request.getBody().clone();
        Map<String, Object> bulkDocsJson = Manager.getObjectMapper().readValue(clone.readByteArray(), Map.class);
        List docs = (List) bulkDocsJson.get("docs");
        Map<String, Object> firstDoc = (Map<String, Object>) docs.get(0);
        assertEquals(doc.getId(), firstDoc.get("_id"));
    }

    protected boolean isBulkDocJsonContainsDoc(RecordedRequest request, Document doc) throws Exception {
        Buffer clone = request.getBody().clone();
        Map<String, Object> bulkDocsJson = Manager.getObjectMapper().readValue(clone.readByteArray(), Map.class);
        List docs = (List) bulkDocsJson.get("docs");
        Iterator<Object> itr = docs.iterator();
        while (itr.hasNext()) {
            Map<String, Object> tmp = (Map<String, Object>) itr.next();
            if (tmp.get("_id").equals(doc.getId()))
                return true;
        }
        return false;
    }

    public static class ReplicationIdleObserver implements Replication.ChangeListener {
        private CountDownLatch idleSignal;

        public ReplicationIdleObserver(CountDownLatch idleSignal) {
            this.idleSignal = idleSignal;
        }

        @Override
        public void changed(Replication.ChangeEvent event) {
            if (event.getTransition() != null && event.getTransition().getDestination() == ReplicationState.IDLE) {
                idleSignal.countDown();
            }
        }
    }

    public static class ReplicationFinishedObserver implements Replication.ChangeListener {
        private CountDownLatch doneSignal;

        public ReplicationFinishedObserver(CountDownLatch doneSignal) {
            this.doneSignal = doneSignal;
        }

        @Override
        public void changed(Replication.ChangeEvent event) {
            if (event.getTransition() != null
                    && event.getTransition().getDestination() == ReplicationState.STOPPED) {
                doneSignal.countDown();
                assertEquals(event.getChangeCount(), event.getCompletedChangeCount());
            }
        }
    }

    public static class ReplicationOfflineObserver implements Replication.ChangeListener {
        private CountDownLatch offlineSignal;

        public ReplicationOfflineObserver(CountDownLatch offlineSignal) {
            this.offlineSignal = offlineSignal;
        }

        @Override
        public void changed(Replication.ChangeEvent event) {
            if (event.getTransition() != null
                    && event.getTransition().getDestination() == ReplicationState.OFFLINE) {
                offlineSignal.countDown();
            }
        }
    }

    public static class ReplicationRunningObserver implements Replication.ChangeListener {
        private CountDownLatch runningSignal;

        public ReplicationRunningObserver(CountDownLatch runningSignal) {
            this.runningSignal = runningSignal;
        }

        @Override
        public void changed(Replication.ChangeEvent event) {
            if (event.getTransition() != null
                    && event.getTransition().getDestination() == ReplicationState.RUNNING) {
                runningSignal.countDown();
            }
        }
    }

    protected void reopenTestDB() throws CouchbaseLiteException {
        Log.i(TAG, "---- closing db ----");
        String dbName = database.getName();
        assertTrue(database.close());

        Log.i(TAG, "---- reopening db ----");
        Database db2 = manager.getDatabase(dbName);
        assertNotNull(db2);
        assertTrue(db2 != database);
        database = db2;
    }
}