/** * 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()); } }