/**
   * Fallback to upload a revision if uploadMultipartRevision failed due to the server's rejecting
   * multipart format. - (void) uploadJSONRevision: (CBL_Revision*)originalRev in CBLRestPusher.m
   */
  private void uploadJsonRevision(final RevisionInternal rev) {
    // Get the revision's properties:
    if (!db.inlineFollowingAttachmentsIn(rev)) {
      setError(new CouchbaseLiteException(Status.BAD_ATTACHMENT));
      return;
    }

    final String path = String.format("/%s?new_edits=false", encodeDocumentId(rev.getDocID()));
    CustomFuture future =
        sendAsyncRequest(
            "PUT",
            path,
            rev.getProperties(),
            new RemoteRequestCompletionBlock() {
              public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) {
                if (e != null) {
                  setError(e);
                } else {
                  Log.v(Log.TAG_SYNC, "%s: Sent %s (JSON), response=%s", this, rev, result);
                  removePending(rev);
                }
              }
            });
    future.setQueue(pendingFutures);
    pendingFutures.add(future);
  }
  // This invokes the tranformation block if one is installed and queues the resulting CBL_Revision
  private void queueDownloadedRevision(RevisionInternal rev) {

    if (revisionBodyTransformationBlock != null) {
      // Add 'file' properties to attachments pointing to their bodies:

      for (Map.Entry<String, Map<String, Object>> entry :
          ((Map<String, Map<String, Object>>) rev.getProperties().get("_attachments")).entrySet()) {
        String name = entry.getKey();
        Map<String, Object> attachment = entry.getValue();
        attachment.remove("file");
        if (attachment.get("follows") != null && attachment.get("data") == null) {
          String filePath = db.fileForAttachmentDict(attachment).getPath();
          if (filePath != null) attachment.put("file", filePath);
        }
      }

      RevisionInternal xformed = transformRevision(rev);
      if (xformed == null) {
        Log.v(Log.TAG_SYNC, "%s: Transformer rejected revision %s", this, rev);
        pendingSequences.removeSequence(rev.getSequence());
        lastSequence = pendingSequences.getCheckpointedValue();
        pauseOrResume();
        return;
      }
      rev = xformed;

      // Clean up afterwards
      Map<String, Object> attachments =
          (Map<String, Object>) rev.getProperties().get("_attachments");

      for (Map.Entry<String, Map<String, Object>> entry :
          ((Map<String, Map<String, Object>>) rev.getProperties().get("_attachments")).entrySet()) {
        Map<String, Object> attachment = entry.getValue();
        attachment.remove("file");
      }
    }

    if (rev != null && rev.getBody() != null) rev.getBody().compact();

    downloadsToInsert.queueObject(rev);
  }
  /**
   * Given a revision and an array of revIDs, finds the latest common ancestor revID and returns its
   * generation #. If there is none, returns 0.
   *
   * <p>int CBLFindCommonAncestor(CBL_Revision* rev, NSArray* possibleRevIDs) in CBLRestPusher.m
   */
  private static int findCommonAncestor(RevisionInternal rev, List<String> possibleRevIDs) {
    if (possibleRevIDs == null || possibleRevIDs.size() == 0) {
      return 0;
    }
    List<String> history = Database.parseCouchDBRevisionHistory(rev.getProperties());

    // rev is missing _revisions property
    assert (history != null);

    boolean changed = history.retainAll(possibleRevIDs);
    String ancestorID = history.size() == 0 ? null : history.get(0);

    if (ancestorID == null) {
      return 0;
    }

    int generation = RevisionUtils.parseRevIDNumber(ancestorID);

    return generation;
  }
  /** in CBL_Pusher.m - (CBLMultipartWriter*)multipartWriterForRevision: (CBL_Revision*)rev */
  @InterfaceAudience.Private
  private boolean uploadMultipartRevision(final RevisionInternal revision) {

    // holds inputStream for blob to close after using
    final List<InputStream> streamList = new ArrayList<InputStream>();

    MultipartEntity multiPart = null;

    Map<String, Object> revProps = revision.getProperties();

    Map<String, Object> attachments = (Map<String, Object>) revProps.get("_attachments");
    for (String attachmentKey : attachments.keySet()) {
      Map<String, Object> attachment = (Map<String, Object>) attachments.get(attachmentKey);
      if (attachment.containsKey("follows")) {

        if (multiPart == null) {

          multiPart = new MultipartEntity();

          try {
            String json = Manager.getObjectMapper().writeValueAsString(revProps);
            Charset utf8charset = Charset.forName("UTF-8");
            byte[] uncompressed = json.getBytes(utf8charset);
            byte[] compressed = null;
            byte[] data = uncompressed;
            String contentEncoding = null;
            if (uncompressed.length > RemoteRequest.MIN_JSON_LENGTH_TO_COMPRESS
                && canSendCompressedRequests()) {
              compressed = Utils.compressByGzip(uncompressed);
              if (compressed.length < uncompressed.length) {
                data = compressed;
                contentEncoding = "gzip";
              }
            }
            // NOTE: StringBody.contentEncoding default value is null. Setting null value to
            // contentEncoding does not cause any impact.
            multiPart.addPart(
                "param1", new StringBody(data, "application/json", utf8charset, contentEncoding));
          } catch (IOException e) {
            throw new IllegalArgumentException(e);
          }
        }

        BlobStore blobStore = this.db.getAttachmentStore();
        String base64Digest = (String) attachment.get("digest");
        BlobKey blobKey = new BlobKey(base64Digest);
        InputStream blobStream = blobStore.blobStreamForKey(blobKey);
        if (blobStream == null) {
          Log.w(
              Log.TAG_SYNC,
              "Unable to load the blob stream for blobKey: %s - Skipping upload of multipart revision.",
              blobKey);
          return false;
        } else {
          streamList.add(blobStream);
          String contentType = null;
          if (attachment.containsKey("content_type")) {
            contentType = (String) attachment.get("content_type");
          } else if (attachment.containsKey("type")) {
            contentType = (String) attachment.get("type");
          } else if (attachment.containsKey("content-type")) {
            Log.w(
                Log.TAG_SYNC,
                "Found attachment that uses content-type"
                    + " field name instead of content_type (see couchbase-lite-android"
                    + " issue #80): %s",
                attachment);
          }

          // contentType = null causes Exception from FileBody of apache.
          if (contentType == null) contentType = "application/octet-stream"; // default

          // NOTE: Content-Encoding might not be necessary to set. Apache FileBody does not set
          // Content-Encoding.
          //       FileBody always return null for getContentEncoding(), and Content-Encoding header
          // is not set in multipart
          // CBL iOS:
          // https://github.com/couchbase/couchbase-lite-ios/blob/feb7ff5eda1e80bd00e5eb19f1d46c793f7a1951/Source/CBL_Pusher.m#L449-L452
          String contentEncoding = null;
          if (attachment.containsKey("encoding")) {
            contentEncoding = (String) attachment.get("encoding");
          }

          InputStreamBody inputStreamBody =
              new CustomStreamBody(blobStream, contentType, attachmentKey, contentEncoding);
          multiPart.addPart(attachmentKey, inputStreamBody);
        }
      }
    }

    if (multiPart == null) {
      return false;
    }

    final String path = String.format("/%s?new_edits=false", encodeDocumentId(revision.getDocID()));

    Log.d(Log.TAG_SYNC, "Uploading multipart request.  Revision: %s", revision);

    addToChangesCount(1);

    CustomFuture future =
        sendAsyncMultipartRequest(
            "PUT",
            path,
            multiPart,
            new RemoteRequestCompletionBlock() {
              @Override
              public void onCompletion(HttpResponse httpResponse, Object result, Throwable e) {
                try {
                  if (e != null) {
                    if (e instanceof HttpResponseException) {
                      // Server doesn't like multipart, eh? Fall back to JSON.
                      if (((HttpResponseException) e).getStatusCode() == 415) {
                        // status 415 = "bad_content_type"
                        dontSendMultipart = true;
                        uploadJsonRevision(revision);
                      }
                    } else {
                      Log.e(Log.TAG_SYNC, "Exception uploading multipart request", e);
                      setError(e);
                    }
                  } else {
                    Log.v(Log.TAG_SYNC, "Uploaded multipart request.  Revision: %s", revision);
                    removePending(revision);
                  }
                } finally {
                  // close all inputStreams for Blob
                  for (InputStream stream : streamList) {
                    try {
                      stream.close();
                    } catch (IOException ioe) {
                    }
                  }
                  addToCompletedChangesCount(1);
                }
              }
            });
    future.setQueue(pendingFutures);
    pendingFutures.add(future);

    return true;
  }
 /**
  * The properties of the document this row was mapped from. To get this, you must have set the
  * -prefetch property on the query; else this will be nil. The map returned is immutable (run
  * through Collections.unmodifiableMap)
  */
 @InterfaceAudience.Public
 public Map<String, Object> getDocumentProperties() {
   return documentRevision != null ? documentRevision.getProperties() : null;
 }