/** * 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); } }