/** * Original iOS version by Jens Alfke * Ported to Android by Marty Schoch * <p/> * Copyright (c) 2012 Couchbase, Inc. All rights reserved. * <p/> * 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 * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * 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.AttachmentInternal; import com.couchbase.lite.internal.RevisionInternal; import com.couchbase.lite.support.Base64; import com.couchbase.lite.support.security.SymmetricKeyException; import com.couchbase.lite.util.Log; import com.couchbase.lite.util.TextUtils; import junit.framework.Assert; import org.apache.commons.io.IOUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.zip.GZIPOutputStream; /** * Tests ported from DatabaseAttachment_Tests.m */ public class DatabaseAttachmentTest extends LiteTestCaseWithDB { public static final String TAG = "Attachments"; /** * in DatabaseAttachment_Tests.m * - (void) test10_Attachments */ @SuppressWarnings("unchecked") public void testAttachments() throws Exception { BlobStore attachments = database.getAttachmentStore(); Assert.assertEquals(0, attachments.count()); Assert.assertEquals(new HashSet<Object>(), attachments.allKeys()); // Add a revision and an attachment to it: Status status = new Status(Status.OK); byte[] attach1 = "This is the body of attach1".getBytes(); Map<String, Object> props = new HashMap<String, Object>(); props.put("foo", 1); props.put("bar", false); props.put("_attachments", getAttachmentsDict(attach1, "attach", "text/plain", false)); RevisionInternal rev1 = database.putRevision(new RevisionInternal(props), null, false, status); Assert.assertEquals(Status.CREATED, status.getCode()); AttachmentInternal att = database.getAttachment(rev1, "attach"); Assert.assertNotNull(att); Log.i(TAG, new String(att.getContent())); Assert.assertTrue(Arrays.equals(attach1, att.getContent())); Assert.assertEquals("text/plain", att.getContentType()); Assert.assertEquals(AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, att.getEncoding()); // Check the attachment dict: Map<String, Object> itemDict = new HashMap<String, Object>(); itemDict.put("content_type", "text/plain"); itemDict.put("digest", "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE="); itemDict.put("length", 27); itemDict.put("stub", true); itemDict.put("revpos", 1); Map<String, Object> attachmentDict = new HashMap<String, Object>(); attachmentDict.put("attach", itemDict); RevisionInternal gotRev1 = database.getDocument(rev1.getDocID(), rev1.getRevID(), true); Map<String, Object> gotAttachmentDict = (Map<String, Object>) gotRev1.getProperties().get("_attachments"); Assert.assertEquals(attachmentDict, gotAttachmentDict); // Check the attachment dict, with attachments included: itemDict.remove("stub"); itemDict.put("data", Base64.encodeBytes(attach1)); gotRev1 = database.getDocument(rev1.getDocID(), rev1.getRevID(), true); RevisionInternal expandedRev = gotRev1.copy(); Assert.assertTrue(database.expandAttachments(expandedRev, 0, false, true, status)); Assert.assertEquals(attachmentDict, expandedRev.getAttachments()); // Add a second revision that doesn't update the attachment: props = new HashMap<String, Object>(); props.put("_id", rev1.getDocID()); props.put("foo", 2); props.put("bazz", false); props.put("_attachments", getAttachmentsStub("attach")); RevisionInternal rev2 = database.putRevision(new RevisionInternal(props), rev1.getRevID(), false, status); Assert.assertEquals(Status.CREATED, status.getCode()); // Add a third revision of the same document: byte[] attach2 = "<html>And this is attach2</html>".getBytes(); props = new HashMap<String, Object>(); props.put("_id", rev2.getDocID()); props.put("foo", 2); props.put("bazz", false); props.put("_attachments", getAttachmentsDict(attach2, "attach", "text/html", false)); RevisionInternal rev3 = database.putRevision(new RevisionInternal(props), rev2.getRevID(), false, status); Assert.assertEquals(Status.CREATED, status.getCode()); // Check the 2nd revision's attachment: att = database.getAttachment(rev2, "attach"); Assert.assertNotNull(att); Assert.assertEquals("text/plain", att.getContentType()); Assert.assertEquals(AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, att.getEncoding()); Assert.assertTrue(Arrays.equals(attach1, att.getContent())); expandedRev = rev2.copy(); Assert.assertTrue(database.expandAttachments(expandedRev, 2, false, true, status)); attachmentDict = new HashMap<String, Object>(); itemDict = new HashMap<String, Object>(); itemDict.put("stub", true); itemDict.put("revpos", 1); attachmentDict.put("attach", itemDict); Assert.assertEquals(attachmentDict, expandedRev.getAttachments()); // Check the 3rd revision's attachment: att = database.getAttachment(rev3, "attach"); Assert.assertNotNull(att); Assert.assertEquals("text/html", att.getContentType()); Assert.assertEquals(AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, att.getEncoding()); Assert.assertTrue(Arrays.equals(attach2, att.getContent())); expandedRev = rev3.copy(); Assert.assertTrue(database.expandAttachments(expandedRev, 2, false, true, status)); attachmentDict = new HashMap<String, Object>(); itemDict = new HashMap<String, Object>(); itemDict.put("content_type", "text/html"); itemDict.put("data", "PGh0bWw+QW5kIHRoaXMgaXMgYXR0YWNoMjwvaHRtbD4="); itemDict.put("digest", "sha1-s14XRTXlwvzYfjo1t1u0rjB+ZUA="); itemDict.put("length", 32); itemDict.put("revpos", 3); attachmentDict.put("attach", itemDict); Map<String, Object> data = expandedRev.getAttachments(); Assert.assertEquals(attachmentDict, expandedRev.getAttachments()); // Examine the attachment store: Assert.assertEquals(2, attachments.count()); Set<BlobKey> expected = new HashSet<BlobKey>(); expected.add(BlobStore.keyForBlob(attach1)); expected.add(BlobStore.keyForBlob(attach2)); Assert.assertEquals(expected, attachments.allKeys()); database.compact(); // This clears the body of the first revision Assert.assertEquals(1, attachments.count()); Set<BlobKey> expected2 = new HashSet<BlobKey>(); expected2.add(BlobStore.keyForBlob(attach2)); Assert.assertEquals(expected2, attachments.allKeys()); } /** * in DatabaseAttachment_Tests.m * - (void) test11_PutAttachment */ @SuppressWarnings("unchecked") public void test11_PutAttachment() throws Exception { // Put a revision that includes an _attachments dict: RevisionInternal rev1 = putDocWithAttachment(null, "This is the body of attach1", false); Map<String, Object> itemDict = new HashMap<String, Object>(); itemDict.put("content_type", "text/plain"); itemDict.put("digest", "sha1-gOHUOBmIMoDCrMuGyaLWzf1hQTE="); itemDict.put("length", 27); itemDict.put("stub", true); itemDict.put("revpos", 1); Map<String, Object> attachmentDict = new HashMap<String, Object>(); attachmentDict.put("attach", itemDict); Assert.assertEquals(attachmentDict, rev1.getAttachments()); // Examine the attachment store: Assert.assertEquals(database.getAttachmentStore().count(), 1); // Get the revision: RevisionInternal gotRev1 = database.getDocument(rev1.getDocID(), rev1.getRevID(), true); Assert.assertEquals(attachmentDict, gotRev1.getAttachments()); // Update the attachment directly: boolean gotExpectedErrorCode = false; byte[] attachv2 = "Replaced body of attach".getBytes(); try { database.updateAttachment("attach", blobForData(database, attachv2), "application/foo", AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, rev1.getDocID(), null, null); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.getCBLStatus().getCode() == Status.CONFLICT); } Assert.assertTrue(gotExpectedErrorCode); gotExpectedErrorCode = false; try { database.updateAttachment("attach", blobForData(database, attachv2), "application/foo", AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, rev1.getDocID(), "1-deadbeef", null); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.getCBLStatus().getCode() == Status.CONFLICT); } Assert.assertTrue(gotExpectedErrorCode); RevisionInternal rev2 = database.updateAttachment("attach", blobForData(database, attachv2), "application/foo", AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, rev1.getDocID(), rev1.getRevID(), null); Assert.assertNotNull(rev2); Assert.assertEquals(rev1.getDocID(), rev2.getDocID()); Assert.assertEquals(2, rev2.getGeneration()); // Get the updated revision: RevisionInternal gotRev2 = database.getDocument(rev2.getDocID(), rev2.getRevID(), true); itemDict = new HashMap<String, Object>(); itemDict.put("content_type", "application/foo"); itemDict.put("digest", "sha1-mbT3208HI3PZgbG4zYWbDW2HsPk="); itemDict.put("length", 23); itemDict.put("stub", true); itemDict.put("revpos", 2); attachmentDict = new HashMap<String, Object>(); attachmentDict.put("attach", itemDict); Assert.assertEquals(attachmentDict, gotRev2.getAttachments()); // Delete the attachment: gotExpectedErrorCode = false; try { database.updateAttachment("nosuchattach", null, null, AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, rev2.getDocID(), rev2.getRevID(), null); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.getCBLStatus().getCode() == Status.NOT_FOUND); } Assert.assertTrue(gotExpectedErrorCode); gotExpectedErrorCode = false; try { database.updateAttachment("nosuchattach", null, null, AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, "nosuchdoc", "nosuchrev", null); } catch (CouchbaseLiteException e) { gotExpectedErrorCode = (e.getCBLStatus().getCode() == Status.NOT_FOUND); } Assert.assertTrue(gotExpectedErrorCode); RevisionInternal rev3 = database.updateAttachment("attach", null, null, AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, rev2.getDocID(), rev2.getRevID(), null); Assert.assertNotNull(rev2); Assert.assertEquals(rev2.getDocID(), rev3.getDocID()); Assert.assertEquals(3, rev3.getGeneration()); // Get the updated revision: RevisionInternal gotRev3 = database.getDocument(rev3.getDocID(), rev3.getRevID(), true); Assert.assertNull(gotRev3.getAttachments()); } /** * - (void) test13_GarbageCollectAttachments */ public void test13_GarbageCollectAttachments() throws CouchbaseLiteException { List<RevisionInternal> revs = new ArrayList<RevisionInternal>(); for (int i = 0; i < 100; i++) revs.add(this.putDocWithAttachment(String.format(Locale.ENGLISH, "doc-%d", i), String.format(Locale.ENGLISH, "Attachment #%d", i), false)); for (int i = 0; i < 40; i++) { revs.set(i, database.updateAttachment( "attach", null, null, AttachmentInternal.AttachmentEncoding.AttachmentEncodingNone, revs.get(i).getDocID(), revs.get(i).getRevID(), null)); } database.compact(); assertEquals(60, database.getAttachmentStore().count()); } public void testAddAndGetAttachment() throws CouchbaseLiteException { Document document = database.createDocument(); UnsavedRevision rev = document.createRevision(); byte[] attach = "This is the body of attach".getBytes(); ; InputStream in = new ByteArrayInputStream(attach); rev.setAttachment("attach", "text/plain", in); assertNotNull(rev.getAttachment("attach")); assertEquals(1, rev.getAttachments().size()); rev.save(); } public void testStreamAttachmentBlobStoreWriter() throws Exception { BlobStore attachments = database.getAttachmentStore(); BlobStoreWriter blobWriter = new com.couchbase.lite.BlobStoreWriter(attachments); String testBlob = "foo"; blobWriter.appendData(new String(testBlob).getBytes()); blobWriter.finish(); String sha1Base64Digest = "sha1-C+7Hteo/D9vJXQ3UfzxbwnXaijM="; Assert.assertEquals(blobWriter.sHA1DigestString(), sha1Base64Digest); Assert.assertEquals(blobWriter.mD5DigestString(), "md5-rL0Y20zC+Fzt72VPzMSk2A=="); // install it blobWriter.install(); // look it up in blob store and make sure it's there BlobKey blobKey = new BlobKey(sha1Base64Digest); byte[] blob = attachments.blobForKey(blobKey); Assert.assertTrue(Arrays.equals(testBlob.getBytes(Charset.forName("UTF-8")), blob)); } /** * https://github.com/couchbase/couchbase-lite-android/issues/134 */ public void testGetAttachmentBodyUsingPrefetch() throws CouchbaseLiteException, IOException { // add a doc with an attachment Document doc = database.createDocument(); UnsavedRevision rev = doc.createRevision(); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("foo", "bar"); rev.setUserProperties(properties); final byte[] attachBodyBytes = "attach body".getBytes(); Attachment attachment = new Attachment( new ByteArrayInputStream(attachBodyBytes), "text/plain" ); final String attachmentName = "test_attachment.txt"; rev.addAttachment(attachment, attachmentName); rev.save(); // do query that finds that doc with prefetch View view = database.getView("aview"); view.setMapReduce(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { String id = (String) document.get("_id"); emitter.emit(id, null); } }, null, "1"); // try to get the attachment Query query = view.createQuery(); query.setPrefetch(true); QueryEnumerator results = query.run(); while (results.hasNext()) { QueryRow row = results.next(); // This returns the revision just fine, but the sequence number // is set to 0. SavedRevision revision = row.getDocument().getCurrentRevision(); // This returns an Attachment object which looks ok, except again // its sequence number is 0. The metadata property knows about // the length and mime type of the attachment. It also says // "stub" -> "true". Attachment attachmentRetrieved = revision.getAttachment(attachmentName); // This throws a CouchbaseLiteException with Status.NOT_FOUND. InputStream is = attachmentRetrieved.getContent(); assertNotNull(is); byte[] attachmentDataRetrieved; try { attachmentDataRetrieved = TextUtils.read(is); } finally { is.close(); } String attachmentDataRetrievedString = new String(attachmentDataRetrieved); String attachBodyString = new String(attachBodyBytes); assertEquals(attachBodyString, attachmentDataRetrievedString); } } /** * Regression test for https://github.com/couchbase/couchbase-lite-java-core/issues/218 */ public void testGetAttachmentAfterItDeleted() throws CouchbaseLiteException, IOException { // add a doc with an attachment Document doc = database.createDocument(); UnsavedRevision rev = doc.createRevision(); final byte[] attachBodyBytes = "attach body".getBytes(); Attachment attachment = new Attachment( new ByteArrayInputStream(attachBodyBytes), "text/plain" ); String attachmentName = "test_delete_attachment.txt"; rev.addAttachment(attachment, attachmentName); rev.save(); UnsavedRevision rev1 = doc.createRevision(); Attachment currentAttachment = rev1.getAttachment(attachmentName); assertNotNull(currentAttachment); rev1.removeAttachment(attachmentName); currentAttachment = rev1.getAttachment(attachmentName); assertNull(currentAttachment); // otherwise NullPointerException when currentAttachment.getMetadata() rev1.save(); currentAttachment = doc.getCurrentRevision().getAttachment(attachmentName); assertNull(currentAttachment); // otherwise NullPointerException when currentAttachment.getMetadata() } /** * Regression test for https://github.com/couchbase/couchbase-lite-android-core/issues/70 */ public void testAttachmentDisappearsAfterSave() throws CouchbaseLiteException, IOException { // create a doc with an attachment Document doc = database.createDocument(); String content = "This is a test attachment!"; ByteArrayInputStream body = new ByteArrayInputStream(content.getBytes()); UnsavedRevision rev = doc.createRevision(); rev.setAttachment("index.html", "text/plain; charset=utf-8", body); rev.save(); // make sure the doc's latest revision has the attachment Map<String, Object> attachments = (Map) doc.getCurrentRevision().getProperty("_attachments"); assertNotNull(attachments); assertEquals(1, attachments.size()); // make sure the rev has the attachment attachments = (Map) rev.getProperty("_attachments"); assertNotNull(attachments); assertEquals(1, attachments.size()); // create new properties to add Map<String, Object> properties = new HashMap<String, Object>(); properties.put("foo", "bar"); // make sure the new rev still has the attachment UnsavedRevision rev2 = doc.createRevision(); rev2.getProperties().putAll(properties); rev2.save(); attachments = (Map) rev2.getProperty("_attachments"); assertNotNull(attachments); assertEquals(1, attachments.size()); } /** * attempt to reproduce https://github.com/couchbase/couchbase-lite-android/issues/328 & * https://github.com/couchbase/couchbase-lite-android/issues/325 */ public void testSetAttachmentsSequentially() throws CouchbaseLiteException, IOException { try { //Create rev1 of document with just properties Document doc = database.createDocument(); String id = doc.getId(); Map<String, Object> docProperties = new HashMap<String, Object>(); docProperties.put("Iteration", 0); doc.putProperties(docProperties); UnsavedRevision rev = null; //Create a new revision with attachment1 InputStream attachmentStream1 = getAsset("attachment.png"); doc = database.getDocument(id);//not required byte[] jsonb = Manager.getObjectMapper().writeValueAsBytes(doc.getProperties().get("_attachments")); Log.d(Database.TAG, "Doc _rev = %s", doc.getProperties().get("_rev")); Log.d(Database.TAG, "Doc properties = %s", new String(jsonb)); rev = doc.createRevision(); rev.setAttachment("attachment1", "image/png", attachmentStream1); rev.save(); attachmentStream1.close(); //Create a new revision updated properties doc = database.getDocument(id);//not required jsonb = Manager.getObjectMapper().writeValueAsBytes(doc.getProperties().get("_attachments")); Log.d(Database.TAG, "Doc _rev = %s", doc.getProperties().get("_rev")); Log.d(Database.TAG, "Doc properties = %s", new String(jsonb)); Map<String, Object> curProperties; curProperties = doc.getProperties(); docProperties = new HashMap<String, Object>(); docProperties.putAll(curProperties); docProperties.put("Iteration", 1); doc.putProperties(docProperties); //Create a new revision with attachment2 InputStream attachmentStream2 = getAsset("attachment.png"); doc = database.getDocument(id);//not required jsonb = Manager.getObjectMapper().writeValueAsBytes(doc.getProperties().get("_attachments")); Log.d(Database.TAG, "Doc _rev = %s", doc.getProperties().get("_rev")); Log.d(Database.TAG, "Doc properties = %s", new String(jsonb)); rev = doc.createRevision(); rev.setAttachment("attachment2", "image/png", attachmentStream2); rev.save(); attachmentStream2.close(); //Assert final document revision doc = database.getDocument(id); curProperties = doc.getProperties(); assertEquals(4, curProperties.size()); Map<String, Object> attachments = (Map<String, Object>) doc.getCurrentRevision().getProperty("_attachments"); assertNotNull(attachments); assertEquals(2, attachments.size()); } catch (CouchbaseLiteException e) { Log.e(Database.TAG, "Error adding attachment: " + e.getMessage(), e); fail(); } } /** * attempt to reproduce * - https://github.com/couchbase/couchbase-lite-android/issues/328 & * - https://github.com/couchbase/couchbase-lite-android/issues/325 */ public void testSetAttachmentsSequentiallyInTransaction() throws CouchbaseLiteException, IOException { boolean success = database.runInTransaction(new TransactionalTask() { public boolean run() { try { // add a doc with an attachment Document doc = database.createDocument(); String id = doc.getId(); InputStream jsonStream = getAsset("300k.json"); Map<String, Object> docProperties = null; docProperties = Manager.getObjectMapper().readValue(jsonStream, Map.class); docProperties.put("Iteration", 0); doc.putProperties(docProperties); jsonStream.close(); UnsavedRevision rev = null; for (int i = 0; i < 5; i++) { InputStream attachmentStream1 = getAsset("attachment.png"); Log.e(Database.TAG, "TEST ITERATION " + i); doc = database.getDocument(id);//not required rev = doc.createRevision(); rev.setAttachment("attachment " + i * 3, "image/png", attachmentStream1); rev.save(); attachmentStream1.close(); InputStream attachmentStream2 = getAsset("attachment.png"); doc = database.getDocument(id);//not required rev = doc.createRevision(); rev.setAttachment("attachment " + i * 3 + 1, "image/png", attachmentStream2); rev.save(); attachmentStream2.close(); InputStream attachmentStream3 = getAsset("attachment.png"); doc = database.getDocument(id);//not required rev = doc.createRevision(); rev.setAttachment("attachment " + i * 3 + 2, "image/png", attachmentStream3); rev.save(); attachmentStream3.close(); Map<String, Object> curProperties; doc = database.getDocument(id);//not required curProperties = doc.getProperties(); docProperties = new HashMap<String, Object>(); docProperties.putAll(curProperties); docProperties.put("Iteration", (i + 1) * 3); doc.putProperties(docProperties); } Map<String, Object> curProperties; doc = database.getDocument(id);//not required curProperties = doc.getProperties(); assertEquals(22, curProperties.size()); Map<String, Object> attachments = (Map<String, Object>) doc.getCurrentRevision().getProperty("_attachments"); assertNotNull(attachments); assertEquals(15, attachments.size()); } catch (Exception e) { Log.e(Database.TAG, "Error deserializing properties from JSON", e); return false; } return true; } }); assertTrue("transaction with set attachments sequentially failed", success); } /** * Regression test for https://github.com/couchbase/couchbase-lite-android-core/issues/70 */ public void testAttachmentInstallBodies() throws Exception { Map<String, Object> attachmentsMap = new HashMap<String, Object>(); Map<String, Object> attachmentMap = new HashMap<String, Object>(); attachmentMap.put("length", 25); String attachmentName = "index.html"; attachmentsMap.put(attachmentName, attachmentMap); Map<String, Object> updatedAttachments = Attachment.installAttachmentBodies(attachmentsMap, database); assertTrue(updatedAttachments.size() > 0); assertTrue(updatedAttachments.containsKey(attachmentName)); } public void testGetContentURL() throws Exception { String attachmentName = "index.html"; String content = "This is a test attachment!"; Document doc = createDocWithAttachment(database, attachmentName, content); Attachment attachment = doc.getCurrentRevision().getAttachment(attachmentName); URL url = attachment.getContentURL(); assertNotNull(url); FileInputStream fis = new FileInputStream(new File(url.toURI())); byte[] buffer = new byte[1024]; int len = fis.read(buffer); assertTrue(len != -1); String content2 = new String(buffer, 0, len); assertEquals(content, content2); fis.close(); } public void testAttachmentThrowIoException() { InputStream in = new InputStream() { @Override public int read() throws IOException { throw new IOException(); } }; Document doc = database.createDocument(); UnsavedRevision rev = doc.createRevision(); rev.setAttachment("ioe_attach", "text/plain", in); try { rev.save(); fail("Saved revision with corrupt attachment"); } catch (CouchbaseLiteException expected) { assertEquals(Status.ATTACHMENT_ERROR, expected.getCBLStatus().getCode()); } } public void testGetAttachmentFromUnsavedRevision() throws Exception { String attachmentName = "index.html"; String content = "This is a test attachment!"; Document doc = createDocWithAttachment(database, attachmentName, content); UnsavedRevision rev = doc.createRevision(); Attachment attachment = rev.getAttachment(attachmentName); assertNotNull(attachment); InputStream in = attachment.getContent(); assertNotNull(in); assertEquals(IOUtils.toString(in, "UTF-8"), content); in.close(); } public void testGetAttachmentFromUnsavedRevisionWithMultipleRevisions() throws Exception { String attachmentName = "index.html"; String content = "This is a test attachment!"; Document doc = createDocWithAttachment(database, attachmentName, content); // added extra two revisions to make sure in case the revision that has an attachment is // more than two generation older than current. doc.createRevision().save(); doc.createRevision().save(); UnsavedRevision rev = doc.createRevision(); Attachment attachment = rev.getAttachment(attachmentName); assertNotNull(attachment); InputStream in = attachment.getContent(); assertNotNull(in); assertEquals(IOUtils.toString(in, "UTF-8"), content); in.close(); } public void testGzippedAttachments() throws Exception { String attachmentName = "index.html"; byte content[] = "This is a test attachment!".getBytes("UTF-8"); ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); GZIPOutputStream gzipOut = new GZIPOutputStream(byteOut); gzipOut.write(content); gzipOut.close(); byte contentGzipped[] = byteOut.toByteArray(); Document doc = database.createDocument(); UnsavedRevision rev = doc.createRevision(); rev.setAttachment(attachmentName, "text/html", new ByteArrayInputStream(contentGzipped)); rev.save(); SavedRevision savedRev = doc.getCurrentRevision(); Attachment attachment = savedRev.getAttachment(attachmentName); // As far as revision users are concerned their data is not gzipped InputStream in = attachment.getContent(); assertNotNull(in); assertTrue(Arrays.equals(content, IOUtils.toByteArray(in))); in.close(); Document gotDoc = database.getDocument(doc.getId()); Revision gotRev = gotDoc.getCurrentRevision(); Attachment gotAtt = gotRev.getAttachment(attachmentName); in = gotAtt.getContent(); assertNotNull(in); assertTrue(Arrays.equals(content, IOUtils.toByteArray(in))); in.close(); } // Store Gzipped attachment by Base64 encoding public void testGzippedAttachmentByBase64() throws Exception { String attachmentName = "attachment.png"; // 1. store attachment with doc // 1.a load attachment data from asset InputStream attachmentStream = getAsset(attachmentName); ByteArrayOutputStream baos = new ByteArrayOutputStream(); IOUtils.copy(attachmentStream, baos); baos.close(); attachmentStream.close(); byte[] bytes = baos.toByteArray(); // 1.b apply GZIP + Base64 String attachmentBase64 = Base64.encodeBytes(bytes, Base64.GZIP); // 1.c attachment Map object Map<String, Object> attachmentMap = new HashMap<String, Object>(); attachmentMap.put("content_type", "image/png"); attachmentMap.put("data", attachmentBase64); attachmentMap.put("encoding", "gzip"); attachmentMap.put("length", bytes.length); // 1.d attachments Map object Map<String, Object> attachmentsMap = new HashMap<String, Object>(); attachmentsMap.put(attachmentName, attachmentMap); // 1.e document property Map object Map<String, Object> propsMap = new HashMap<String, Object>(); propsMap.put("_attachments", attachmentsMap); // 1.f store document into database Document putDoc = database.createDocument(); putDoc.putProperties(propsMap); String docId = putDoc.getId(); // 2. Load attachment from database and compare it with original // 2.a load doc and attachment from database Document getDoc = database.getDocument(docId); Attachment attachment = getDoc.getCurrentRevision().getAttachment(attachmentName); assertEquals(bytes.length, attachment.getLength()); assertEquals("image/png", attachment.getContentType()); assertEquals("gzip", attachment.getMetadata().get("encoding")); InputStream is = attachment.getContent(); byte[] receivedBytes = getBytesFromInputStream(is); assertEquals(bytes.length, receivedBytes.length); is.close(); assertTrue(Arrays.equals(bytes, receivedBytes)); } private RevisionInternal putDocWithAttachment(String docID, String attachmentText, boolean compress) throws CouchbaseLiteException { byte[] attachmentData = attachmentText.getBytes(); String encoding = null; int length = 0; if (compress) { length = attachmentData.length; encoding = "gzip"; // encode } String base64 = Base64.encodeBytes(attachmentData); Map<String, Object> itemDict = new HashMap<String, Object>(); itemDict.put("content_type", "text/plain"); itemDict.put("data", base64); itemDict.put("encoding", encoding); itemDict.put("length", length == 0 ? null : length); Map<String, Object> attachmentDict = new HashMap<String, Object>(); attachmentDict.put("attach", itemDict); Map<String, Object> props = new HashMap<String, Object>(); props.put("_id", docID); props.put("foo", 1); props.put("bar", false); props.put("_attachments", attachmentDict); Status status = new Status(Status.OK); RevisionInternal rev = database.putRevision(new RevisionInternal(props), null, false, status); Assert.assertEquals(Status.CREATED, status.getCode()); return rev; } private static byte[] getBytesFromInputStream(InputStream is) { org.apache.commons.io.output.ByteArrayOutputStream os = new org.apache.commons.io.output.ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len = 0; try { while ((len = is.read(buffer)) > 0) { os.write(buffer, 0, len); } os.flush(); } catch (IOException e) { Log.e(Log.TAG, "is.read(buffer) or os.flush() error", e); return null; } return os.toByteArray(); } private static Map<String, Map<String, Object>> getAttachmentsDict(byte[] data, String name, String type, boolean gzipped) { if (gzipped) // TODO ; Map<String, Object> att = new HashMap<String, Object>(); att.put("content_type", type); att.put("data", data); if (gzipped) att.put("encoding", "gzip"); Map<String, Map<String, Object>> atts = new HashMap<String, Map<String, Object>>(); atts.put(name, att); return atts; } private static Map<String, Map<String, Object>> getAttachmentsStub(String name) { Map<String, Object> att = new HashMap<String, Object>(); att.put("stub", true); Map<String, Map<String, Object>> atts = new HashMap<String, Map<String, Object>>(); atts.put(name, att); return atts; } private static BlobStoreWriter blobForData(Database db, byte[] data) throws SymmetricKeyException { try { BlobStoreWriter blob = db.getAttachmentWriter(); blob.appendData(data); blob.finish(); return blob; } catch (IOException e) { return null; } } }