/** * Copyright (c) 2016 Couchbase, Inc. All rights reserved. * <p/> * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ package com.couchbase.lite.mockserver; import com.couchbase.lite.BlobKey; import com.couchbase.lite.BlobStore; import com.couchbase.lite.Manager; import com.couchbase.lite.support.Base64; import org.apache.commons.io.IOUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import okhttp3.Headers; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.RequestBody; import okhttp3.mockwebserver.MockResponse; import okio.Buffer; /* Generate mock document GET response, eg { "_id":"doc1-1402588904847", "_rev":"1-d57b1bc60eb9273c3349d932e15f9949", "_revisions":{ "ids":[ "d57b1bc60eb9273c3349d932e15f9949" ], "start":1 }, "bar":false, "foo":1 } Limitations: it cannot represent docs with revision histories longer than one rev. */ public class MockDocumentGet { private String docId; private String rev; private Map<String, Object> jsonMap; private boolean includeAttachmentPart; // you can optionally supply a revHistoryMap, otherwise // a simple default rev history will be generated. private Map<String, Object> revHistoryMap; // a corresponding file must be in the /assets/ directory private List<String> attachmentFileNames; public MockDocumentGet() { attachmentFileNames = new ArrayList<String>(); this.revHistoryMap = new HashMap<String, Object>(); this.includeAttachmentPart = true; } public MockDocumentGet(MockDocument mockDocument) { this(); this.docId = mockDocument.getDocId(); this.rev = mockDocument.getDocRev(); this.jsonMap = mockDocument.getJsonMap(); } public Map<String, Object> getRevHistoryMap() { return revHistoryMap; } public void setRevHistoryMap(Map<String, Object> revHistoryMap) { this.revHistoryMap = revHistoryMap; } public String getDocId() { return docId; } public MockDocumentGet setDocId(String docId) { this.docId = docId; return this; } public String getRev() { return rev; } public MockDocumentGet setRev(String rev) { this.rev = rev; return this; } public void addAttachmentFilename(String attachmentFilename) { attachmentFileNames.add(attachmentFilename); } public Map<String, Object> getJsonMap() { return jsonMap; } public MockDocumentGet setJsonMap(Map<String, Object> jsonMap) { this.jsonMap = jsonMap; return this; } private Map<String, Object> generateRevHistoryMap() { if (revHistoryMap.isEmpty()) { Map<String, Object> simpleRevHIstoryMap = new HashMap<String, Object>(); // parse rev into components, eg String[] revComponents = rev.split("-"); String numericPrefixStr = revComponents[0]; // eg, "1" String digest = revComponents[1]; // eg, "d57b1bc60eb9273c3349d932e15f9949" int numericPrefix = Integer.parseInt(numericPrefixStr); List<String> revHistoryDigestIds = new ArrayList<String>(); revHistoryDigestIds.add(digest); simpleRevHIstoryMap.put("ids", revHistoryDigestIds); simpleRevHIstoryMap.put("start", numericPrefix); return simpleRevHIstoryMap; } else { return revHistoryMap; } } private Map<String, Object> generateDocumentMap() { Map<String, Object> docMap = new HashMap<String, Object>(); docMap.put("_id", getDocId()); docMap.put("_rev", getRev()); docMap.put("_revisions", generateRevHistoryMap()); if (!attachmentFileNames.isEmpty()) { docMap.put("_attachments", generateAttachmentsMap()); } docMap.putAll(jsonMap); return docMap; } /* { "_attachments":{ "attachment3.png":{ "content_type":"image/png", "digest":"sha1-nKikeP2tQRpJCHOpS4w7G+Kc12Y=", "follows":true, "length":19693, "revpos":1 } }, "_id":"...", ... } */ private Map<String, Object> generateAttachmentsMap() { Map<String, Object> attachmentsMap = new HashMap<String, Object>(); for (String attachmentName : attachmentFileNames) { Map<String, Object> attachmentMap = new HashMap<String, Object>(); if (attachmentName.endsWith("png")) { attachmentMap.put("content_type", "image/png"); } else { throw new RuntimeException("Only png files are supported as test attachemnts"); } attachmentMap.put("digest", calculateSha1Digest(attachmentName)); if (this.isIncludeAttachmentPart()) { attachmentMap.put("follows", true); } else { attachmentMap.put("stub", true); } attachmentMap.put("length", MockDocumentGet.getAssetByteArray(attachmentName).length); attachmentMap.put("revpos", 1); attachmentsMap.put(attachmentName, attachmentMap); } return attachmentsMap; } public static String calculateSha1Digest(String attachmentAssetName) { byte[] attachmentBytes = MockDocumentGet.getAssetByteArray(attachmentAssetName); BlobKey blobKey = BlobStore.keyForBlob(attachmentBytes); String base64Sha1Digest = Base64.encodeBytes(blobKey.getBytes()); String sha1 = String.format(Locale.ENGLISH, "sha1-%s", base64Sha1Digest); return sha1; } private String generateDocumentBody() { try { return Manager.getObjectMapper().writeValueAsString(generateDocumentMap()); } catch (IOException e) { throw new RuntimeException(e); } } public MockResponse generateMockResponse() { MockResponse mockResponse = new MockResponse(); if (attachmentFileNames.isEmpty()) { mockResponse.setBody(generateDocumentBody()); mockResponse.setHeader("Content-Type", "application/json"); } else { createMultipartResponse(mockResponse); } mockResponse.setStatus("HTTP/1.1 200 OK"); return mockResponse; } private void createMultipartResponse(MockResponse mockResponse) { final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png"); for (String attachmentName : attachmentFileNames) { Buffer buffer = new Buffer(); MultipartBody.Builder builder = new MultipartBody.Builder() .setType(MediaType.parse("multipart/related")) .addPart(RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), generateDocumentBody())); if (isIncludeAttachmentPart()) { Headers.Builder hb = new Headers.Builder(); hb.add("Content-Disposition", String.format(Locale.ENGLISH, "attachment; filename=\"%s\"", attachmentName)); hb.add("Content-Transfer-Encoding", "binary"); builder.addPart(hb.build(), RequestBody.create(MEDIA_TYPE_PNG, getAssetByteArray(attachmentName))); } MultipartBody requestBody = builder.build(); try { requestBody.writeTo(buffer); mockResponse.setHeader("Content-Type", requestBody.contentType().toString()); mockResponse.setBody(buffer); } catch (IOException e) { e.printStackTrace(); } } } private static InputStream getAsset(String name) { MockDocumentGet o = new MockDocumentGet(); return o.getClass().getResourceAsStream("/assets/" + name); } public static byte[] getAssetByteArray(String name) { try { InputStream attachmentStream = getAsset(name); ByteArrayOutputStream attachmentBaos = new ByteArrayOutputStream(); IOUtils.copy(attachmentStream, attachmentBaos); byte[] attachmentBytes = attachmentBaos.toByteArray(); attachmentStream.close(); attachmentBaos.close(); return attachmentBytes; } catch (IOException e) { throw new RuntimeException(e); } } public boolean isIncludeAttachmentPart() { return includeAttachmentPart; } public void setIncludeAttachmentPart(boolean includeAttachmentPart) { // TOO: rename to isStubbed() this.includeAttachmentPart = includeAttachmentPart; } public static class MockDocument { private String docId; private String docRev; private int docSeq; private String attachmentName; private Map<String, Object> jsonMap; private boolean missing = false; public MockDocument(String docId, String docRev, int docSeq) { this.docId = docId; this.docRev = docRev; this.docSeq = docSeq; } public MockDocument(String docId, String docRev, int docSeq, boolean missing) { this.docId = docId; this.docRev = docRev; this.docSeq = docSeq; this.missing = missing; } /** * TODO: the MockDocumentGet.generateDocumentMap() method should * TODO: be refactored to use this, but first the revision history * TODO: will need to be moved to this object. */ private Map<String, Object> generateDocumentMap(boolean attachmentFollows) { Map<String, Object> docMap = new HashMap<String, Object>(); if (missing) { docMap.put("id", getDocId()); docMap.put("rev", getDocRev()); docMap.put("error", "not_found"); docMap.put("reason", "missing"); docMap.put("status", 404); } else { docMap.put("_id", getDocId()); docMap.put("_rev", getDocRev()); if (hasAttachment()) { docMap.put("_attachments", generateAttachmentsMap(attachmentFollows)); } docMap.putAll(jsonMap); } return docMap; } /** * TODO: the MockDocumentGet.generateDocumentMap() method should * TODO: be refactored to use this. */ public String generateDocumentBody(boolean attachmentFollows) { Map documentMap = generateDocumentMap(attachmentFollows); try { return Manager.getObjectMapper().writeValueAsString(documentMap); } catch (IOException e) { throw new RuntimeException(e); } } /** * TODO: the MockDocumentGet.generateDocumentMap() method should * TODO: be refactored to use this, but first need to remove * TODO: attachmentFileNames field from MockDocumentGet */ private Map<String, Object> generateAttachmentsMap(boolean attachmentFollows) { Map<String, Object> attachmentsMap = new HashMap<String, Object>(); Map<String, Object> attachmentMap = new HashMap<String, Object>(); if (attachmentName.endsWith("png")) { attachmentMap.put("content_type", "image/png"); } else { throw new RuntimeException("Only png files are supported as test attachemnts"); } attachmentMap.put("digest", calculateSha1Digest(attachmentName)); if (attachmentFollows) { attachmentMap.put("follows", true); } else { attachmentMap.put("stub", true); } attachmentMap.put("length", MockDocumentGet.getAssetByteArray(attachmentName).length); attachmentMap.put("revpos", 1); attachmentsMap.put(attachmentName, attachmentMap); return attachmentsMap; } public Map<String, Object> getJsonMap() { return jsonMap; } public void setJsonMap(Map<String, Object> jsonMap) { this.jsonMap = jsonMap; } public String getAttachmentName() { return attachmentName; } public void setAttachmentName(String attachmentName) { this.attachmentName = attachmentName; } public boolean hasAttachment() { if (this.attachmentName != null && this.attachmentName.length() > 0) { return true; } return false; } public String getDocPathRegex() { return String.format(Locale.ENGLISH, "/db/%s\\?.*", getDocId()); } public String getDocId() { return docId; } public void setDocId(String docId) { this.docId = docId; } public String getDocRev() { return docRev; } public void setDocRev(String docRev) { this.docRev = docRev; } public int getDocSeq() { return docSeq; } public void setDocSeq(int docSeq) { this.docSeq = docSeq; } public boolean isMissing() { return missing; } public void setMissing(boolean missing) { this.missing = missing; } } }