github.com/darrenli6/fabric-sdk-example@v0.0.0-20220109053535-94b13b56df8c/core/ledger/util/couchdb/couchdb.go (about)

     1  /*
     2  Copyright IBM Corp. 2016, 2017 All Rights Reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  		 http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package couchdb
    18  
    19  import (
    20  	"bytes"
    21  	"compress/gzip"
    22  	"encoding/base64"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"log"
    28  	"mime"
    29  	"mime/multipart"
    30  	"net/http"
    31  	"net/http/httputil"
    32  	"net/textproto"
    33  	"net/url"
    34  	"regexp"
    35  	"strconv"
    36  	"strings"
    37  	"time"
    38  	"unicode/utf8"
    39  
    40  	"github.com/hyperledger/fabric/common/flogging"
    41  	logging "github.com/op/go-logging"
    42  )
    43  
    44  var logger = flogging.MustGetLogger("couchdb")
    45  
    46  //time between retry attempts in milliseconds
    47  const retryWaitTime = 125
    48  
    49  // DBOperationResponse is body for successful database calls.
    50  type DBOperationResponse struct {
    51  	Ok  bool
    52  	id  string
    53  	rev string
    54  }
    55  
    56  // DBInfo is body for database information.
    57  type DBInfo struct {
    58  	DbName    string `json:"db_name"`
    59  	UpdateSeq string `json:"update_seq"`
    60  	Sizes     struct {
    61  		File     int `json:"file"`
    62  		External int `json:"external"`
    63  		Active   int `json:"active"`
    64  	} `json:"sizes"`
    65  	PurgeSeq int `json:"purge_seq"`
    66  	Other    struct {
    67  		DataSize int `json:"data_size"`
    68  	} `json:"other"`
    69  	DocDelCount       int    `json:"doc_del_count"`
    70  	DocCount          int    `json:"doc_count"`
    71  	DiskSize          int    `json:"disk_size"`
    72  	DiskFormatVersion int    `json:"disk_format_version"`
    73  	DataSize          int    `json:"data_size"`
    74  	CompactRunning    bool   `json:"compact_running"`
    75  	InstanceStartTime string `json:"instance_start_time"`
    76  }
    77  
    78  //ConnectionInfo is a structure for capturing the database info and version
    79  type ConnectionInfo struct {
    80  	Couchdb string `json:"couchdb"`
    81  	Version string `json:"version"`
    82  	Vendor  struct {
    83  		Name string `json:"name"`
    84  	} `json:"vendor"`
    85  }
    86  
    87  //RangeQueryResponse is used for processing REST range query responses from CouchDB
    88  type RangeQueryResponse struct {
    89  	TotalRows int `json:"total_rows"`
    90  	Offset    int `json:"offset"`
    91  	Rows      []struct {
    92  		ID    string `json:"id"`
    93  		Key   string `json:"key"`
    94  		Value struct {
    95  			Rev string `json:"rev"`
    96  		} `json:"value"`
    97  		Doc json.RawMessage `json:"doc"`
    98  	} `json:"rows"`
    99  }
   100  
   101  //QueryResponse is used for processing REST query responses from CouchDB
   102  type QueryResponse struct {
   103  	Warning string            `json:"warning"`
   104  	Docs    []json.RawMessage `json:"docs"`
   105  }
   106  
   107  //Doc is used for capturing if attachments are return in the query from CouchDB
   108  type Doc struct {
   109  	ID          string          `json:"_id"`
   110  	Rev         string          `json:"_rev"`
   111  	Attachments json.RawMessage `json:"_attachments"`
   112  }
   113  
   114  //DocID is a minimal structure for capturing the ID from a query result
   115  type DocID struct {
   116  	ID string `json:"_id"`
   117  }
   118  
   119  //QueryResult is used for returning query results from CouchDB
   120  type QueryResult struct {
   121  	ID          string
   122  	Value       []byte
   123  	Attachments []*Attachment
   124  }
   125  
   126  //CouchConnectionDef contains parameters
   127  type CouchConnectionDef struct {
   128  	URL                 string
   129  	Username            string
   130  	Password            string
   131  	MaxRetries          int
   132  	MaxRetriesOnStartup int
   133  	RequestTimeout      time.Duration
   134  }
   135  
   136  //CouchInstance represents a CouchDB instance
   137  type CouchInstance struct {
   138  	conf   CouchConnectionDef //connection configuration
   139  	client *http.Client       // a client to connect to this instance
   140  }
   141  
   142  //CouchDatabase represents a database within a CouchDB instance
   143  type CouchDatabase struct {
   144  	CouchInstance CouchInstance //connection configuration
   145  	DBName        string
   146  }
   147  
   148  //DBReturn contains an error reported by CouchDB
   149  type DBReturn struct {
   150  	StatusCode int    `json:"status_code"`
   151  	Error      string `json:"error"`
   152  	Reason     string `json:"reason"`
   153  }
   154  
   155  //Attachment contains the definition for an attached file for couchdb
   156  type Attachment struct {
   157  	Name            string
   158  	ContentType     string
   159  	Length          uint64
   160  	AttachmentBytes []byte
   161  }
   162  
   163  //DocMetadata returns the ID, version and revision for a couchdb document
   164  type DocMetadata struct {
   165  	ID      string
   166  	Rev     string
   167  	Version string
   168  }
   169  
   170  //FileDetails defines the structure needed to send an attachment to couchdb
   171  type FileDetails struct {
   172  	Follows     bool   `json:"follows"`
   173  	ContentType string `json:"content_type"`
   174  	Length      int    `json:"length"`
   175  }
   176  
   177  //CouchDoc defines the structure for a JSON document value
   178  type CouchDoc struct {
   179  	JSONValue   []byte
   180  	Attachments []*Attachment
   181  }
   182  
   183  //BatchRetrieveDocMedatadataResponse is used for processing REST batch responses from CouchDB
   184  type BatchRetrieveDocMedatadataResponse struct {
   185  	Rows []struct {
   186  		ID  string `json:"id"`
   187  		Doc struct {
   188  			ID      string `json:"_id"`
   189  			Rev     string `json:"_rev"`
   190  			Version string `json:"version"`
   191  		} `json:"doc"`
   192  	} `json:"rows"`
   193  }
   194  
   195  //BatchUpdateResponse defines a structure for batch update response
   196  type BatchUpdateResponse struct {
   197  	ID     string `json:"id"`
   198  	Error  string `json:"error"`
   199  	Reason string `json:"reason"`
   200  	Ok     bool   `json:"ok"`
   201  	Rev    string `json:"rev"`
   202  }
   203  
   204  //Base64Attachment contains the definition for an attached file for couchdb
   205  type Base64Attachment struct {
   206  	ContentType    string `json:"content_type"`
   207  	AttachmentData string `json:"data"`
   208  }
   209  
   210  // closeResponseBody discards the body and then closes it to enable returning it to
   211  // connection pool
   212  func closeResponseBody(resp *http.Response) {
   213  	io.Copy(ioutil.Discard, resp.Body) // discard whatever is remaining of body
   214  	resp.Body.Close()
   215  }
   216  
   217  //CreateConnectionDefinition for a new client connection
   218  func CreateConnectionDefinition(couchDBAddress, username, password string, maxRetries,
   219  	maxRetriesOnStartup int, requestTimeout time.Duration) (*CouchConnectionDef, error) {
   220  
   221  	logger.Debugf("Entering CreateConnectionDefinition()")
   222  
   223  	connectURL := &url.URL{
   224  		Host:   couchDBAddress,
   225  		Scheme: "http",
   226  	}
   227  
   228  	//parse the constructed URL to verify no errors
   229  	finalURL, err := url.Parse(connectURL.String())
   230  	if err != nil {
   231  		logger.Errorf("URL parse error: %s", err.Error())
   232  		return nil, err
   233  	}
   234  
   235  	logger.Debugf("Created database configuration  URL=[%s]", finalURL.String())
   236  	logger.Debugf("Exiting CreateConnectionDefinition()")
   237  
   238  	//return an object containing the connection information
   239  	return &CouchConnectionDef{finalURL.String(), username, password, maxRetries,
   240  		maxRetriesOnStartup, requestTimeout}, nil
   241  
   242  }
   243  
   244  //CreateDatabaseIfNotExist method provides function to create database
   245  func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() (*DBOperationResponse, error) {
   246  
   247  	logger.Debugf("Entering CreateDatabaseIfNotExist()")
   248  
   249  	dbInfo, couchDBReturn, err := dbclient.GetDatabaseInfo()
   250  	if err != nil {
   251  		if couchDBReturn == nil || couchDBReturn.StatusCode != 404 {
   252  			return nil, err
   253  		}
   254  	}
   255  
   256  	if dbInfo == nil && couchDBReturn.StatusCode == 404 {
   257  
   258  		logger.Debugf("Database %s does not exist.", dbclient.DBName)
   259  
   260  		connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   261  		if err != nil {
   262  			logger.Errorf("URL parse error: %s", err.Error())
   263  			return nil, err
   264  		}
   265  		connectURL.Path = dbclient.DBName
   266  
   267  		//get the number of retries
   268  		maxRetries := dbclient.CouchInstance.conf.MaxRetries
   269  
   270  		//process the URL with a PUT, creates the database
   271  		resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPut, connectURL.String(), nil, "", "", maxRetries, true)
   272  		if err != nil {
   273  			return nil, err
   274  		}
   275  		defer closeResponseBody(resp)
   276  
   277  		//Get the response from the create REST call
   278  		dbResponse := &DBOperationResponse{}
   279  		json.NewDecoder(resp.Body).Decode(&dbResponse)
   280  
   281  		if dbResponse.Ok == true {
   282  			logger.Debugf("Created database %s ", dbclient.DBName)
   283  		}
   284  
   285  		logger.Debugf("Exiting CreateDatabaseIfNotExist()")
   286  
   287  		return dbResponse, nil
   288  
   289  	}
   290  
   291  	logger.Debugf("Database %s already exists", dbclient.DBName)
   292  
   293  	logger.Debugf("Exiting CreateDatabaseIfNotExist()")
   294  
   295  	return nil, nil
   296  
   297  }
   298  
   299  //GetDatabaseInfo method provides function to retrieve database information
   300  func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) {
   301  
   302  	connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   303  	if err != nil {
   304  		logger.Errorf("URL parse error: %s", err.Error())
   305  		return nil, nil, err
   306  	}
   307  	connectURL.Path = dbclient.DBName
   308  
   309  	//get the number of retries
   310  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   311  
   312  	resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "", maxRetries, true)
   313  	if err != nil {
   314  		return nil, couchDBReturn, err
   315  	}
   316  	defer closeResponseBody(resp)
   317  
   318  	dbResponse := &DBInfo{}
   319  	json.NewDecoder(resp.Body).Decode(&dbResponse)
   320  
   321  	// trace the database info response
   322  	if logger.IsEnabledFor(logging.DEBUG) {
   323  		dbResponseJSON, err := json.Marshal(dbResponse)
   324  		if err == nil {
   325  			logger.Debugf("GetDatabaseInfo() dbResponseJSON: %s", dbResponseJSON)
   326  		}
   327  	}
   328  
   329  	return dbResponse, couchDBReturn, nil
   330  
   331  }
   332  
   333  //VerifyCouchConfig method provides function to verify the connection information
   334  func (couchInstance *CouchInstance) VerifyCouchConfig() (*ConnectionInfo, *DBReturn, error) {
   335  
   336  	logger.Debugf("Entering VerifyCouchConfig()")
   337  	defer logger.Debugf("Exiting VerifyCouchConfig()")
   338  
   339  	connectURL, err := url.Parse(couchInstance.conf.URL)
   340  	if err != nil {
   341  		logger.Errorf("URL parse error: %s", err.Error())
   342  		return nil, nil, err
   343  	}
   344  	connectURL.Path = "/"
   345  
   346  	//get the number of retries for startup
   347  	maxRetriesOnStartup := couchInstance.conf.MaxRetriesOnStartup
   348  
   349  	resp, couchDBReturn, err := couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil,
   350  		couchInstance.conf.Username, couchInstance.conf.Password, maxRetriesOnStartup, true)
   351  
   352  	if err != nil {
   353  		return nil, couchDBReturn, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port: %s", err.Error())
   354  	}
   355  	defer closeResponseBody(resp)
   356  
   357  	dbResponse := &ConnectionInfo{}
   358  	errJSON := json.NewDecoder(resp.Body).Decode(&dbResponse)
   359  	if errJSON != nil {
   360  		return nil, nil, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port: %s", errJSON.Error())
   361  	}
   362  
   363  	// trace the database info response
   364  	if logger.IsEnabledFor(logging.DEBUG) {
   365  		dbResponseJSON, err := json.Marshal(dbResponse)
   366  		if err == nil {
   367  			logger.Debugf("VerifyConnection() dbResponseJSON: %s", dbResponseJSON)
   368  		}
   369  	}
   370  
   371  	//check to see if the system databases exist
   372  	//Verifying the existence of the system database accomplishes two steps
   373  	//1.  Ensures the system databases are created
   374  	//2.  Verifies the username password provided in the CouchDB config are valid for system admin
   375  	err = CreateSystemDatabasesIfNotExist(*couchInstance)
   376  	if err != nil {
   377  		logger.Errorf("Unable to connect to CouchDB,  error: %s   Check the admin username and password.\n", err.Error())
   378  		return nil, nil, fmt.Errorf("Unable to connect to CouchDB,  error: %s   Check the admin username and password.\n", err.Error())
   379  	}
   380  
   381  	return dbResponse, couchDBReturn, nil
   382  }
   383  
   384  //DropDatabase provides method to drop an existing database
   385  func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) {
   386  
   387  	logger.Debugf("Entering DropDatabase()")
   388  
   389  	connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   390  	if err != nil {
   391  		logger.Errorf("URL parse error: %s", err.Error())
   392  		return nil, err
   393  	}
   394  	connectURL.Path = dbclient.DBName
   395  
   396  	//get the number of retries
   397  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   398  
   399  	resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "", maxRetries, true)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	defer closeResponseBody(resp)
   404  
   405  	dbResponse := &DBOperationResponse{}
   406  	json.NewDecoder(resp.Body).Decode(&dbResponse)
   407  
   408  	if dbResponse.Ok == true {
   409  		logger.Debugf("Dropped database %s ", dbclient.DBName)
   410  	}
   411  
   412  	logger.Debugf("Exiting DropDatabase()")
   413  
   414  	if dbResponse.Ok == true {
   415  
   416  		return dbResponse, nil
   417  
   418  	}
   419  
   420  	return dbResponse, fmt.Errorf("Error dropping database")
   421  
   422  }
   423  
   424  // EnsureFullCommit calls _ensure_full_commit for explicit fsync
   425  func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error) {
   426  
   427  	logger.Debugf("Entering EnsureFullCommit()")
   428  
   429  	connectURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   430  	if err != nil {
   431  		logger.Errorf("URL parse error: %s", err.Error())
   432  		return nil, err
   433  	}
   434  	connectURL.Path = dbclient.DBName + "/_ensure_full_commit"
   435  
   436  	//get the number of retries
   437  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   438  
   439  	resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, connectURL.String(), nil, "", "", maxRetries, true)
   440  	if err != nil {
   441  		logger.Errorf("Failed to invoke _ensure_full_commit Error: %s\n", err.Error())
   442  		return nil, err
   443  	}
   444  	defer closeResponseBody(resp)
   445  
   446  	dbResponse := &DBOperationResponse{}
   447  	json.NewDecoder(resp.Body).Decode(&dbResponse)
   448  
   449  	if dbResponse.Ok == true {
   450  		logger.Debugf("_ensure_full_commit database %s ", dbclient.DBName)
   451  	}
   452  
   453  	logger.Debugf("Exiting EnsureFullCommit()")
   454  
   455  	if dbResponse.Ok == true {
   456  
   457  		return dbResponse, nil
   458  
   459  	}
   460  
   461  	return dbResponse, fmt.Errorf("Error syncing database")
   462  }
   463  
   464  //SaveDoc method provides a function to save a document, id and byte array
   465  func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc) (string, error) {
   466  
   467  	logger.Debugf("Entering SaveDoc()  id=[%s]", id)
   468  
   469  	if !utf8.ValidString(id) {
   470  		return "", fmt.Errorf("doc id [%x] not a valid utf8 string", id)
   471  	}
   472  
   473  	saveURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   474  	if err != nil {
   475  		logger.Errorf("URL parse error: %s", err.Error())
   476  		return "", err
   477  	}
   478  
   479  	saveURL.Path = dbclient.DBName
   480  	// id can contain a '/', so encode separately
   481  	saveURL = &url.URL{Opaque: saveURL.String() + "/" + encodePathElement(id)}
   482  
   483  	logger.Debugf("  rev=%s", rev)
   484  
   485  	//Set up a buffer for the data to be pushed to couchdb
   486  	data := []byte{}
   487  
   488  	//Set up a default boundary for use by multipart if sending attachments
   489  	defaultBoundary := ""
   490  
   491  	//Create a flag for shared connections.  This is set to false for zero length attachments
   492  	keepConnectionOpen := true
   493  
   494  	//check to see if attachments is nil, if so, then this is a JSON only
   495  	if couchDoc.Attachments == nil {
   496  
   497  		//Test to see if this is a valid JSON
   498  		if IsJSON(string(couchDoc.JSONValue)) != true {
   499  			return "", fmt.Errorf("JSON format is not valid")
   500  		}
   501  
   502  		// if there are no attachments, then use the bytes passed in as the JSON
   503  		data = couchDoc.JSONValue
   504  
   505  	} else { // there are attachments
   506  
   507  		//attachments are included, create the multipart definition
   508  		multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc, defaultBoundary)
   509  		if err3 != nil {
   510  			return "", err3
   511  		}
   512  
   513  		//If there is a zero length attachment, do not keep the connection open
   514  		for _, attach := range couchDoc.Attachments {
   515  			if attach.Length < 1 {
   516  				keepConnectionOpen = false
   517  			}
   518  		}
   519  
   520  		//Set the data buffer to the data from the create multi-part data
   521  		data = multipartData.Bytes()
   522  
   523  		//Set the default boundary to the value generated in the multipart creation
   524  		defaultBoundary = multipartBoundary
   525  
   526  	}
   527  
   528  	//get the number of retries
   529  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   530  
   531  	//handle the request for saving document with a retry if there is a revision conflict
   532  	resp, _, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodPut,
   533  		*saveURL, data, rev, defaultBoundary, maxRetries, keepConnectionOpen)
   534  
   535  	if err != nil {
   536  		return "", err
   537  	}
   538  	defer closeResponseBody(resp)
   539  
   540  	//get the revision and return
   541  	revision, err := getRevisionHeader(resp)
   542  	if err != nil {
   543  		return "", err
   544  	}
   545  
   546  	logger.Debugf("Exiting SaveDoc()")
   547  
   548  	return revision, nil
   549  
   550  }
   551  
   552  //getDocumentRevision will return the revision if the document exists, otherwise it will return ""
   553  func (dbclient *CouchDatabase) getDocumentRevision(id string) string {
   554  
   555  	var rev = ""
   556  
   557  	//See if the document already exists, we need the rev for saves and deletes
   558  	_, revdoc, err := dbclient.ReadDoc(id)
   559  	if err == nil {
   560  		//set the revision to the rev returned from the document read
   561  		rev = revdoc
   562  	}
   563  	return rev
   564  }
   565  
   566  func createAttachmentPart(couchDoc *CouchDoc, defaultBoundary string) (bytes.Buffer, string, error) {
   567  
   568  	//Create a buffer for writing the result
   569  	writeBuffer := new(bytes.Buffer)
   570  
   571  	// read the attachment and save as an attachment
   572  	writer := multipart.NewWriter(writeBuffer)
   573  
   574  	//retrieve the boundary for the multipart
   575  	defaultBoundary = writer.Boundary()
   576  
   577  	fileAttachments := map[string]FileDetails{}
   578  
   579  	for _, attachment := range couchDoc.Attachments {
   580  		fileAttachments[attachment.Name] = FileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)}
   581  	}
   582  
   583  	attachmentJSONMap := map[string]interface{}{
   584  		"_attachments": fileAttachments}
   585  
   586  	//Add any data uploaded with the files
   587  	if couchDoc.JSONValue != nil {
   588  
   589  		//create a generic map
   590  		genericMap := make(map[string]interface{})
   591  
   592  		//unmarshal the data into the generic map
   593  		decoder := json.NewDecoder(bytes.NewBuffer(couchDoc.JSONValue))
   594  		decoder.UseNumber()
   595  		decoder.Decode(&genericMap)
   596  
   597  		//add all key/values to the attachmentJSONMap
   598  		for jsonKey, jsonValue := range genericMap {
   599  			attachmentJSONMap[jsonKey] = jsonValue
   600  		}
   601  
   602  	}
   603  
   604  	filesForUpload, _ := json.Marshal(attachmentJSONMap)
   605  	logger.Debugf(string(filesForUpload))
   606  
   607  	//create the header for the JSON
   608  	header := make(textproto.MIMEHeader)
   609  	header.Set("Content-Type", "application/json")
   610  
   611  	part, err := writer.CreatePart(header)
   612  	if err != nil {
   613  		return *writeBuffer, defaultBoundary, err
   614  	}
   615  
   616  	part.Write(filesForUpload)
   617  
   618  	for _, attachment := range couchDoc.Attachments {
   619  
   620  		header := make(textproto.MIMEHeader)
   621  		part, err2 := writer.CreatePart(header)
   622  		if err2 != nil {
   623  			return *writeBuffer, defaultBoundary, err2
   624  		}
   625  		part.Write(attachment.AttachmentBytes)
   626  
   627  	}
   628  
   629  	err = writer.Close()
   630  	if err != nil {
   631  		return *writeBuffer, defaultBoundary, err
   632  	}
   633  
   634  	return *writeBuffer, defaultBoundary, nil
   635  
   636  }
   637  
   638  func getRevisionHeader(resp *http.Response) (string, error) {
   639  
   640  	revision := resp.Header.Get("Etag")
   641  
   642  	if revision == "" {
   643  		return "", fmt.Errorf("No revision tag detected")
   644  	}
   645  
   646  	reg := regexp.MustCompile(`"([^"]*)"`)
   647  	revisionNoQuotes := reg.ReplaceAllString(revision, "${1}")
   648  	return revisionNoQuotes, nil
   649  
   650  }
   651  
   652  //ReadDoc method provides function to retrieve a document and its revision
   653  //from the database by id
   654  func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) {
   655  	var couchDoc CouchDoc
   656  	attachments := []*Attachment{}
   657  
   658  	logger.Debugf("Entering ReadDoc()  id=[%s]", id)
   659  	if !utf8.ValidString(id) {
   660  		return nil, "", fmt.Errorf("doc id [%x] not a valid utf8 string", id)
   661  	}
   662  
   663  	readURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   664  	if err != nil {
   665  		logger.Errorf("URL parse error: %s", err.Error())
   666  		return nil, "", err
   667  	}
   668  	readURL.Path = dbclient.DBName
   669  	// id can contain a '/', so encode separately
   670  	readURL = &url.URL{Opaque: readURL.String() + "/" + encodePathElement(id)}
   671  
   672  	query := readURL.Query()
   673  	query.Add("attachments", "true")
   674  
   675  	readURL.RawQuery = query.Encode()
   676  
   677  	//get the number of retries
   678  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   679  
   680  	resp, couchDBReturn, err := dbclient.CouchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "", maxRetries, true)
   681  	if err != nil {
   682  		if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
   683  			logger.Debug("Document not found (404), returning nil value instead of 404 error")
   684  			// non-existent document should return nil value instead of a 404 error
   685  			// for details see https://github.com/hyperledger-archives/fabric/issues/936
   686  			return nil, "", nil
   687  		}
   688  		logger.Debugf("couchDBReturn=%v\n", couchDBReturn)
   689  		return nil, "", err
   690  	}
   691  	defer closeResponseBody(resp)
   692  
   693  	//Get the media type from the Content-Type header
   694  	mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
   695  	if err != nil {
   696  		log.Fatal(err)
   697  	}
   698  
   699  	//Get the revision from header
   700  	revision, err := getRevisionHeader(resp)
   701  	if err != nil {
   702  		return nil, "", err
   703  	}
   704  
   705  	//check to see if the is multipart,  handle as attachment if multipart is detected
   706  	if strings.HasPrefix(mediaType, "multipart/") {
   707  		//Set up the multipart reader based on the boundary
   708  		multipartReader := multipart.NewReader(resp.Body, params["boundary"])
   709  		for {
   710  			p, err := multipartReader.NextPart()
   711  			if err == io.EOF {
   712  				break // processed all parts
   713  			}
   714  			if err != nil {
   715  				return nil, "", err
   716  			}
   717  
   718  			defer p.Close()
   719  
   720  			logger.Debugf("part header=%s", p.Header)
   721  			switch p.Header.Get("Content-Type") {
   722  			case "application/json":
   723  				partdata, err := ioutil.ReadAll(p)
   724  				if err != nil {
   725  					return nil, "", err
   726  				}
   727  				couchDoc.JSONValue = partdata
   728  			default:
   729  
   730  				//Create an attachment structure and load it
   731  				attachment := &Attachment{}
   732  				attachment.ContentType = p.Header.Get("Content-Type")
   733  				contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";")
   734  				if strings.TrimSpace(contentDispositionParts[0]) == "attachment" {
   735  					switch p.Header.Get("Content-Encoding") {
   736  					case "gzip": //See if the part is gzip encoded
   737  
   738  						var respBody []byte
   739  
   740  						gr, err := gzip.NewReader(p)
   741  						if err != nil {
   742  							return nil, "", err
   743  						}
   744  						respBody, err = ioutil.ReadAll(gr)
   745  						if err != nil {
   746  							return nil, "", err
   747  						}
   748  
   749  						logger.Debugf("Retrieved attachment data")
   750  						attachment.AttachmentBytes = respBody
   751  						attachment.Name = p.FileName()
   752  						attachments = append(attachments, attachment)
   753  
   754  					default:
   755  
   756  						//retrieve the data,  this is not gzip
   757  						partdata, err := ioutil.ReadAll(p)
   758  						if err != nil {
   759  							return nil, "", err
   760  						}
   761  						logger.Debugf("Retrieved attachment data")
   762  						attachment.AttachmentBytes = partdata
   763  						attachment.Name = p.FileName()
   764  						attachments = append(attachments, attachment)
   765  
   766  					} // end content-encoding switch
   767  				} // end if attachment
   768  			} // end content-type switch
   769  		} // for all multiparts
   770  
   771  		couchDoc.Attachments = attachments
   772  
   773  		return &couchDoc, revision, nil
   774  	}
   775  
   776  	//handle as JSON document
   777  	couchDoc.JSONValue, err = ioutil.ReadAll(resp.Body)
   778  	if err != nil {
   779  		return nil, "", err
   780  	}
   781  
   782  	logger.Debugf("Exiting ReadDoc()")
   783  	return &couchDoc, revision, nil
   784  }
   785  
   786  //ReadDocRange method provides function to a range of documents based on the start and end keys
   787  //startKey and endKey can also be empty strings.  If startKey and endKey are empty, all documents are returned
   788  //This function provides a limit option to specify the max number of entries and is supplied by config.
   789  //Skip is reserved for possible future future use.
   790  func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip int) (*[]QueryResult, error) {
   791  
   792  	logger.Debugf("Entering ReadDocRange()  startKey=%s, endKey=%s", startKey, endKey)
   793  
   794  	var results []QueryResult
   795  
   796  	rangeURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   797  	if err != nil {
   798  		logger.Errorf("URL parse error: %s", err.Error())
   799  		return nil, err
   800  	}
   801  	rangeURL.Path = dbclient.DBName + "/_all_docs"
   802  
   803  	queryParms := rangeURL.Query()
   804  	queryParms.Set("limit", strconv.Itoa(limit))
   805  	queryParms.Add("skip", strconv.Itoa(skip))
   806  	queryParms.Add("include_docs", "true")
   807  	queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb
   808  
   809  	//Append the startKey if provided
   810  
   811  	if startKey != "" {
   812  		var err error
   813  		if startKey, err = encodeForJSON(startKey); err != nil {
   814  			return nil, err
   815  		}
   816  		queryParms.Add("startkey", "\""+startKey+"\"")
   817  	}
   818  
   819  	//Append the endKey if provided
   820  	if endKey != "" {
   821  		var err error
   822  		if endKey, err = encodeForJSON(endKey); err != nil {
   823  			return nil, err
   824  		}
   825  		queryParms.Add("endkey", "\""+endKey+"\"")
   826  	}
   827  
   828  	rangeURL.RawQuery = queryParms.Encode()
   829  
   830  	//get the number of retries
   831  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   832  
   833  	resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "", maxRetries, true)
   834  	if err != nil {
   835  		return nil, err
   836  	}
   837  	defer closeResponseBody(resp)
   838  
   839  	if logger.IsEnabledFor(logging.DEBUG) {
   840  		dump, err2 := httputil.DumpResponse(resp, true)
   841  		if err2 != nil {
   842  			log.Fatal(err2)
   843  		}
   844  		logger.Debugf("%s", dump)
   845  	}
   846  
   847  	//handle as JSON document
   848  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
   849  	if err != nil {
   850  		return nil, err
   851  	}
   852  
   853  	var jsonResponse = &RangeQueryResponse{}
   854  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
   855  	if err2 != nil {
   856  		return nil, err2
   857  	}
   858  
   859  	logger.Debugf("Total Rows: %d", jsonResponse.TotalRows)
   860  
   861  	for _, row := range jsonResponse.Rows {
   862  
   863  		var jsonDoc = &Doc{}
   864  		err3 := json.Unmarshal(row.Doc, &jsonDoc)
   865  		if err3 != nil {
   866  			return nil, err3
   867  		}
   868  
   869  		if jsonDoc.Attachments != nil {
   870  
   871  			logger.Debugf("Adding JSON document and attachments for id: %s", jsonDoc.ID)
   872  
   873  			couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID)
   874  			if err != nil {
   875  				return nil, err
   876  			}
   877  
   878  			var addDocument = &QueryResult{jsonDoc.ID, couchDoc.JSONValue, couchDoc.Attachments}
   879  			results = append(results, *addDocument)
   880  
   881  		} else {
   882  
   883  			logger.Debugf("Adding json docment for id: %s", jsonDoc.ID)
   884  
   885  			var addDocument = &QueryResult{jsonDoc.ID, row.Doc, nil}
   886  			results = append(results, *addDocument)
   887  
   888  		}
   889  
   890  	}
   891  
   892  	logger.Debugf("Exiting ReadDocRange()")
   893  
   894  	return &results, nil
   895  
   896  }
   897  
   898  //DeleteDoc method provides function to delete a document from the database by id
   899  func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error {
   900  
   901  	logger.Debugf("Entering DeleteDoc()  id=%s", id)
   902  
   903  	deleteURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   904  	if err != nil {
   905  		logger.Errorf("URL parse error: %s", err.Error())
   906  		return err
   907  	}
   908  
   909  	deleteURL.Path = dbclient.DBName
   910  	// id can contain a '/', so encode separately
   911  	deleteURL = &url.URL{Opaque: deleteURL.String() + "/" + encodePathElement(id)}
   912  
   913  	//get the number of retries
   914  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   915  
   916  	//handle the request for saving document with a retry if there is a revision conflict
   917  	resp, couchDBReturn, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodDelete,
   918  		*deleteURL, nil, "", "", maxRetries, true)
   919  
   920  	if err != nil {
   921  		if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
   922  			logger.Debug("Document not found (404), returning nil value instead of 404 error")
   923  			// non-existent document should return nil value instead of a 404 error
   924  			// for details see https://github.com/hyperledger-archives/fabric/issues/936
   925  			return nil
   926  		}
   927  		return err
   928  	}
   929  	defer closeResponseBody(resp)
   930  
   931  	logger.Debugf("Exiting DeleteDoc()")
   932  
   933  	return nil
   934  
   935  }
   936  
   937  //QueryDocuments method provides function for processing a query
   938  func (dbclient *CouchDatabase) QueryDocuments(query string) (*[]QueryResult, error) {
   939  
   940  	logger.Debugf("Entering QueryDocuments()  query=%s", query)
   941  
   942  	var results []QueryResult
   943  
   944  	queryURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
   945  	if err != nil {
   946  		logger.Errorf("URL parse error: %s", err.Error())
   947  		return nil, err
   948  	}
   949  
   950  	queryURL.Path = dbclient.DBName + "/_find"
   951  
   952  	//get the number of retries
   953  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
   954  
   955  	resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, queryURL.String(), []byte(query), "", "", maxRetries, true)
   956  	if err != nil {
   957  		return nil, err
   958  	}
   959  	defer closeResponseBody(resp)
   960  
   961  	if logger.IsEnabledFor(logging.DEBUG) {
   962  		dump, err2 := httputil.DumpResponse(resp, true)
   963  		if err2 != nil {
   964  			log.Fatal(err2)
   965  		}
   966  		logger.Debugf("%s", dump)
   967  	}
   968  
   969  	//handle as JSON document
   970  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
   971  	if err != nil {
   972  		return nil, err
   973  	}
   974  
   975  	var jsonResponse = &QueryResponse{}
   976  
   977  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
   978  	if err2 != nil {
   979  		return nil, err2
   980  	}
   981  
   982  	for _, row := range jsonResponse.Docs {
   983  
   984  		var jsonDoc = &Doc{}
   985  		err3 := json.Unmarshal(row, &jsonDoc)
   986  		if err3 != nil {
   987  			return nil, err3
   988  		}
   989  
   990  		if jsonDoc.Attachments != nil {
   991  
   992  			logger.Debugf("Adding JSON docment and attachments for id: %s", jsonDoc.ID)
   993  
   994  			couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID)
   995  			if err != nil {
   996  				return nil, err
   997  			}
   998  			var addDocument = &QueryResult{ID: jsonDoc.ID, Value: couchDoc.JSONValue, Attachments: couchDoc.Attachments}
   999  			results = append(results, *addDocument)
  1000  
  1001  		} else {
  1002  			logger.Debugf("Adding json docment for id: %s", jsonDoc.ID)
  1003  			var addDocument = &QueryResult{ID: jsonDoc.ID, Value: row, Attachments: nil}
  1004  
  1005  			results = append(results, *addDocument)
  1006  
  1007  		}
  1008  	}
  1009  	logger.Debugf("Exiting QueryDocuments()")
  1010  
  1011  	return &results, nil
  1012  
  1013  }
  1014  
  1015  //BatchRetrieveIDRevision - batch method to retrieve IDs and revisions
  1016  func (dbclient *CouchDatabase) BatchRetrieveIDRevision(keys []string) ([]*DocMetadata, error) {
  1017  
  1018  	batchURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
  1019  	if err != nil {
  1020  		logger.Errorf("URL parse error: %s", err.Error())
  1021  		return nil, err
  1022  	}
  1023  	batchURL.Path = dbclient.DBName + "/_all_docs"
  1024  
  1025  	queryParms := batchURL.Query()
  1026  	queryParms.Add("include_docs", "true")
  1027  	batchURL.RawQuery = queryParms.Encode()
  1028  
  1029  	keymap := make(map[string]interface{})
  1030  
  1031  	keymap["keys"] = keys
  1032  
  1033  	jsonKeys, err := json.Marshal(keymap)
  1034  	if err != nil {
  1035  		return nil, err
  1036  	}
  1037  
  1038  	//get the number of retries
  1039  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
  1040  
  1041  	resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries, true)
  1042  	if err != nil {
  1043  		return nil, err
  1044  	}
  1045  	defer closeResponseBody(resp)
  1046  
  1047  	if logger.IsEnabledFor(logging.DEBUG) {
  1048  		dump, _ := httputil.DumpResponse(resp, false)
  1049  		// compact debug log by replacing carriage return / line feed with dashes to separate http headers
  1050  		logger.Debugf("HTTP Response: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
  1051  	}
  1052  
  1053  	//handle as JSON document
  1054  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
  1055  	if err != nil {
  1056  		return nil, err
  1057  	}
  1058  
  1059  	var jsonResponse = &BatchRetrieveDocMedatadataResponse{}
  1060  
  1061  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
  1062  	if err2 != nil {
  1063  		return nil, err2
  1064  	}
  1065  
  1066  	revisionDocs := []*DocMetadata{}
  1067  
  1068  	for _, row := range jsonResponse.Rows {
  1069  		revisionDoc := &DocMetadata{ID: row.ID, Rev: row.Doc.Rev, Version: row.Doc.Version}
  1070  		revisionDocs = append(revisionDocs, revisionDoc)
  1071  	}
  1072  
  1073  	return revisionDocs, nil
  1074  
  1075  }
  1076  
  1077  //BatchUpdateDocuments - batch method to batch update documents
  1078  func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*BatchUpdateResponse, error) {
  1079  
  1080  	logger.Debugf("Entering BatchUpdateDocuments()  documents=%v", documents)
  1081  
  1082  	batchURL, err := url.Parse(dbclient.CouchInstance.conf.URL)
  1083  	if err != nil {
  1084  		logger.Errorf("URL parse error: %s", err.Error())
  1085  		return nil, err
  1086  	}
  1087  	batchURL.Path = dbclient.DBName + "/_bulk_docs"
  1088  
  1089  	documentMap := make(map[string]interface{})
  1090  
  1091  	var jsonDocumentMap []interface{}
  1092  
  1093  	for _, jsonDocument := range documents {
  1094  
  1095  		//create a document map
  1096  		var document = make(map[string]interface{})
  1097  
  1098  		//unmarshal the JSON component of the CouchDoc into the document
  1099  		json.Unmarshal(jsonDocument.JSONValue, &document)
  1100  
  1101  		//iterate through any attachments
  1102  		if len(jsonDocument.Attachments) > 0 {
  1103  
  1104  			//create a file attachment map
  1105  			fileAttachment := make(map[string]interface{})
  1106  
  1107  			//for each attachment, create a Base64Attachment, name the attachment,
  1108  			//add the content type and base64 encode the attachment
  1109  			for _, attachment := range jsonDocument.Attachments {
  1110  				fileAttachment[attachment.Name] = Base64Attachment{attachment.ContentType,
  1111  					base64.StdEncoding.EncodeToString(attachment.AttachmentBytes)}
  1112  			}
  1113  
  1114  			//add attachments to the document
  1115  			document["_attachments"] = fileAttachment
  1116  
  1117  		}
  1118  
  1119  		//Append the document to the map of documents
  1120  		jsonDocumentMap = append(jsonDocumentMap, document)
  1121  
  1122  	}
  1123  
  1124  	//Add the documents to the "docs" item
  1125  	documentMap["docs"] = jsonDocumentMap
  1126  
  1127  	jsonKeys, err := json.Marshal(documentMap)
  1128  
  1129  	if err != nil {
  1130  		return nil, err
  1131  	}
  1132  
  1133  	//get the number of retries
  1134  	maxRetries := dbclient.CouchInstance.conf.MaxRetries
  1135  
  1136  	resp, _, err := dbclient.CouchInstance.handleRequest(http.MethodPost, batchURL.String(), jsonKeys, "", "", maxRetries, true)
  1137  	if err != nil {
  1138  		return nil, err
  1139  	}
  1140  	defer closeResponseBody(resp)
  1141  
  1142  	if logger.IsEnabledFor(logging.DEBUG) {
  1143  		dump, _ := httputil.DumpResponse(resp, false)
  1144  		// compact debug log by replacing carriage return / line feed with dashes to separate http headers
  1145  		logger.Debugf("HTTP Response: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
  1146  	}
  1147  
  1148  	//handle as JSON document
  1149  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
  1150  	if err != nil {
  1151  		return nil, err
  1152  	}
  1153  
  1154  	var jsonResponse = []*BatchUpdateResponse{}
  1155  
  1156  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
  1157  	if err2 != nil {
  1158  		return nil, err2
  1159  	}
  1160  
  1161  	logger.Debugf("Exiting BatchUpdateDocuments()")
  1162  
  1163  	return jsonResponse, nil
  1164  
  1165  }
  1166  
  1167  //handleRequestWithRevisionRetry method is a generic http request handler with
  1168  //a retry for document revision conflict errors,
  1169  //which may be detected during saves or deletes that timed out from client http perspective,
  1170  //but which eventually succeeded in couchdb
  1171  func (dbclient *CouchDatabase) handleRequestWithRevisionRetry(id, method string, connectURL url.URL, data []byte, rev string,
  1172  	multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) {
  1173  
  1174  	//Initialize a flag for the revsion conflict
  1175  	revisionConflictDetected := false
  1176  	var resp *http.Response
  1177  	var couchDBReturn *DBReturn
  1178  	var errResp error
  1179  
  1180  	//attempt the http request for the max number of retries
  1181  	//In this case, the retry is to catch problems where a client timeout may miss a
  1182  	//successful CouchDB update and cause a document revision conflict on a retry in handleRequest
  1183  	for attempts := 0; attempts < maxRetries; attempts++ {
  1184  
  1185  		//if the revision was not passed in, or if a revision conflict is detected on prior attempt,
  1186  		//query CouchDB for the document revision
  1187  		if rev == "" || revisionConflictDetected {
  1188  			rev = dbclient.getDocumentRevision(id)
  1189  		}
  1190  
  1191  		//handle the request for saving/deleting the couchdb data
  1192  		resp, couchDBReturn, errResp = dbclient.CouchInstance.handleRequest(method, connectURL.String(),
  1193  			data, rev, multipartBoundary, maxRetries, keepConnectionOpen)
  1194  
  1195  		//If there was a 409 conflict error during the save/delete, log it and retry it.
  1196  		//Otherwise, break out of the retry loop
  1197  		if couchDBReturn != nil && couchDBReturn.StatusCode == 409 {
  1198  			logger.Warningf("CouchDB document revision conflict detected, retrying. Attempt:%v", attempts+1)
  1199  			revisionConflictDetected = true
  1200  		} else {
  1201  			break
  1202  		}
  1203  	}
  1204  
  1205  	// return the handleRequest results
  1206  	return resp, couchDBReturn, errResp
  1207  }
  1208  
  1209  //handleRequest method is a generic http request handler.
  1210  // If it returns an error, it ensures that the response body is closed, else it is the
  1211  // callee's responsibility to close response correctly.
  1212  // Any http error or CouchDB error (4XX or 500) will result in a golang error getting returned
  1213  func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data []byte, rev string,
  1214  	multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) {
  1215  
  1216  	logger.Debugf("Entering handleRequest()  method=%s  url=%v", method, connectURL)
  1217  
  1218  	//create the return objects for couchDB
  1219  	var resp *http.Response
  1220  	var errResp error
  1221  	couchDBReturn := &DBReturn{}
  1222  
  1223  	//set initial wait duration for retries
  1224  	waitDuration := retryWaitTime * time.Millisecond
  1225  
  1226  	if maxRetries < 1 {
  1227  		return nil, nil, fmt.Errorf("Number of retries must be greater than zero.")
  1228  	}
  1229  
  1230  	//attempt the http request for the max number of retries
  1231  	for attempts := 0; attempts < maxRetries; attempts++ {
  1232  
  1233  		//Set up a buffer for the payload data
  1234  		payloadData := new(bytes.Buffer)
  1235  
  1236  		payloadData.ReadFrom(bytes.NewReader(data))
  1237  
  1238  		//Create request based on URL for couchdb operation
  1239  		req, err := http.NewRequest(method, connectURL, payloadData)
  1240  		if err != nil {
  1241  			return nil, nil, err
  1242  		}
  1243  
  1244  		//set the request to close on completion if shared connections are not allowSharedConnection
  1245  		//Current CouchDB has a problem with zero length attachments, do not allow the connection to be reused.
  1246  		//Apache JIRA item for CouchDB   https://issues.apache.org/jira/browse/COUCHDB-3394
  1247  		if !keepConnectionOpen {
  1248  			req.Close = true
  1249  		}
  1250  
  1251  		//add content header for PUT
  1252  		if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete {
  1253  
  1254  			//If the multipartBoundary is not set, then this is a JSON and content-type should be set
  1255  			//to application/json.   Else, this is contains an attachment and needs to be multipart
  1256  			if multipartBoundary == "" {
  1257  				req.Header.Set("Content-Type", "application/json")
  1258  			} else {
  1259  				req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"")
  1260  			}
  1261  
  1262  			//check to see if the revision is set,  if so, pass as a header
  1263  			if rev != "" {
  1264  				req.Header.Set("If-Match", rev)
  1265  			}
  1266  		}
  1267  
  1268  		//add content header for PUT
  1269  		if method == http.MethodPut || method == http.MethodPost {
  1270  			req.Header.Set("Accept", "application/json")
  1271  		}
  1272  
  1273  		//add content header for GET
  1274  		if method == http.MethodGet {
  1275  			req.Header.Set("Accept", "multipart/related")
  1276  		}
  1277  
  1278  		//If username and password are set the use basic auth
  1279  		if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" {
  1280  			req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password)
  1281  		}
  1282  
  1283  		if logger.IsEnabledFor(logging.DEBUG) {
  1284  			dump, _ := httputil.DumpRequestOut(req, false)
  1285  			// compact debug log by replacing carriage return / line feed with dashes to separate http headers
  1286  			logger.Debugf("HTTP Request: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
  1287  		}
  1288  
  1289  		//Execute http request
  1290  		resp, errResp = couchInstance.client.Do(req)
  1291  
  1292  		//check to see if the return from CouchDB is valid
  1293  		if invalidCouchDBReturn(resp, errResp) {
  1294  			continue
  1295  		}
  1296  
  1297  		//if there is no golang http error and no CouchDB 500 error, then drop out of the retry
  1298  		if errResp == nil && resp != nil && resp.StatusCode < 500 {
  1299  			break
  1300  		}
  1301  
  1302  		//if this is an unexpected golang http error, log the error and retry
  1303  		if errResp != nil {
  1304  
  1305  			//Log the error with the retry count and continue
  1306  			logger.Warningf("Retrying couchdb request in %s. Attempt:%v  Error:%v",
  1307  				waitDuration.String(), attempts+1, errResp.Error())
  1308  
  1309  			//otherwise this is an unexpected 500 error from CouchDB. Log the error and retry.
  1310  		} else {
  1311  			//Read the response body and close it for next attempt
  1312  			jsonError, err := ioutil.ReadAll(resp.Body)
  1313  			closeResponseBody(resp)
  1314  			if err != nil {
  1315  				return nil, nil, err
  1316  			}
  1317  
  1318  			errorBytes := []byte(jsonError)
  1319  
  1320  			//Unmarshal the response
  1321  			json.Unmarshal(errorBytes, &couchDBReturn)
  1322  
  1323  			//Log the 500 error with the retry count and continue
  1324  			logger.Warningf("Retrying couchdb request in %s. Attempt:%v  Couch DB Error:%s,  Status Code:%v  Reason:%v",
  1325  				waitDuration.String(), attempts+1, couchDBReturn.Error, resp.Status, couchDBReturn.Reason)
  1326  
  1327  		}
  1328  		//sleep for specified sleep time, then retry
  1329  		time.Sleep(waitDuration)
  1330  
  1331  		//backoff, doubling the retry time for next attempt
  1332  		waitDuration *= 2
  1333  
  1334  	} // end retry loop
  1335  
  1336  	//if a golang http error is still present after retries are exhausted, return the error
  1337  	if errResp != nil {
  1338  		return nil, nil, errResp
  1339  	}
  1340  
  1341  	//This situation should not occur according to the golang spec.
  1342  	//if this error returned (errResp) from an http call, then the resp should be not nil,
  1343  	//this is a structure and StatusCode is an int
  1344  	//This is meant to provide a more graceful error if this should occur
  1345  	if invalidCouchDBReturn(resp, errResp) {
  1346  		return nil, nil, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port.")
  1347  	}
  1348  
  1349  	//set the return code for the couchDB request
  1350  	couchDBReturn.StatusCode = resp.StatusCode
  1351  
  1352  	//check to see if the status code from couchdb is 400 or higher
  1353  	//response codes 4XX and 500 will be treated as errors -
  1354  	//golang error will be created from the couchDBReturn contents and both will be returned
  1355  	if resp.StatusCode >= 400 {
  1356  		// close the response before returning error
  1357  		defer closeResponseBody(resp)
  1358  
  1359  		//Read the response body
  1360  		jsonError, err := ioutil.ReadAll(resp.Body)
  1361  		if err != nil {
  1362  			return nil, nil, err
  1363  		}
  1364  
  1365  		errorBytes := []byte(jsonError)
  1366  
  1367  		//marshal the response
  1368  		json.Unmarshal(errorBytes, &couchDBReturn)
  1369  
  1370  		logger.Debugf("Couch DB Error:%s,  Status Code:%v,  Reason:%s",
  1371  			couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason)
  1372  
  1373  		return nil, couchDBReturn, fmt.Errorf("Couch DB Error:%s,  Status Code:%v,  Reason:%s",
  1374  			couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason)
  1375  
  1376  	}
  1377  
  1378  	logger.Debugf("Exiting handleRequest()")
  1379  
  1380  	//If no errors, then return the http response and the couchdb return object
  1381  	return resp, couchDBReturn, nil
  1382  }
  1383  
  1384  //invalidCouchDBResponse checks to make sure either a valid response or error is returned
  1385  func invalidCouchDBReturn(resp *http.Response, errResp error) bool {
  1386  	if resp == nil && errResp == nil {
  1387  		return true
  1388  	}
  1389  	return false
  1390  }
  1391  
  1392  //IsJSON tests a string to determine if a valid JSON
  1393  func IsJSON(s string) bool {
  1394  	var js map[string]interface{}
  1395  	return json.Unmarshal([]byte(s), &js) == nil
  1396  }
  1397  
  1398  // encodePathElement uses Golang for url path encoding, additionally:
  1399  // '/' is replaced by %2F, otherwise path encoding will treat as path separator and ignore it
  1400  // '+' is replaced by %2B, otherwise path encoding will ignore it, while CouchDB will unencode the plus as a space
  1401  // Note that all other URL special characters have been tested successfully without need for special handling
  1402  func encodePathElement(str string) string {
  1403  
  1404  	logger.Debugf("Entering encodePathElement()  string=%s", str)
  1405  
  1406  	u := &url.URL{}
  1407  	u.Path = str
  1408  	encodedStr := u.EscapedPath() // url encode using golang url path encoding rules
  1409  	encodedStr = strings.Replace(encodedStr, "/", "%2F", -1)
  1410  	encodedStr = strings.Replace(encodedStr, "+", "%2B", -1)
  1411  
  1412  	logger.Debugf("Exiting encodePathElement()  encodedStr=%s", encodedStr)
  1413  
  1414  	return encodedStr
  1415  }
  1416  
  1417  func encodeForJSON(str string) (string, error) {
  1418  	buf := &bytes.Buffer{}
  1419  	encoder := json.NewEncoder(buf)
  1420  	if err := encoder.Encode(str); err != nil {
  1421  		return "", err
  1422  	}
  1423  	// Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127)
  1424  	buffer := buf.Bytes()
  1425  	return string(buffer[1 : len(buffer)-2]), nil
  1426  }