@Override
  public BasicDocumentRevision updateLocalDocument(
      String docId, String prevRevId, final DocumentBody body) {
    Preconditions.checkState(this.isOpen(), "Database is closed");
    Preconditions.checkArgument(
        !Strings.isNullOrEmpty(docId), "Input document id can not be empty");
    Preconditions.checkArgument(
        !Strings.isNullOrEmpty(prevRevId), "Input previous revision id can not be empty");
    Preconditions.checkNotNull(body, "Input document body can not be null");

    CouchUtils.validateRevisionId(prevRevId);

    DocumentRevision preRevision = this.getLocalDocument(docId, prevRevId);
    this.sqlDb.beginTransaction();
    try {
      String newRevId = CouchUtils.generateNextLocalRevisionId(prevRevId);
      ContentValues values = new ContentValues();
      values.put("revid", newRevId);
      values.put("json", body.asBytes());
      String[] whereArgs = new String[] {docId, prevRevId};
      int rowsUpdated = this.sqlDb.update("localdocs", values, "docid=? AND revid=?", whereArgs);
      if (rowsUpdated == 1) {
        this.sqlDb.setTransactionSuccessful();
        return this.getLocalDocument(docId, newRevId);
      } else {
        throw new IllegalStateException("Error updating local docs: " + preRevision);
      }
    } finally {
      this.sqlDb.endTransaction();
    }
  }
  @Override
  public BasicDocumentRevision createLocalDocument(String docId, final DocumentBody body) {
    Preconditions.checkState(this.isOpen(), "Database is closed");
    CouchUtils.validateDocumentId(docId);
    Preconditions.checkNotNull(body, "Input document body can not be null");
    this.sqlDb.beginTransaction();
    try {
      String firstRevId = CouchUtils.getFirstLocalDocRevisionId();
      ContentValues values = new ContentValues();
      values.put("docid", docId);
      values.put("revid", firstRevId);
      values.put("json", body.asBytes());

      long lineId = this.sqlDb.insert("localdocs", values);
      if (lineId < 0) {
        throw new IllegalArgumentException(
            "Can not insert new local doc, likely the docId exists already: " + docId);
      } else {
        Log.d(LOG_TAG, "New local doc inserted: " + lineId + ", " + docId);
      }

      this.sqlDb.setTransactionSuccessful();
      return getLocalDocument(docId, firstRevId);
    } finally {
      this.sqlDb.endTransaction();
    }
  }
 private String[] findRevisionIds(Response[] responses) {
   return new String[] {
     CouchUtils.getRevisionIdSuffix(responses[0].getRev()),
     CouchUtils.getRevisionIdSuffix(responses[1].getRev()),
     CouchUtils.getRevisionIdSuffix(responses[2].getRev())
   };
 }
  @Override
  public void deleteDocument(String docId, String prevRevId) throws ConflictException {
    Preconditions.checkState(this.isOpen(), "Database is closed");
    Preconditions.checkArgument(
        !Strings.isNullOrEmpty(docId), "Input document id can not be empty");
    Preconditions.checkArgument(
        !Strings.isNullOrEmpty(prevRevId), "Input previous revision id can not be empty");

    CouchUtils.validateRevisionId(prevRevId);

    DocumentDeleted documentDeleted = null;
    this.sqlDb.beginTransaction();
    try {
      BasicDocumentRevision preRevision = this.getDocument(docId, prevRevId);
      if (preRevision == null) {
        throw new IllegalArgumentException("The document trying to update does not exist.");
      }

      DocumentRevisionTree revisionTree = this.getAllRevisionsOfDocument(docId);
      if (revisionTree == null) {
        throw new IllegalArgumentException("Document does not exist for id: " + docId);
      } else if (!revisionTree.leafRevisionIds().contains(prevRevId)) {
        throw new ConflictException("Revision to be deleted is not a leaf node:" + prevRevId);
      }

      if (!preRevision.isDeleted()) {
        this.checkOffPreviousWinnerRevisionStatus(preRevision);
        String newRevisionId = CouchUtils.generateNextRevisionId(preRevision.getRevision());
        // Previous revision to be deleted could be winner revision ("current" == true),
        // or a non-winner leaf revision ("current" == false), the new inserted
        // revision must have the same flag as it previous revision.
        // Deletion of non-winner leaf revision is mainly used when resolving
        // conflicts.
        this.insertRevision(
            preRevision.getInternalNumericId(),
            newRevisionId,
            preRevision.getSequence(),
            true,
            preRevision.isCurrent(),
            JSONUtils.EMPTY_JSON,
            false);
        BasicDocumentRevision newRevision = this.getDocument(preRevision.getId(), newRevisionId);
        documentDeleted = new DocumentDeleted(preRevision, newRevision);
      }

      // Very tricky! Must call setTransactionSuccessful() even no change
      // to the db within this method. This is to allow this method to be
      // nested to other outer transaction, otherwise, the outer transaction
      // will rollback.
      this.sqlDb.setTransactionSuccessful();
    } finally {
      this.sqlDb.endTransaction();
      if (documentDeleted != null) {
        eventBus.post(documentDeleted);
      }
    }
  }
 private boolean checkRevisionIsInCorrectOrder(List<String> revisionHistory) {
   for (int i = 0; i < revisionHistory.size() - 1; i++) {
     CouchUtils.validateRevisionId(revisionHistory.get(i));
     int l = CouchUtils.generationFromRevId(revisionHistory.get(i));
     int m = CouchUtils.generationFromRevId(revisionHistory.get(i + 1));
     if (l >= m) {
       return false;
     }
   }
   return true;
 }
 private String insertNewWinnerRevision(DocumentBody newWinner, DocumentRevision oldWinner) {
   String newRevisionId = CouchUtils.generateNextRevisionId(oldWinner.getRevision());
   this.insertRevision(
       oldWinner.getInternalNumericId(),
       newRevisionId,
       oldWinner.getSequence(),
       false,
       true,
       newWinner.asBytes(),
       true);
   return newRevisionId;
 }
  public List<BasicDocumentRevision> createTwoDBObjects(
      CouchClientWrapper remoteDb, String id1, String id2) {
    List<BasicDocumentRevision> objects = new ArrayList<BasicDocumentRevision>();

    DocumentRevisionBuilder builder = new DocumentRevisionBuilder();
    builder.setDocId(id1);
    builder.setRevId(CouchUtils.getFirstRevisionId());
    builder.setBody(bodyOne);

    BasicDocumentRevision todo1 = builder.build();
    objects.add(todo1);

    DocumentRevisionBuilder builder2 = new DocumentRevisionBuilder();
    builder2.setDocId(id2);
    builder2.setRevId(CouchUtils.getFirstRevisionId());
    builder2.setBody(bodyTwo);

    BasicDocumentRevision tdo2 = builder2.build();
    objects.add(tdo2);

    return objects;
  }
  @Override
  public BasicDocumentRevision createDocument(String docId, final DocumentBody body) {
    Preconditions.checkState(this.isOpen(), "Database is closed");
    CouchUtils.validateDocumentId(docId);
    Preconditions.checkNotNull(body, "Input document body can not be null");
    this.validateDBBody(body);

    DocumentCreated documentCreated = null;
    this.sqlDb.beginTransaction();
    try {
      long docNumericID = insertDocumentID(docId);
      if (docNumericID < 0) {
        throw new IllegalArgumentException(
            "Can not insert new doc, likely the docId exists already: " + docId);
      }

      String revisionId = CouchUtils.getFirstRevisionId();
      long newSequence =
          insertRevision(docNumericID, revisionId, -1l, false, true, body.asBytes(), true);
      if (newSequence < 0) {
        throw new IllegalStateException("Error inserting data, please checking data.");
      }

      BasicDocumentRevision doc = getDocument(docId, revisionId);
      documentCreated = new DocumentCreated(doc);

      Log.d(LOG_TAG, "New document created: " + doc.toString());

      this.sqlDb.setTransactionSuccessful();
      return doc;
    } finally {
      this.sqlDb.endTransaction();
      if (documentCreated != null) {
        eventBus.post(documentCreated);
      }
    }
  }
  @Override
  public BasicDocumentRevision updateDocument(
      String docId, String prevRevId, final DocumentBody body) throws ConflictException {
    Preconditions.checkState(this.isOpen(), "Database is closed");
    Preconditions.checkArgument(
        !Strings.isNullOrEmpty(docId), "Input document id can not be empty");
    Preconditions.checkArgument(
        !Strings.isNullOrEmpty(prevRevId), "Input previous revision id can not be empty");
    Preconditions.checkNotNull(body, "Input document body can not be null");
    this.validateDBBody(body);

    CouchUtils.validateRevisionId(prevRevId);

    DocumentUpdated documentUpdated = null;
    this.sqlDb.beginTransaction();
    try {
      BasicDocumentRevision preRevision = this.getDocument(docId, prevRevId);
      if (preRevision == null) {
        throw new IllegalArgumentException("The document trying to update does not exist.");
      }

      if (!preRevision.isCurrent()) {
        throw new ConflictException("Revision to be updated is not current revision.");
      }

      this.checkOffPreviousWinnerRevisionStatus(preRevision);
      String newRevisionId = this.insertNewWinnerRevision(body, preRevision);
      BasicDocumentRevision newRevision = this.getDocument(preRevision.getId(), newRevisionId);

      this.sqlDb.setTransactionSuccessful();
      documentUpdated = new DocumentUpdated(preRevision, newRevision);
      return newRevision;
    } finally {
      this.sqlDb.endTransaction();
      if (documentUpdated != null) {
        eventBus.post(documentUpdated);
      }
    }
  }
  @Override
  public void forceInsert(
      DocumentRevision rev, List<String> revisionHistory, Map<String, Object> attachments) {
    Preconditions.checkState(this.isOpen(), "Database is closed");
    Preconditions.checkNotNull(rev, "Input document revision can not be null");
    Preconditions.checkNotNull(revisionHistory, "Input revision history must not be null");
    Preconditions.checkArgument(
        revisionHistory.size() > 0, "Input revision history must not be empty");
    Preconditions.checkArgument(
        checkCurrentRevisionIsInRevisionHistory(rev, revisionHistory),
        "Current revision must exist in revision history.");
    Preconditions.checkArgument(
        checkRevisionIsInCorrectOrder(revisionHistory), "Revision history must be in right order.");
    CouchUtils.validateDocumentId(rev.getId());
    CouchUtils.validateRevisionId(rev.getRevision());

    Log.v(
        LOG_TAG,
        "forceInsert(): " + rev.toString() + ",\n" + JSONUtils.toPrettyJson(revisionHistory));

    DocumentCreated documentCreated = null;
    DocumentUpdated documentUpdated = null;

    boolean ok = true;

    this.sqlDb.beginTransaction();
    try {
      long seq = 0;
      // sequence here is -1, but we need it to insert the attachment - also might be wanted by
      // subscribers
      if (this.containsDocument(rev.getId())) {
        seq = doForceInsertExistingDocumentWithHistory(rev, revisionHistory);
        rev.initialiseSequence(seq);
        // TODO fetch the parent doc?
        documentUpdated = new DocumentUpdated(null, rev);
      } else {
        seq = doForceInsertNewDocumentWithHistory(rev, revisionHistory);
        rev.initialiseSequence(seq);
        documentCreated = new DocumentCreated(rev);
      }
      // now deal with any attachments
      if (attachments != null) {
        for (String att : attachments.keySet()) {
          String data = (String) ((Map<String, Object>) attachments.get(att)).get("data");
          InputStream is = new Base64InputStream(new ByteArrayInputStream(data.getBytes()));
          String type = (String) ((Map<String, Object>) attachments.get(att)).get("content_type");
          // inline attachments are automatically decompressed, so we don't have to worry about that
          UnsavedStreamAttachment ufa = new UnsavedStreamAttachment(is, att, type);
          boolean result = false;
          try {
            PreparedAttachment preparedAttachment =
                new PreparedAttachment(ufa, this.attachmentManager.attachmentsDir);
            result = this.attachmentManager.addAttachment(preparedAttachment, rev);
          } catch (IOException e) {
            Log.e(LOG_TAG, "IOException when preparing attachment " + ufa + ": " + e);
          }
          if (!result) {
            Log.e(
                LOG_TAG,
                "There was a problem adding the attachment "
                    + ufa
                    + " to the datastore; not force inserting this document: "
                    + rev);
            ok = false;
            break;
          }
        }
      }
      if (ok) {
        this.sqlDb.setTransactionSuccessful();
      }
    } finally {
      this.sqlDb.endTransaction();
      if (ok) {
        if (documentCreated != null) {
          eventBus.post(documentCreated);
        } else if (documentUpdated != null) {
          eventBus.post(documentUpdated);
        }
      }
    }
  }
 @Override
 public BasicDocumentRevision createLocalDocument(final DocumentBody body) {
   Preconditions.checkState(this.isOpen(), "Database is closed");
   String documentId = CouchUtils.generateDocumentId();
   return createLocalDocument(documentId, body);
 }