com.couchbase.lite.DocumentTest.java Source code

Java tutorial

Introduction

Here is the source code for com.couchbase.lite.DocumentTest.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.RevisionInternal;
import com.couchbase.lite.util.CountDown;
import com.couchbase.lite.util.Log;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;

import junit.framework.Assert;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class DocumentTest extends LiteTestCaseWithDB {

    public void testNewDocumentHasCurrentRevision() throws CouchbaseLiteException {
        Document document = database.createDocument();
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("foo", "foo");
        properties.put("bar", Boolean.FALSE);
        document.putProperties(properties);
        Assert.assertNotNull(document.getCurrentRevisionId());
        Assert.assertNotNull(document.getCurrentRevision());
    }

    /**
     * https://github.com/couchbase/couchbase-lite-android/issues/301
     */
    public void testPutDeletedDocument() throws CouchbaseLiteException {

        Document document = database.createDocument();
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("foo", "foo");
        properties.put("bar", Boolean.FALSE);
        document.putProperties(properties);
        Assert.assertNotNull(document.getCurrentRevision());
        String docId = document.getId();

        properties.put("_rev", document.getCurrentRevisionId());
        properties.put("_deleted", true);
        properties.put("mykey", "myval");
        SavedRevision newRev = document.putProperties(properties);
        newRev.loadProperties();
        assertTrue(newRev.getProperties().containsKey("mykey"));

        Assert.assertTrue(document.isDeleted());
        Document fetchedDoc = database.getExistingDocument(docId);
        Assert.assertNull(fetchedDoc);

        // query all docs and make sure we don't see that document
        database.getAllDocs(new QueryOptions());
        Query queryAllDocs = database.createAllDocumentsQuery();
        QueryEnumerator queryEnumerator = queryAllDocs.run();
        for (Iterator<QueryRow> it = queryEnumerator; it.hasNext();) {
            QueryRow row = it.next();
            Assert.assertFalse(row.getDocument().getId().equals(docId));
        }
    }

    public void testDeleteDocument() throws CouchbaseLiteException {
        Document document = database.createDocument();
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("foo", "foo");
        properties.put("bar", Boolean.FALSE);
        document.putProperties(properties);
        Assert.assertNotNull(document.getCurrentRevision());
        String docId = document.getId();
        document.delete();
        Assert.assertTrue(document.isDeleted());
        Document fetchedDoc = database.getExistingDocument(docId);
        Assert.assertNull(fetchedDoc);

        // query all docs and make sure we don't see that document
        //database.getAllDocs(new QueryOptions());
        Query queryAllDocs = database.createAllDocumentsQuery();
        QueryEnumerator queryEnumerator = queryAllDocs.run();
        for (Iterator<QueryRow> it = queryEnumerator; it.hasNext();) {
            QueryRow row = it.next();
            Assert.assertFalse(row.getDocument().getId().equals(docId));
        }
    }

    /**
     * Port test over from:
     * https://github.com/couchbase/couchbase-lite-ios/commit/e0469300672a2087feb46b84ca498facd49e0066
     */
    public void testGetNonExistentDocument() throws CouchbaseLiteException {
        assertNull(database.getExistingDocument("missing"));
        Document doc = database.getDocument("missing");
        assertNotNull(doc);
        assertNull(database.getExistingDocument("missing"));
    }

    // Reproduces issue #167
    // https://github.com/couchbase/couchbase-lite-android/issues/167
    public void testLoadRevisionBody() throws CouchbaseLiteException {

        Document document = database.createDocument();
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("foo", "foo");
        properties.put("bar", Boolean.FALSE);
        document.putProperties(properties);
        Assert.assertNotNull(document.getCurrentRevision());

        boolean deleted = false;
        RevisionInternal revisionInternal = new RevisionInternal(document.getId(), document.getCurrentRevisionId(),
                deleted);
        database.loadRevisionBody(revisionInternal);

        // now lets purge the document, and then try to load the revision body again
        document.purge();

        boolean gotExpectedException = false;
        RevisionInternal copyRev = revisionInternal.copyWithoutBody();
        try {
            database.loadRevisionBody(copyRev);
        } catch (CouchbaseLiteException e) {
            if (e.getCBLStatus().getCode() == Status.NOT_FOUND) {
                gotExpectedException = true;
            }
        }
        assertTrue(gotExpectedException);
    }

    /**
     * https://github.com/couchbase/couchbase-lite-android/issues/281
     */
    public void testDocumentWithRemovedProperty() {

        Map<String, Object> props = new HashMap<String, Object>();
        props.put("_id", "fakeid");
        props.put("_removed", true);
        props.put("foo", "bar");

        Document doc = createDocumentWithProperties(database, props);
        assertNotNull(doc);

        Document docFetched = database.getDocument(doc.getId());
        Map<String, Object> fetchedProps = docFetched.getCurrentRevision().getProperties();
        assertNotNull(fetchedProps.get("_removed"));
        assertTrue(docFetched.getCurrentRevision().isGone());
    }

    public void testGetDocumentWithLargeJSON() {
        Map<String, Object> props = new HashMap<String, Object>();
        props.put("_id", "laaargeJSON");
        final String content;
        {
            // NOTE: Java internally uses Unicode, following characters consumes 2.1M * 2Bytes -> 4.2MB in memory.
            char[] chars = new char[2 * 1024 * 1024 + 10 * 1024]; // 2.1K characters
            Arrays.fill(chars, 'a');
            // NOTE: public String(char value[]) copies characters.
            // http://hg.openjdk.java.net/jdk7u/jdk7u6/jdk/file/8c2c5d63a17e/src/share/classes/java/lang/String.java#l168
            content = new String(chars);
            // make GC free data
            chars = null;
        }
        props.put("foo", content);

        Document doc = createDocumentWithProperties(database, props);
        assertNotNull(doc);

        Document docFetched = database.getDocument(doc.getId());
        Map<String, Object> fetchedProps = docFetched.getCurrentRevision().getProperties();
        assertEquals(fetchedProps.get("foo"), content);
    }

    /**
     * Note: If nested dictionary or array is immutable, user needs to deep copy which is not convenient.
     *       For Java, returning Map object is user friendly.
     *       We will not fix this issue.
     */
    public void failingTestDocumentPropertiesAreImmutable() throws Exception {
        String jsonString = "{\n" + "    \"name\":\"praying mantis\",\n" + "    \"wikipedia\":{\n"
                + "        \"behavior\":{\n" + "            \"style\":\"predatory\",\n"
                + "            \"attack\":\"ambush\"\n" + "        },\n" + "        \"evolution\":{\n"
                + "            \"ancestor\":\"proto-roaches\",\n" + "            \"cousin\":\"termite\"\n"
                + "        }       \n" + "    }   \n" + "\n" + "}";
        Map map = (Map) Manager.getObjectMapper().readValue(jsonString, Object.class);
        Document doc = createDocumentWithProperties(database, map);
        boolean firstLevelImmutable = false;
        Map<String, Object> props = doc.getProperties();
        try {
            props.put("name", "bug");
        } catch (UnsupportedOperationException e) {
            firstLevelImmutable = true;
        }
        assertTrue(firstLevelImmutable);

        boolean secondLevelImmutable = false;
        Map wikiProps = (Map) props.get("wikipedia");
        try {
            wikiProps.put("behavior", "unknown");
        } catch (UnsupportedOperationException e) {
            secondLevelImmutable = true;
        }
        assertTrue(secondLevelImmutable);

        boolean thirdLevelImmutable = false;
        Map evolutionProps = (Map) wikiProps.get("behavior");
        try {
            evolutionProps.put("movement", "flight");
        } catch (UnsupportedOperationException e) {
            thirdLevelImmutable = true;
        }
        assertTrue(thirdLevelImmutable);
    }

    public void testProvidedMapChangesAreSafe() throws Exception {
        Map<String, Object> originalProps = new HashMap<String, Object>();
        Document doc = createDocumentWithProperties(database, originalProps);
        Map<String, Object> nestedProps = new HashMap<String, Object>();
        nestedProps.put("version", "original");
        UnsavedRevision rev = doc.createRevision();
        rev.getProperties().put("nested", nestedProps);
        rev.save();
        nestedProps.put("version", "changed");
        assertEquals("original", ((Map) doc.getProperty("nested")).get("version"));
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    static public class Foo {
        private String bar;

        public Foo() {
        }

        public String getBar() {
            return bar;
        }

        public void setBar(String bar) {
            this.bar = bar;
        }
    }

    /**
     * Assert that if you add a
     *
     * @throws Exception
     */
    public void testNonPrimitiveTypesInDocument() throws Exception {

        Object fooProperty;
        Map<String, Object> props = new HashMap<String, Object>();

        Foo foo = new Foo();
        foo.setBar("basic");

        props.put("foo", foo);
        Document doc = createDocumentWithProperties(database, props);
        fooProperty = doc.getProperties().get("foo");
        assertTrue(fooProperty instanceof Map);
        assertFalse(fooProperty instanceof Foo);

        Document fetched = database.getDocument(doc.getId());
        fooProperty = fetched.getProperties().get("foo");
        assertTrue(fooProperty instanceof Map);
        assertFalse(fooProperty instanceof Foo);

        ObjectMapper mapper = new ObjectMapper();
        Foo fooResult = mapper.convertValue(fooProperty, Foo.class);
        assertEquals(foo.bar, fooResult.bar);

    }

    public void testDocCustomID() throws Exception {

        Document document = database.getDocument("my_custom_id");
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("foo", "bar");
        document.putProperties(properties);

        Document documentFetched = database.getDocument("my_custom_id");
        assertEquals("my_custom_id", documentFetched.getId());
        assertEquals("bar", documentFetched.getProperties().get("foo"));

    }

    public void testGetPropertiesFromDocNotYetSaved() {
        Document doc = database.createDocument();
        Map<String, Object> properties = doc.getProperties();
        assertNull(properties);
    }

    /**
     * Document.update() - simple successful scenario
     */
    public void testUpdate() throws Exception {
        Document document = database.getDocument("testUpdate");
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("title", "testUpdate");
        document.putProperties(properties);

        final String title = "testUpdate - 2";
        final String notes = "notes - 2";

        document.update(new Document.DocumentUpdater() {
            @Override
            public boolean update(UnsavedRevision newRevision) {
                Map<String, Object> properties = newRevision.getUserProperties();
                properties.put("title", title);
                properties.put("notes", notes);
                newRevision.setUserProperties(properties);
                return true;
            }
        });
        Document document2 = database.getDocument("testUpdate");
        assertEquals(title, document2.getProperties().get("title"));
        assertEquals(notes, document2.getProperties().get("notes"));
    }

    /**
     * Document.update() - simple scenario with failure
     */
    public void testUpdateFalse() throws Exception {
        Document document = database.getDocument("testUpdateFalse");
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("title", "testUpdate");
        document.putProperties(properties);

        final String title = "testUpdate - 2";
        final String notes = "notes - 2";

        document.update(new Document.DocumentUpdater() {
            @Override
            public boolean update(UnsavedRevision newRevision) {
                Map<String, Object> properties = newRevision.getUserProperties();
                properties.put("title", title);
                properties.put("notes", notes);
                newRevision.setUserProperties(properties);
                return false;
            }
        });
        Document document2 = database.getDocument("testUpdateFalse");
        assertEquals("testUpdate", document2.getProperties().get("title"));
        assertNull(document2.getProperties().get("notes"));
    }

    /**
     * Unit Test for https://github.com/couchbase/couchbase-lite-java-core/issues/472
     * <p/>
     * Tries to reproduce the scenario which is described in following comment.
     * https://github.com/couchbase/couchbase-lite-net/issues/388#issuecomment-77637583
     */
    public void testUpdateConflict() throws Exception {
        Document document = database.getDocument("testUpdateConflict");
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("title", "testUpdateConflict");
        document.putProperties(properties);

        final String title1 = "testUpdateConflict - 1";
        final String text1 = "notes - 1";

        final String title2 = "testUpdateConflict - 2";
        final String notes2 = "notes - 2";

        final CountDownLatch latch1 = new CountDownLatch(1);
        final CountDownLatch latch2 = new CountDownLatch(1);

        // Another thread to update document
        // This thread pretends to be Pull replicator update logic
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.w(TAG, "Thread.run() start");

                // wait till main thread finishes to create newRevision
                Log.w(TAG, "Thread.run() latch1.await()");
                try {
                    latch1.await();
                } catch (InterruptedException e) {
                    Log.e(TAG, e.getMessage());
                }

                Log.w(TAG, "Thread.run() exit from latch1.await()");

                Document document1 = database.getDocument("testUpdateConflict");
                Map<String, Object> properties1 = new HashMap<String, Object>();
                properties1.putAll(document1.getProperties());
                properties1.put("title", title1);
                properties1.put("text", text1);
                try {
                    document1.putProperties(properties1);
                } catch (CouchbaseLiteException e) {
                    Log.e(TAG, "[Thread.run()] " + e.getMessage());
                }

                Log.w(TAG, "Thread.run() latch2.countDown()");
                latch2.countDown();

                Log.w(TAG, "Thread.run() end");
            }
        });
        thread.start();

        // main thread to update document
        document.update(new Document.DocumentUpdater() {
            @Override
            public boolean update(UnsavedRevision newRevision) {

                Log.w(TAG, "DocumentUpdater.update() start");

                // after created newRevision wait till other thread to update document.
                Log.w(TAG, "DocumentUpdater.update() latch1.countDown()");
                latch1.countDown();

                Log.w(TAG, "DocumentUpdater.update() latch2.await()");
                try {
                    latch2.await();
                } catch (InterruptedException e) {
                    Log.e(TAG, "[DocumentUpdater.update()]" + e.getMessage());
                }

                Map<String, Object> properties2 = newRevision.getUserProperties();
                properties2.put("title", title2);
                properties2.put("notes", notes2);
                newRevision.setUserProperties(properties2);

                Log.w(TAG, "DocumentUpdater.update() end");
                return true;
            }
        });

        Document document4 = database.getDocument("testUpdateConflict");
        Log.w(TAG, "" + document4.getProperties());
        assertEquals(title2, document4.getProperties().get("title"));
        assertEquals(notes2, document4.getProperties().get("notes"));
        assertEquals(text1, document4.getProperties().get("text"));
    }

    /**
     * Nonsensical CouchbaseLiteException (Conflict) exception thrown on UnsavedRevision.save() #479
     * https://github.com/couchbase/couchbase-lite-java-core/issues/479
     * <p/>
     * Note: this test fails with 1.0.4 or earlier. This test takes time, as default, test is disabled.
     */
    public void disabledTestNonsensicalConflictExceptionOnUnsavedRevision() throws CouchbaseLiteException {

        View testNonsensicalConflict = database.getView("testNonsensicalConflict");
        testNonsensicalConflict.setMap(new Mapper() {
            @Override
            public void map(Map<String, Object> document, Emitter emitter) {
                emitter.emit(null, null);
            }
        }, "");
        Query query = testNonsensicalConflict.createQuery();
        LiveQuery liveQuery = query.toLiveQuery();

        liveQuery.addChangeListener(new LiveQuery.ChangeListener() {
            @Override
            public void changed(LiveQuery.ChangeEvent event) {
                QueryEnumerator rows = event.getRows();
                while (rows.hasNext()) {
                    QueryRow next = rows.next();
                    next.getDocument();
                }
            }
        });
        liveQuery.run();

        // try 100 times to reproduce the issue
        for (int i = 0; i < 1000; i++) {
            Document document = database.createDocument();
            String documentID = document.getId();

            document.putProperties(Collections.<String, Object>singletonMap("test", "1"));
            document = document.getCurrentRevision().getDocument();
            assertEquals(documentID, document.getProperty("_id"));
            assertTrue(((String) document.getProperty("_rev")).startsWith("1-"));

            SavedRevision savedRevision = document.getCurrentRevision();
            savedRevision.createRevision(Collections.<String, Object>singletonMap("test", "2"));
            assertEquals(documentID, document.getProperty("_id"));
            assertTrue(((String) document.getProperty("_rev")).startsWith("2-"));

            document = document.getCurrentRevision().getDocument();

            UnsavedRevision unsavedRevision = document.createRevision();
            unsavedRevision.setProperties(Collections.<String, Object>singletonMap("test", "3"));
            unsavedRevision.save(); //Nonsensical conflict thrown here
            assertEquals(documentID, document.getProperty("_id"));
            assertTrue(((String) document.getProperty("_rev")).startsWith("3-"));

            document = document.getCurrentRevision().getDocument();

            unsavedRevision = document.createRevision();
            unsavedRevision.setProperties(Collections.<String, Object>singletonMap("test", "4"));
            unsavedRevision.save(); //Or here
            assertEquals(documentID, document.getProperty("_id"));
            assertTrue(((String) document.getProperty("_rev")).startsWith("4-"));
        }
    }

    public void testAddChangeListener() throws CouchbaseLiteException, InterruptedException {
        final CountDownLatch documentChanged = new CountDownLatch(1);
        Document doc = database.createDocument();
        doc.addChangeListener(new Document.ChangeListener() {
            @Override
            public void changed(Document.ChangeEvent event) {
                DocumentChange docChange = event.getChange();
                String msg = "New revision added: %s. Conflict: %s";
                msg = String.format(msg, docChange.getAddedRevision(), docChange.isConflict());
                Log.d(TAG, msg);
                documentChanged.countDown();
            }
        });
        doc.createRevision().save();
        boolean success = documentChanged.await(30, TimeUnit.SECONDS);
        assertTrue(success);
    }

    /**
     * https://github.com/couchbase/couchbase-lite-android/issues/563
     * Updating a document in a transaction block twice using Document.DocumentUpdater results in
     * an infinite loop
     * <p/>
     * NOTE: Use Document.update()
     */
    public void testMultipleUpdatesInTransactionWithUpdate() throws CouchbaseLiteException {
        final Document doc = database.createDocument();
        HashMap<String, Object> properties = new HashMap<String, Object>();
        properties.put("key", "value1");
        doc.putProperties(properties);
        database.runInTransaction(new TransactionalTask() {
            @Override
            public boolean run() {
                try {
                    doc.update(new Document.DocumentUpdater() {
                        @Override
                        public boolean update(UnsavedRevision newRevision) {
                            Log.i(TAG, "Trying to update key to value 2");
                            Map<String, Object> properties = newRevision.getUserProperties();
                            properties.put("key", "value2");
                            newRevision.setUserProperties(properties);
                            return true;
                        }
                    });
                    doc.update(new Document.DocumentUpdater() {
                        @Override
                        public boolean update(UnsavedRevision newRevision) {
                            Log.i(TAG, "Trying to update key to value 3");
                            Map<String, Object> properties = newRevision.getUserProperties();
                            properties.put("key", "value3");
                            newRevision.setUserProperties(properties);
                            return true;
                        }
                    });
                } catch (CouchbaseLiteException e) {
                    Log.e(TAG, "Trying to update key to value 2 or 3");
                    fail("Trying to update key to value 2 or 3, but failed.");
                    return false;
                }
                return true;
            }
        });
        Map<String, Object> properties4 = doc.getProperties();
        Log.i(TAG, "properties4 = " + properties4);
    }

    /**
     * NOTE: This is variation of testMultipleUpdatesInTransactionWithUpdate() test
     * with using Document.putProperties()
     */
    public void testMultipleUpdatesInTransactionWithPutProperties() throws CouchbaseLiteException {
        final Document doc = database.createDocument();
        HashMap<String, Object> properties1 = new HashMap<String, Object>();
        properties1.put("key", "value1");
        doc.putProperties(properties1);
        assertTrue(database.runInTransaction(new TransactionalTask() {
            @Override
            public boolean run() {
                try {
                    Map<String, Object> properties2 = new HashMap<String, Object>(doc.getProperties());
                    properties2.put("key", "value2");
                    doc.putProperties(properties2);
                } catch (CouchbaseLiteException e) {
                    Log.e(TAG, "Trying to update key to value 2");
                    fail("Trying to update key to value 2, but failed.");
                    return false;
                }
                try {
                    Map<String, Object> properties3 = new HashMap<String, Object>(doc.getProperties());
                    properties3.put("key", "value3");
                    doc.putProperties(properties3);
                } catch (CouchbaseLiteException e) {
                    Log.e(TAG, "Trying to update key to value 3");
                    fail("Trying to update key to value 3, but failed.");
                    return false;
                }
                return true;
            }
        }));
        Map<String, Object> properties4 = doc.getProperties();
        Log.i(TAG, "properties4 = " + properties4);
        assertEquals("value3", properties4.get("key"));
    }

    public static SavedRevision createRevisionWithProps(SavedRevision createRevFrom, Map<String, Object> properties,
            boolean allowConflict) throws Exception {
        UnsavedRevision unsavedRevision = createRevFrom.createRevision();
        unsavedRevision.setUserProperties(properties);
        return unsavedRevision.save(allowConflict);
    }

    public void testResolveConflict() throws Exception {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put("testName", "testResolveConflict");
        properties.put("key", "1");

        final Document doc = database.getDocument("testResolveConflict");

        UnsavedRevision newRev1 = doc.createRevision();
        newRev1.setUserProperties(properties);
        SavedRevision rev1 = newRev1.save();

        Map<String, Object> props1 = new HashMap<String, Object>();
        props1.put("key", "2a");
        SavedRevision rev2a = createRevisionWithProps(rev1, props1, false);
        Map<String, Object> props2 = new HashMap<String, Object>();
        props2.put("key", "2b");
        SavedRevision rev2b = createRevisionWithProps(rev1, props2, true);
        Map<String, Object> props3 = new HashMap<String, Object>();
        props3.put("key", "2c");
        SavedRevision rev2c = createRevisionWithProps(rev1, props3, true);

        final List<SavedRevision> conflicts = doc.getConflictingRevisions();
        if (conflicts.size() > 1) {
            // There is more than one current revision, thus a conflict!
            assertTrue(database.runInTransaction(new TransactionalTask() {
                @Override
                public boolean run() {
                    try {
                        // Come up with a merged/resolved document in some way that's
                        // appropriate for the app. You could even just pick the body of
                        // one of the revisions.
                        Map<String, Object> mergedProps = new HashMap<String, Object>(
                                conflicts.get(0).getUserProperties());
                        mergedProps.put("key", "3");

                        // Delete the conflicting revisions to get rid of the conflict:
                        SavedRevision current = doc.getCurrentRevision();
                        for (SavedRevision rev : conflicts) {
                            UnsavedRevision newRev = rev.createRevision();
                            if (rev.getId().equals(current.getId())) {
                                newRev.setProperties(mergedProps);
                            } else {
                                newRev.setIsDeletion(true);
                            }
                            // saveAllowingConflict allows 'rev' to be updated even if it
                            // is not the document's current revision.
                            newRev.save(true);
                        }
                    } catch (CouchbaseLiteException e) {
                        return false;
                    }
                    return true;
                }
            }));
        }
        assertEquals(1, doc.getConflictingRevisions().size());
        assertEquals("3", doc.getProperties().get("key"));
    }

    public void testResolveConflictInChangeListener() throws Exception {
        Map<String, Object> properties = new TreeMap<String, Object>();
        properties.put("foo", "bar");

        Document doc = database.createDocument();
        UnsavedRevision rev1 = doc.createRevision();
        rev1.setProperties(properties);
        SavedRevision rev1Saved = rev1.save();

        UnsavedRevision rev2a = rev1Saved.createRevision();
        properties.put("what", "rev2a");
        rev2a.setUserProperties(properties);
        SavedRevision rev2aSaved = rev2a.save(true);

        UnsavedRevision rev2b = rev1Saved.createRevision();
        properties.put("what", "rev2b");
        rev2b.setUserProperties(properties);
        rev2b.save(true);

        final CountDown counter = new CountDown(2);
        database.addChangeListener(new Database.ChangeListener() {
            @Override
            public void changed(Database.ChangeEvent event) {
                Log.e(TAG, "changed() event=%s", event);
                counter.countDown();
                try {
                    List<DocumentChange> changes = event.getChanges();
                    Log.e(TAG, "changed() changes.size()=%d", changes.size());
                    int conflictsInDocumentChange = 0;
                    for (DocumentChange documentChange : changes) {
                        Log.e(TAG, "changed() documentChange.isConflict()=%b", documentChange.isConflict());
                        if (documentChange.isConflict()) {
                            conflictsInDocumentChange++;
                            Document document = database.getDocument(documentChange.getDocumentId());
                            List<SavedRevision> conflictRevisions = document.getConflictingRevisions();
                            if (conflictRevisions.size() > 1) {
                                for (SavedRevision conflictingRevision : conflictRevisions) {
                                    UnsavedRevision newRevision = conflictingRevision.createRevision();
                                    if (!conflictingRevision.equals(document.getCurrentRevision())) {
                                        newRevision.setIsDeletion(true);
                                    }
                                    SavedRevision srev = newRevision.save(true);
                                    Log.e(TAG, "SavedRevision=%s", srev);
                                }
                            }
                        }
                    }
                    Log.e(TAG, "conflictsInDocumentChange=%d", conflictsInDocumentChange);
                    if (counter.getCount() == 1)
                        assertEquals(1, conflictsInDocumentChange);
                    else if (counter.getCount() == 0)
                        assertEquals(2, conflictsInDocumentChange);
                } catch (Exception e) {
                    Log.e(TAG, "Error in resolving conflict", e);
                }
            }
        });

        UnsavedRevision rev2c = rev1Saved.createRevision();
        properties.put("what", "rev2c");
        rev2c.setUserProperties(properties);
        rev2c.save(true);

        // Without fix, ChangeListener.changed() method called more than twice.
        assertEquals(0, counter.getCount());
    }
}