/**
  * Removes a revision from the "pending" set after it's been uploaded. Advances checkpoint. -
  * (void) removePending: (CBL_Revision*)rev in CBLRestPusher.m
  */
 @InterfaceAudience.Private
 private void removePending(RevisionInternal revisionInternal) {
   long seq = revisionInternal.getSequence();
   if (pendingSequences == null || pendingSequences.isEmpty()) {
     Log.w(
         Log.TAG_SYNC,
         "%s: removePending() called w/ rev: %s, but pendingSequences empty",
         this,
         revisionInternal);
     if (revisionInternal.getBody() != null) revisionInternal.getBody().release();
     pauseOrResume();
     return;
   }
   boolean wasFirst = (seq == pendingSequences.first());
   if (!pendingSequences.contains(seq)) {
     Log.w(
         Log.TAG_SYNC,
         "%s: removePending: sequence %s not in set, for rev %s",
         this,
         seq,
         revisionInternal);
   }
   pendingSequences.remove(seq);
   if (wasFirst) {
     // If I removed the first pending sequence, can advance the checkpoint:
     long maxCompleted;
     if (pendingSequences.size() == 0) {
       maxCompleted = maxPendingSequence;
     } else {
       maxCompleted = pendingSequences.first();
       --maxCompleted;
     }
     setLastSequence(Long.toString(maxCompleted));
   }
   if (revisionInternal.getBody() != null) revisionInternal.getBody().release();
   pauseOrResume();
 }
 static void runReplication(Replication repl) throws InterruptedException {
   Log.i(TAG, "Waiting for " + repl + " to finish...");
   boolean started = false, done = false;
   repl.start();
   long lastTime = System.currentTimeMillis();
   ;
   while (!done) {
     if (repl.isRunning()) {
       started = true;
     }
     // TODO getMode() always throws UnsupportedOperationException (see ios test)
     if (started
         && (repl.getMode() == Replication.ReplicationMode.REPLICATION_ACTIVE
             || repl.getMode() == Replication.ReplicationMode.REPLICATION_ACTIVE)) {
       done = true;
     }
     // Replication runs on a background thread, so the main runloop should not be blocked.
     // Make sure it's spinning in a timely manner:
     long now = System.currentTimeMillis();
     if (lastTime > 0 && now - lastTime > 25)
       Log.w(TAG, "Runloop was blocked for " + (now - lastTime) * 100 + " sec");
     lastTime = now;
     Thread.sleep(100);
     break;
   }
   if (repl.getLastError() == null) {
     Log.i(
         TAG,
         String.format(
             "...replicator finished. progress %d/%d without error",
             repl.getCompletedChangesCount(), repl.getChangesCount()));
   } else {
     Log.i(
         TAG,
         String.format(
             "...replicator finished. progress %d/%d, error=%s",
             repl.getCompletedChangesCount(),
             repl.getChangesCount(),
             repl.getLastError().toString()));
   }
 }
  /**
   * in CBL_Puller.m - (void) changeTrackerReceivedSequence: (id)remoteSequenceID docID:
   * (NSString*)docID revIDs: (NSArray*)revIDs deleted: (BOOL)deleted
   */
  protected void processChangeTrackerChange(final Map<String, Object> change) {
    String lastSequence = change.get("seq").toString();
    String docID = (String) change.get("id");
    if (docID == null) {
      return;
    }

    if (!Document.isValidDocumentId(docID)) {
      Log.w(Log.TAG_SYNC, "%s: Received invalid doc ID from _changes: %s", this, change);
      return;
    }
    boolean deleted =
        (change.containsKey("deleted") && ((Boolean) change.get("deleted")).equals(Boolean.TRUE));
    List<Map<String, Object>> changes = (List<Map<String, Object>>) change.get("changes");
    for (Map<String, Object> changeDict : changes) {
      String revID = (String) changeDict.get("rev");
      if (revID == null) {
        continue;
      }

      PulledRevision rev = new PulledRevision(docID, revID, deleted);

      // Remember its remote sequence ID (opaque), and make up a numeric sequence
      // based on the order in which it appeared in the _changes feed:
      rev.setRemoteSequenceID(lastSequence);

      if (changes.size() > 1) rev.setConflicted(true);

      Log.d(Log.TAG_SYNC, "%s: adding rev to inbox %s", this, rev);

      Log.v(Log.TAG_SYNC, "%s: changeTrackerReceivedChange() incrementing changesCount by 1", this);

      // this is purposefully done slightly different than the ios version
      addToChangesCount(1);

      addToInbox(rev);
    }

    pauseOrResume();
  }
  /** 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;
  }
  /** - (void) beginReplicating in CBL_Replicator.m */
  @Override
  @InterfaceAudience.Private
  public void beginReplicating() {
    // If we're still waiting to create the remote db, do nothing now. (This method will be
    // re-invoked after that request finishes; see -maybeCreateRemoteDB above.)

    Log.d(Log.TAG_SYNC, "%s: beginReplicating() called", this);

    // If we're still waiting to create the remote db, do nothing now. (This method will be
    // re-invoked after that request finishes; see maybeCreateRemoteDB() above.)
    if (creatingTarget) {
      Log.d(Log.TAG_SYNC, "%s: creatingTarget == true, doing nothing", this);
      return;
    }

    pendingSequences = Collections.synchronizedSortedSet(new TreeSet<Long>());
    try {
      maxPendingSequence = Long.parseLong(lastSequence);
    } catch (NumberFormatException e) {
      Log.w(Log.TAG_SYNC, "Error converting lastSequence: %s to long.  Using 0", lastSequence);
      maxPendingSequence = new Long(0);
    }

    filter = compilePushReplicationFilter();
    if (filterName != null && filter == null) {
      Log.w(
          Log.TAG_SYNC,
          "%s: No ReplicationFilter registered for filter '%s'; ignoring",
          this,
          filterName);
    }

    // Process existing changes since the last push:
    long lastSequenceLong = 0;
    if (lastSequence != null) {
      lastSequenceLong = Long.parseLong(lastSequence);
    }
    ChangesOptions options = new ChangesOptions();
    options.setIncludeConflicts(true);
    Log.d(Log.TAG_SYNC, "%s: Getting changes since %s", this, lastSequence);
    RevisionList changes = db.changesSince(lastSequenceLong, options, filter, filterParams);
    if (changes.size() > 0) {
      Log.d(Log.TAG_SYNC, "%s: Queuing %d changes since %s", this, changes.size(), lastSequence);
      int remaining = changes.size();
      int size = batcher.getCapacity();
      int start = 0;
      while (remaining > 0) {
        if (size > remaining) size = remaining;
        RevisionList subChanges = new RevisionList(changes.subList(start, start + size));
        batcher.queueObjects(subChanges);
        start += size;
        remaining -= size;
        pauseOrResume();
        waitIfPaused();
      }
    } else {
      Log.d(Log.TAG_SYNC, "%s: No changes since %s", this, lastSequence);
    }

    // Now listen for future changes (in continuous mode):
    if (isContinuous()) {
      observing = true;
      db.addChangeListener(this);
    }
  }
  /** Process a bunch of remote revisions from the _changes feed at once */
  @Override
  @InterfaceAudience.Private
  protected void processInbox(RevisionList inbox) {
    Log.d(Log.TAG_SYNC, "processInbox called");

    if (canBulkGet == null) {
      canBulkGet = serverIsSyncGatewayVersion("0.81");
    }

    // Ask the local database which of the revs are not known to it:
    String lastInboxSequence = ((PulledRevision) inbox.get(inbox.size() - 1)).getRemoteSequenceID();

    int numRevisionsRemoved = 0;
    try {
      // findMissingRevisions is the local equivalent of _revs_diff. it looks at the
      // array of revisions in "inbox" and removes the ones that already exist.
      // So whatever's left in 'inbox'
      // afterwards are the revisions that need to be downloaded.
      numRevisionsRemoved = db.findMissingRevisions(inbox);
    } catch (SQLException e) {
      Log.e(Log.TAG_SYNC, String.format("%s failed to look up local revs", this), e);
      inbox = null;
    }

    // introducing this to java version since inbox may now be null everywhere
    int inboxCount = 0;
    if (inbox != null) {
      inboxCount = inbox.size();
    }

    if (numRevisionsRemoved > 0) {
      Log.v(
          Log.TAG_SYNC,
          "%s: processInbox() setting changesCount to: %s",
          this,
          getChangesCount().get() - numRevisionsRemoved);
      // May decrease the changesCount, to account for the revisions we just found out we don't need
      // to get.
      addToChangesCount(-1 * numRevisionsRemoved);
    }

    if (inboxCount == 0) {
      // Nothing to do. Just bump the lastSequence.
      Log.w(
          Log.TAG_SYNC,
          "%s no new remote revisions to fetch.  add lastInboxSequence (%s) to pendingSequences (%s)",
          this,
          lastInboxSequence,
          pendingSequences);
      long seq = pendingSequences.addValue(lastInboxSequence);
      pendingSequences.removeSequence(seq);
      setLastSequence(pendingSequences.getCheckpointedValue());
      pauseOrResume();
      return;
    }

    Log.v(Log.TAG_SYNC, "%s: fetching %s remote revisions...", this, inboxCount);

    // Dump the revs into the queue of revs to pull from the remote db:
    for (int i = 0; i < inbox.size(); i++) {
      PulledRevision rev = (PulledRevision) inbox.get(i);
      if (canBulkGet || (rev.getGeneration() == 1 && !rev.isDeleted() && !rev.isConflicted())) {
        bulkRevsToPull.add(rev);
      } else {
        queueRemoteRevision(rev);
      }
      rev.setSequence(pendingSequences.addValue(rev.getRemoteSequenceID()));
    }
    pullRemoteRevisions();
    pauseOrResume();
  }
  public void testHelloWorld() throws Exception {

    Log.w(TAG, "Create a document");

    // get the current date and time
    SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd");
    Calendar calendar = GregorianCalendar.getInstance();
    String currentTimeString = dateFormatter.format(calendar.getTime());

    // create an object that contains data for a document
    Map<String, Object> docContent = new HashMap<String, Object>();
    docContent.put("message", "Hello Couchbase Lite");
    docContent.put("creationDate", currentTimeString);

    // display the data for the new document
    Log.w(TAG, "new docContent=" + String.valueOf(docContent));

    // create an empty document
    Document document = database.createDocument();
    assertNotNull(document);

    // write the document to the database
    try {
      document.putProperties(docContent);
      Log.w(
          TAG,
          "Document written to database named "
              + database.getName()
              + " with ID = "
              + document.getId());
    } catch (CouchbaseLiteException e) {
      Log.e(TAG, "Cannot write document to database", e);
    }

    // save the ID of the new document
    String docID = document.getId();
    assertNotNull(docID);
    assertNotSame("", docID);

    Log.w(TAG, "Retrieve a document");

    // retrieve the document from the database
    Document retrievedDocument = database.getDocument(docID);
    assertNotNull(retrievedDocument);

    // display the retrieved document
    Log.w(TAG, "retrievedDocument=" + String.valueOf(retrievedDocument.getProperties()));

    Log.w(TAG, "Update a document");

    // update the document
    Map<String, Object> updatedProperties = new HashMap<String, Object>();
    updatedProperties.putAll(retrievedDocument.getProperties());
    updatedProperties.put("message", "We're having a heat wave!");
    updatedProperties.put("temperature", "95");

    // display the data for the update document
    Log.w(TAG, "update docContent=" + String.valueOf(updatedProperties));

    try {
      retrievedDocument.putProperties(updatedProperties);
      Log.w(TAG, "updated retrievedDocument=" + String.valueOf(retrievedDocument.getProperties()));
    } catch (CouchbaseLiteException e) {
      Log.e(TAG, "Cannot update document", e);
    }

    Log.w(TAG, "Delete a document");

    // delete the document
    try {
      retrievedDocument.delete();
      Log.w(TAG, "Deleted document, deletion status = " + retrievedDocument.isDeleted());
    } catch (CouchbaseLiteException e) {
      Log.e(TAG, "Cannot delete document", e);
    }
  }