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