/** The revision ID of the document this row was mapped from. */
 @InterfaceAudience.Public
 public String getDocumentRevisionId() {
   // Get the revision id from either the embedded document contents,
   // or the '_rev' or 'rev' value key:
   String rev = null;
   if (documentRevision != null) rev = documentRevision.getRevID();
   if (rev == null) {
     if (value instanceof Map) {
       Map<String, Object> mapValue = (Map<String, Object>) value;
       rev = (String) mapValue.get("_rev");
       if (rev == null) {
         rev = (String) mapValue.get("rev");
       }
     }
   }
   return rev;
 }
  /** - (void) processInbox: (CBL_RevisionList*)changes in CBLRestPusher.m */
  @Override
  @InterfaceAudience.Private
  protected void processInbox(final RevisionList changes) {

    Log.v(Log.TAG_SYNC, "processInbox() changes=" + changes.size());

    // Generate a set of doc/rev IDs in the JSON format that _revs_diff wants:
    // <http://wiki.apache.org/couchdb/HttpPostRevsDiff>
    Map<String, List<String>> diffs = new HashMap<String, List<String>>();
    for (RevisionInternal rev : changes) {
      String docID = rev.getDocID();
      List<String> revs = diffs.get(docID);
      if (revs == null) {
        revs = new ArrayList<String>();
        diffs.put(docID, revs);
      }
      revs.add(rev.getRevID());
      addPending(rev);
    }

    // Call _revs_diff on the target db:
    Log.v(Log.TAG_SYNC, "%s: posting to /_revs_diff", this);

    CustomFuture future =
        sendAsyncRequest(
            "POST",
            "/_revs_diff",
            diffs,
            new RemoteRequestCompletionBlock() {

              @Override
              public void onCompletion(HttpResponse httpResponse, Object response, Throwable e) {

                Log.v(Log.TAG_SYNC, "%s: got /_revs_diff response", this);
                Map<String, Object> results = (Map<String, Object>) response;
                if (e != null) {
                  setError(e);
                } else {
                  if (results.size() != 0) {
                    // Go through the list of local changes again, selecting the ones the
                    // destination server
                    // said were missing and mapping them to a JSON dictionary in the form
                    // _bulk_docs wants:
                    List<Object> docsToSend = new ArrayList<Object>();
                    RevisionList revsToSend = new RevisionList();
                    long bufferedSize = 0;
                    for (RevisionInternal rev : changes) {
                      // Is this revision in the server's 'missing' list?
                      Map<String, Object> properties = null;
                      Map<String, Object> revResults =
                          (Map<String, Object>) results.get(rev.getDocID());
                      if (revResults == null) {
                        removePending(rev);
                        continue;
                      }
                      List<String> revs = (List<String>) revResults.get("missing");
                      if (revs == null || !revs.contains(rev.getRevID())) {
                        removePending(rev);
                        continue;
                      }

                      // NOTE: force to load body by Database.loadRevisionBody()
                      // In SQLiteStore.loadRevisionBody() does not load data from database
                      // if sequence != 0 && body != null
                      rev.setSequence(0);
                      rev.setBody(null);

                      RevisionInternal loadedRev;
                      try {
                        loadedRev = db.loadRevisionBody(rev);
                      } catch (CouchbaseLiteException e1) {
                        Log.w(
                            Log.TAG_SYNC,
                            "%s Couldn't get local contents of %s",
                            rev,
                            PusherInternal.this);
                        continue;
                      }

                      RevisionInternal populatedRev = transformRevision(loadedRev);
                      loadedRev = null;

                      List<String> possibleAncestors =
                          (List<String>) revResults.get("possible_ancestors");

                      properties = new HashMap<String, Object>(populatedRev.getProperties());
                      Map<String, Object> revisions =
                          db.getRevisionHistoryDictStartingFromAnyAncestor(
                              populatedRev, possibleAncestors);
                      properties.put("_revisions", revisions);
                      populatedRev.setProperties(properties);

                      // Strip any attachments already known to the target db:
                      if (properties.containsKey("_attachments")) {
                        // Look for the latest common ancestor and stub out older attachments:
                        int minRevPos = findCommonAncestor(populatedRev, possibleAncestors);

                        Status status = new Status(Status.OK);
                        if (!db.expandAttachments(
                            populatedRev, minRevPos + 1, !dontSendMultipart, false, status)) {
                          Log.w(
                              Log.TAG_SYNC,
                              "%s: Couldn't expand attachments of %s",
                              this,
                              populatedRev);
                          continue;
                        }

                        properties = populatedRev.getProperties();
                        if (!dontSendMultipart && uploadMultipartRevision(populatedRev)) {
                          continue;
                        }
                      }

                      if (properties == null || !properties.containsKey("_id")) {
                        throw new IllegalStateException("properties must contain a document _id");
                      }

                      revsToSend.add(rev);
                      docsToSend.add(properties);

                      bufferedSize += JSONUtils.estimate(properties);
                      if (bufferedSize > kMaxBulkDocsObjectSize) {
                        uploadBulkDocs(docsToSend, revsToSend);
                        docsToSend = new ArrayList<Object>();
                        revsToSend = new RevisionList();
                        bufferedSize = 0;
                      }
                    }

                    // Post the revisions to the destination:
                    uploadBulkDocs(docsToSend, revsToSend);

                  } else {
                    // None of the revisions are new to the remote
                    for (RevisionInternal revisionInternal : changes) {
                      removePending(revisionInternal);
                    }
                  }
                }
              }
            });
    future.setQueue(pendingFutures);
    pendingFutures.add(future);

    pauseOrResume();
  }
  /**
   * Fetches the contents of a revision from the remote db, including its parent revision ID. The
   * contents are stored into rev.properties.
   */
  @InterfaceAudience.Private
  public void pullRemoteRevision(final RevisionInternal rev) {

    Log.d(Log.TAG_SYNC, "%s: pullRemoteRevision with rev: %s", this, rev);

    ++httpConnectionCount;

    // Construct a query. We want the revision history, and the bodies of attachments that have
    // been added since the latest revisions we have locally.
    // See: http://wiki.apache.org/couchdb/HTTP_Document_API#Getting_Attachments_With_a_Document
    StringBuilder path = new StringBuilder("/");
    path.append(encodeDocumentId(rev.getDocID()));
    path.append("?rev=").append(URIUtils.encode(rev.getRevID()));
    path.append("&revs=true&attachments=true");

    // If the document has attachments, add an 'atts_since' param with a list of
    // already-known revisions, so the server can skip sending the bodies of any
    // attachments we already have locally:
    AtomicBoolean hasAttachment = new AtomicBoolean(false);
    List<String> knownRevs =
        db.getPossibleAncestorRevisionIDs(
            rev, PullerInternal.MAX_NUMBER_OF_ATTS_SINCE, hasAttachment);
    if (hasAttachment.get() && knownRevs != null && knownRevs.size() > 0) {
      path.append("&atts_since=");
      path.append(joinQuotedEscaped(knownRevs));
    }

    // create a final version of this variable for the log statement inside
    // FIXME find a way to avoid this
    final String pathInside = path.toString();
    CustomFuture future =
        sendAsyncMultipartDownloaderRequest(
            "GET",
            pathInside,
            null,
            db,
            new RemoteRequestCompletionBlock() {

              @Override
              public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) {
                if (e != null) {
                  Log.e(Log.TAG_SYNC, "Error pulling remote revision", e);
                  revisionFailed(rev, e);
                } else {
                  Map<String, Object> properties = (Map<String, Object>) result;
                  PulledRevision gotRev = new PulledRevision(properties);
                  gotRev.setSequence(rev.getSequence());

                  Log.d(
                      Log.TAG_SYNC,
                      "%s: pullRemoteRevision add rev: %s to batcher: %s",
                      PullerInternal.this,
                      gotRev,
                      downloadsToInsert);

                  if (gotRev.getBody() != null) gotRev.getBody().compact();

                  // Add to batcher ... eventually it will be fed to -insertRevisions:.
                  downloadsToInsert.queueObject(gotRev);
                }

                // Note that we've finished this task:
                --httpConnectionCount;

                // Start another task if there are still revisions waiting to be pulled:
                pullRemoteRevisions();
              }
            });
    future.setQueue(pendingFutures);
    pendingFutures.add(future);
  }