github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/core/ledger/kvledger/txmgmt/statedb/statecouchdb/couchdb.go (about)

     1  /*
     2  Copyright IBM Corp. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package statecouchdb
     8  
     9  import (
    10  	"bytes"
    11  	"compress/gzip"
    12  	"context"
    13  	"encoding/base64"
    14  	"encoding/json"
    15  	"fmt"
    16  	"io"
    17  	"io/ioutil"
    18  	"log"
    19  	"mime"
    20  	"mime/multipart"
    21  	"net/http"
    22  	"net/http/httputil"
    23  	"net/textproto"
    24  	"net/url"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  	"unicode/utf8"
    30  
    31  	"github.com/osdi23p228/fabric/common/flogging"
    32  	"github.com/osdi23p228/fabric/core/ledger"
    33  	"github.com/pkg/errors"
    34  	"go.uber.org/zap/zapcore"
    35  )
    36  
    37  var couchdbLogger = flogging.MustGetLogger("couchdb")
    38  
    39  //time between retry attempts in milliseconds
    40  const retryWaitTime = 125
    41  
    42  // dbOperationResponse is body for successful database calls.
    43  type dbOperationResponse struct {
    44  	Ok bool
    45  }
    46  
    47  // dbInfo is body for database information.
    48  type dbInfo struct {
    49  	DbName string `json:"db_name"`
    50  	Sizes  struct {
    51  		File     int `json:"file"`
    52  		External int `json:"external"`
    53  		Active   int `json:"active"`
    54  	} `json:"sizes"`
    55  	Other struct {
    56  		DataSize int `json:"data_size"`
    57  	} `json:"other"`
    58  	DocDelCount       int    `json:"doc_del_count"`
    59  	DocCount          int    `json:"doc_count"`
    60  	DiskSize          int    `json:"disk_size"`
    61  	DiskFormatVersion int    `json:"disk_format_version"`
    62  	DataSize          int    `json:"data_size"`
    63  	CompactRunning    bool   `json:"compact_running"`
    64  	InstanceStartTime string `json:"instance_start_time"`
    65  }
    66  
    67  //connectionInfo is a structure for capturing the database info and version
    68  type connectionInfo struct {
    69  	Couchdb string `json:"couchdb"`
    70  	Version string `json:"version"`
    71  	Vendor  struct {
    72  		Name string `json:"name"`
    73  	} `json:"vendor"`
    74  }
    75  
    76  //rangeQueryResponse is used for processing REST range query responses from CouchDB
    77  type rangeQueryResponse struct {
    78  	TotalRows int32 `json:"total_rows"`
    79  	Offset    int32 `json:"offset"`
    80  	Rows      []struct {
    81  		ID    string `json:"id"`
    82  		Key   string `json:"key"`
    83  		Value struct {
    84  			Rev string `json:"rev"`
    85  		} `json:"value"`
    86  		Doc json.RawMessage `json:"doc"`
    87  	} `json:"rows"`
    88  }
    89  
    90  //queryResponse is used for processing REST query responses from CouchDB
    91  type queryResponse struct {
    92  	Warning  string            `json:"warning"`
    93  	Docs     []json.RawMessage `json:"docs"`
    94  	Bookmark string            `json:"bookmark"`
    95  }
    96  
    97  // docMetadata is used for capturing CouchDB document header info,
    98  // used to capture id, version, rev and attachments returned in the query from CouchDB
    99  type docMetadata struct {
   100  	ID              string                     `json:"_id"`
   101  	Rev             string                     `json:"_rev"`
   102  	Version         string                     `json:"~version"`
   103  	AttachmentsInfo map[string]*attachmentInfo `json:"_attachments"`
   104  }
   105  
   106  //queryResult is used for returning query results from CouchDB
   107  type queryResult struct {
   108  	id          string
   109  	value       []byte
   110  	attachments []*attachmentInfo
   111  }
   112  
   113  //couchInstance represents a CouchDB instance
   114  type couchInstance struct {
   115  	conf   *ledger.CouchDBConfig
   116  	client *http.Client // a client to connect to this instance
   117  	stats  *stats
   118  }
   119  
   120  //couchDatabase represents a database within a CouchDB instance
   121  type couchDatabase struct {
   122  	couchInstance    *couchInstance //connection configuration
   123  	dbName           string
   124  	indexWarmCounter int
   125  }
   126  
   127  //dbReturn contains an error reported by CouchDB
   128  type dbReturn struct {
   129  	StatusCode int    `json:"status_code"`
   130  	Error      string `json:"error"`
   131  	Reason     string `json:"reason"`
   132  }
   133  
   134  //createIndexResponse contains an the index creation response from CouchDB
   135  type createIndexResponse struct {
   136  	Result string `json:"result"`
   137  	ID     string `json:"id"`
   138  	Name   string `json:"name"`
   139  }
   140  
   141  //attachmentInfo contains the definition for an attached file for couchdb
   142  type attachmentInfo struct {
   143  	Name            string
   144  	ContentType     string `json:"content_type"`
   145  	Length          uint64
   146  	AttachmentBytes []byte `json:"data"`
   147  }
   148  
   149  //fileDetails defines the structure needed to send an attachment to couchdb
   150  type fileDetails struct {
   151  	Follows     bool   `json:"follows"`
   152  	ContentType string `json:"content_type"`
   153  	Length      int    `json:"length"`
   154  }
   155  
   156  //batchRetrieveDocMetadataResponse is used for processing REST batch responses from CouchDB
   157  type batchRetrieveDocMetadataResponse struct {
   158  	Rows []struct {
   159  		ID          string `json:"id"`
   160  		DocMetadata struct {
   161  			ID      string `json:"_id"`
   162  			Rev     string `json:"_rev"`
   163  			Version string `json:"~version"`
   164  		} `json:"doc"`
   165  	} `json:"rows"`
   166  }
   167  
   168  //batchUpdateResponse defines a structure for batch update response
   169  type batchUpdateResponse struct {
   170  	ID     string `json:"id"`
   171  	Error  string `json:"error"`
   172  	Reason string `json:"reason"`
   173  	Ok     bool   `json:"ok"`
   174  	Rev    string `json:"rev"`
   175  }
   176  
   177  //base64Attachment contains the definition for an attached file for couchdb
   178  type base64Attachment struct {
   179  	ContentType    string `json:"content_type"`
   180  	AttachmentData string `json:"data"`
   181  }
   182  
   183  //indexResult contains the definition for a couchdb index
   184  type indexResult struct {
   185  	DesignDocument string `json:"designdoc"`
   186  	Name           string `json:"name"`
   187  	Definition     string `json:"definition"`
   188  }
   189  
   190  //databaseSecurity contains the definition for CouchDB database security
   191  type databaseSecurity struct {
   192  	Admins struct {
   193  		Names []string `json:"names"`
   194  		Roles []string `json:"roles"`
   195  	} `json:"admins"`
   196  	Members struct {
   197  		Names []string `json:"names"`
   198  		Roles []string `json:"roles"`
   199  	} `json:"members"`
   200  }
   201  
   202  //couchDoc defines the structure for a JSON document value
   203  type couchDoc struct {
   204  	jsonValue   []byte
   205  	attachments []*attachmentInfo
   206  }
   207  
   208  func (d *couchDoc) key() (string, error) {
   209  	m := make(jsonValue)
   210  	if err := json.Unmarshal(d.jsonValue, &m); err != nil {
   211  		return "", err
   212  	}
   213  	return m[idField].(string), nil
   214  
   215  }
   216  
   217  // closeResponseBody discards the body and then closes it to enable returning it to
   218  // connection pool
   219  func closeResponseBody(resp *http.Response) {
   220  	if resp != nil {
   221  		io.Copy(ioutil.Discard, resp.Body) // discard whatever is remaining of body
   222  		resp.Body.Close()
   223  	}
   224  }
   225  
   226  //createDatabaseIfNotExist method provides function to create database
   227  func (dbclient *couchDatabase) createDatabaseIfNotExist() error {
   228  	couchdbLogger.Debugf("[%s] Entering CreateDatabaseIfNotExist()", dbclient.dbName)
   229  
   230  	dbInfo, couchDBReturn, err := dbclient.getDatabaseInfo()
   231  	if err != nil {
   232  		if couchDBReturn == nil || couchDBReturn.StatusCode != 404 {
   233  			return err
   234  		}
   235  	}
   236  
   237  	if dbInfo == nil || couchDBReturn.StatusCode == 404 {
   238  		couchdbLogger.Debugf("[%s] Database does not exist.", dbclient.dbName)
   239  
   240  		connectURL, err := url.Parse(dbclient.couchInstance.url())
   241  		if err != nil {
   242  			couchdbLogger.Errorf("URL parse error: %s", err)
   243  			return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
   244  		}
   245  
   246  		//get the number of retries
   247  		maxRetries := dbclient.couchInstance.conf.MaxRetries
   248  
   249  		//process the URL with a PUT, creates the database
   250  		resp, _, err := dbclient.handleRequest(http.MethodPut, "CreateDatabaseIfNotExist", connectURL, nil, "", "", maxRetries, true, nil)
   251  		if err != nil {
   252  			// Check to see if the database exists
   253  			// Even though handleRequest() returned an error, the
   254  			// database may have been created and a false error
   255  			// returned due to a timeout or race condition.
   256  			// Do a final check to see if the database really got created.
   257  			dbInfo, couchDBReturn, dbInfoErr := dbclient.getDatabaseInfo()
   258  			if dbInfoErr != nil || dbInfo == nil || couchDBReturn.StatusCode == 404 {
   259  				return err
   260  			}
   261  		}
   262  		defer closeResponseBody(resp)
   263  		couchdbLogger.Infof("Created state database %s", dbclient.dbName)
   264  	} else {
   265  		couchdbLogger.Debugf("[%s] Database already exists", dbclient.dbName)
   266  	}
   267  
   268  	if dbclient.dbName != "_users" {
   269  		errSecurity := dbclient.applyDatabasePermissions()
   270  		if errSecurity != nil {
   271  			return errSecurity
   272  		}
   273  	}
   274  
   275  	couchdbLogger.Debugf("[%s] Exiting CreateDatabaseIfNotExist()", dbclient.dbName)
   276  	return nil
   277  }
   278  
   279  func (dbclient *couchDatabase) applyDatabasePermissions() error {
   280  
   281  	//If the username and password are not set, then skip applying permissions
   282  	if dbclient.couchInstance.conf.Username == "" && dbclient.couchInstance.conf.Password == "" {
   283  		return nil
   284  	}
   285  
   286  	securityPermissions := &databaseSecurity{}
   287  
   288  	securityPermissions.Admins.Names = append(securityPermissions.Admins.Names, dbclient.couchInstance.conf.Username)
   289  	securityPermissions.Members.Names = append(securityPermissions.Members.Names, dbclient.couchInstance.conf.Username)
   290  
   291  	err := dbclient.applyDatabaseSecurity(securityPermissions)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	return nil
   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.url())
   303  	if err != nil {
   304  		couchdbLogger.Errorf("URL parse error: %s", err)
   305  		return nil, nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
   306  	}
   307  
   308  	//get the number of retries
   309  	maxRetries := dbclient.couchInstance.conf.MaxRetries
   310  
   311  	resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, "GetDatabaseInfo", connectURL, nil, "", "", maxRetries, true, nil)
   312  	if err != nil {
   313  		return nil, couchDBReturn, err
   314  	}
   315  	defer closeResponseBody(resp)
   316  
   317  	dbResponse := &dbInfo{}
   318  	decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse)
   319  	if decodeErr != nil {
   320  		return nil, nil, errors.Wrap(decodeErr, "error decoding response body")
   321  	}
   322  
   323  	// trace the database info response
   324  	couchdbLogger.Debugw("GetDatabaseInfo()", "dbResponseJSON", dbResponse)
   325  
   326  	return dbResponse, couchDBReturn, nil
   327  
   328  }
   329  
   330  //verifyCouchConfig method provides function to verify the connection information
   331  func (couchInstance *couchInstance) verifyCouchConfig() (*connectionInfo, *dbReturn, error) {
   332  
   333  	couchdbLogger.Debugf("Entering VerifyCouchConfig()")
   334  	defer couchdbLogger.Debugf("Exiting VerifyCouchConfig()")
   335  
   336  	connectURL, err := url.Parse(couchInstance.url())
   337  	if err != nil {
   338  		couchdbLogger.Errorf("URL parse error: %s", err)
   339  		return nil, nil, errors.Wrapf(err, "error parsing couch instance URL: %s", couchInstance.url())
   340  	}
   341  	connectURL.Path = "/"
   342  
   343  	//get the number of retries for startup
   344  	maxRetriesOnStartup := couchInstance.conf.MaxRetriesOnStartup
   345  
   346  	resp, couchDBReturn, err := couchInstance.handleRequest(context.Background(), http.MethodGet, "", "VerifyCouchConfig", connectURL, nil,
   347  		"", "", maxRetriesOnStartup, true, nil)
   348  
   349  	if err != nil {
   350  		return nil, couchDBReturn, errors.WithMessage(err, "unable to connect to CouchDB, check the hostname and port")
   351  	}
   352  	defer closeResponseBody(resp)
   353  
   354  	dbResponse := &connectionInfo{}
   355  	decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse)
   356  	if decodeErr != nil {
   357  		return nil, nil, errors.Wrap(decodeErr, "error decoding response body")
   358  	}
   359  
   360  	// trace the database info response
   361  	couchdbLogger.Debugw("VerifyConnection()", "dbResponseJSON", dbResponse)
   362  
   363  	//check to see if the system databases exist
   364  	//Verifying the existence of the system database accomplishes two steps
   365  	//1.  Ensures the system databases are created
   366  	//2.  Verifies the username password provided in the CouchDB config are valid for system admin
   367  	err = createSystemDatabasesIfNotExist(couchInstance)
   368  	if err != nil {
   369  		couchdbLogger.Errorf("Unable to connect to CouchDB, error: %s. Check the admin username and password.", err)
   370  		return nil, nil, errors.WithMessage(err, "unable to connect to CouchDB. Check the admin username and password")
   371  	}
   372  
   373  	return dbResponse, couchDBReturn, nil
   374  }
   375  
   376  // isEmpty returns false if couchInstance contains any databases
   377  // (except couchdb system databases and any database name supplied in the parameter 'databasesToIgnore')
   378  func (couchInstance *couchInstance) isEmpty(databasesToIgnore []string) (bool, error) {
   379  	toIgnore := map[string]bool{}
   380  	for _, s := range databasesToIgnore {
   381  		toIgnore[s] = true
   382  	}
   383  	applicationDBNames, err := couchInstance.retrieveApplicationDBNames()
   384  	if err != nil {
   385  		return false, err
   386  	}
   387  	for _, dbName := range applicationDBNames {
   388  		if !toIgnore[dbName] {
   389  			return false, nil
   390  		}
   391  	}
   392  	return true, nil
   393  }
   394  
   395  // retrieveApplicationDBNames returns all the application database names in the couch instance
   396  func (couchInstance *couchInstance) retrieveApplicationDBNames() ([]string, error) {
   397  	connectURL, err := url.Parse(couchInstance.url())
   398  	if err != nil {
   399  		couchdbLogger.Errorf("URL parse error: %s", err)
   400  		return nil, errors.Wrapf(err, "error parsing couch instance URL: %s", couchInstance.url())
   401  	}
   402  	connectURL.Path = "/_all_dbs"
   403  	maxRetries := couchInstance.conf.MaxRetries
   404  	resp, _, err := couchInstance.handleRequest(
   405  		context.Background(),
   406  		http.MethodGet,
   407  		"",
   408  		"IsEmpty",
   409  		connectURL,
   410  		nil,
   411  		"",
   412  		"",
   413  		maxRetries,
   414  		true,
   415  		nil,
   416  	)
   417  
   418  	if err != nil {
   419  		return nil, errors.WithMessage(err, "unable to connect to CouchDB, check the hostname and port")
   420  	}
   421  
   422  	var dbNames []string
   423  	defer closeResponseBody(resp)
   424  	if err := json.NewDecoder(resp.Body).Decode(&dbNames); err != nil {
   425  		return nil, errors.Wrap(err, "error decoding response body")
   426  	}
   427  	couchdbLogger.Debugf("dbNames = %s", dbNames)
   428  	applicationsDBNames := []string{}
   429  	for _, d := range dbNames {
   430  		if !isCouchSystemDBName(d) {
   431  			applicationsDBNames = append(applicationsDBNames, d)
   432  		}
   433  	}
   434  	return applicationsDBNames, nil
   435  }
   436  
   437  func isCouchSystemDBName(name string) bool {
   438  	return strings.HasPrefix(name, "_")
   439  }
   440  
   441  // healthCheck checks if the peer is able to communicate with CouchDB
   442  func (couchInstance *couchInstance) healthCheck(ctx context.Context) error {
   443  	connectURL, err := url.Parse(couchInstance.url())
   444  	if err != nil {
   445  		couchdbLogger.Errorf("URL parse error: %s", err)
   446  		return errors.Wrapf(err, "error parsing CouchDB URL: %s", couchInstance.url())
   447  	}
   448  	_, _, err = couchInstance.handleRequest(ctx, http.MethodHead, "", "HealthCheck", connectURL, nil, "", "", 0, true, nil)
   449  	if err != nil {
   450  		return fmt.Errorf("failed to connect to couch db [%s]", err)
   451  	}
   452  	return nil
   453  }
   454  
   455  // internalQueryLimit returns the maximum number of records to return internally
   456  // when querying CouchDB.
   457  func (couchInstance *couchInstance) internalQueryLimit() int32 {
   458  	return int32(couchInstance.conf.InternalQueryLimit)
   459  }
   460  
   461  // maxBatchUpdateSize returns the maximum number of records to include in a
   462  // bulk update operation.
   463  func (couchInstance *couchInstance) maxBatchUpdateSize() int {
   464  	return couchInstance.conf.MaxBatchUpdateSize
   465  }
   466  
   467  // url returns the URL for the CouchDB instance.
   468  func (couchInstance *couchInstance) url() string {
   469  	URL := &url.URL{
   470  		Host:   couchInstance.conf.Address,
   471  		Scheme: "http",
   472  	}
   473  	return URL.String()
   474  }
   475  
   476  //dropDatabase provides method to drop an existing database
   477  func (dbclient *couchDatabase) dropDatabase() (*dbOperationResponse, error) {
   478  	dbName := dbclient.dbName
   479  
   480  	couchdbLogger.Debugf("[%s] Entering DropDatabase()", dbName)
   481  
   482  	connectURL, err := url.Parse(dbclient.couchInstance.url())
   483  	if err != nil {
   484  		couchdbLogger.Errorf("URL parse error: %s", err)
   485  		return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
   486  	}
   487  
   488  	//get the number of retries
   489  	maxRetries := dbclient.couchInstance.conf.MaxRetries
   490  
   491  	resp, _, err := dbclient.handleRequest(http.MethodDelete, "DropDatabase", connectURL, nil, "", "", maxRetries, true, nil)
   492  	if err != nil {
   493  		return nil, err
   494  	}
   495  	defer closeResponseBody(resp)
   496  
   497  	dbResponse := &dbOperationResponse{}
   498  	decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse)
   499  	if decodeErr != nil {
   500  		return nil, errors.Wrap(decodeErr, "error decoding response body")
   501  	}
   502  
   503  	if dbResponse.Ok {
   504  		couchdbLogger.Debugf("[%s] Dropped database", dbclient.dbName)
   505  	}
   506  
   507  	couchdbLogger.Debugf("[%s] Exiting DropDatabase()", dbclient.dbName)
   508  
   509  	if dbResponse.Ok {
   510  		return dbResponse, nil
   511  	}
   512  
   513  	return dbResponse, errors.New("error dropping database")
   514  }
   515  
   516  //saveDoc method provides a function to save a document, id and byte array
   517  func (dbclient *couchDatabase) saveDoc(id string, rev string, couchDoc *couchDoc) (string, error) {
   518  	dbName := dbclient.dbName
   519  
   520  	couchdbLogger.Debugf("[%s] Entering SaveDoc() id=[%s]", dbName, id)
   521  
   522  	if !utf8.ValidString(id) {
   523  		return "", errors.Errorf("doc id [%x] not a valid utf8 string", id)
   524  	}
   525  
   526  	saveURL, err := url.Parse(dbclient.couchInstance.url())
   527  	if err != nil {
   528  		couchdbLogger.Errorf("URL parse error: %s", err)
   529  		return "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
   530  	}
   531  
   532  	//Set up a buffer for the data to be pushed to couchdb
   533  	var data []byte
   534  
   535  	//Set up a default boundary for use by multipart if sending attachments
   536  	defaultBoundary := ""
   537  
   538  	//Create a flag for shared connections.  This is set to false for zero length attachments
   539  	keepConnectionOpen := true
   540  
   541  	//check to see if attachments is nil, if so, then this is a JSON only
   542  	if couchDoc.attachments == nil {
   543  
   544  		//Test to see if this is a valid JSON
   545  		if !isJSON(string(couchDoc.jsonValue)) {
   546  			return "", errors.New("JSON format is not valid")
   547  		}
   548  
   549  		// if there are no attachments, then use the bytes passed in as the JSON
   550  		data = couchDoc.jsonValue
   551  
   552  	} else { // there are attachments
   553  
   554  		//attachments are included, create the multipart definition
   555  		multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc)
   556  		if err3 != nil {
   557  			return "", err3
   558  		}
   559  
   560  		//If there is a zero length attachment, do not keep the connection open
   561  		for _, attach := range couchDoc.attachments {
   562  			if attach.Length < 1 {
   563  				keepConnectionOpen = false
   564  			}
   565  		}
   566  
   567  		//Set the data buffer to the data from the create multi-part data
   568  		data = multipartData.Bytes()
   569  
   570  		//Set the default boundary to the value generated in the multipart creation
   571  		defaultBoundary = multipartBoundary
   572  
   573  	}
   574  
   575  	//get the number of retries
   576  	maxRetries := dbclient.couchInstance.conf.MaxRetries
   577  
   578  	//handle the request for saving document with a retry if there is a revision conflict
   579  	resp, _, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodPut, dbName, "SaveDoc", saveURL, data, rev, defaultBoundary, maxRetries, keepConnectionOpen, nil)
   580  
   581  	if err != nil {
   582  		return "", err
   583  	}
   584  	defer closeResponseBody(resp)
   585  
   586  	//get the revision and return
   587  	revision, err := getRevisionHeader(resp)
   588  	if err != nil {
   589  		return "", err
   590  	}
   591  
   592  	couchdbLogger.Debugf("[%s] Exiting SaveDoc()", dbclient.dbName)
   593  
   594  	return revision, nil
   595  
   596  }
   597  
   598  //getDocumentRevision will return the revision if the document exists, otherwise it will return ""
   599  func (dbclient *couchDatabase) getDocumentRevision(id string) string {
   600  
   601  	var rev = ""
   602  
   603  	//See if the document already exists, we need the rev for saves and deletes
   604  	_, revdoc, err := dbclient.readDoc(id)
   605  	if err == nil {
   606  		//set the revision to the rev returned from the document read
   607  		rev = revdoc
   608  	}
   609  	return rev
   610  }
   611  
   612  func createAttachmentPart(couchDoc *couchDoc) (bytes.Buffer, string, error) {
   613  
   614  	//Create a buffer for writing the result
   615  	writeBuffer := new(bytes.Buffer)
   616  
   617  	// read the attachment and save as an attachment
   618  	writer := multipart.NewWriter(writeBuffer)
   619  
   620  	//retrieve the boundary for the multipart
   621  	defaultBoundary := writer.Boundary()
   622  
   623  	fileAttachments := map[string]fileDetails{}
   624  
   625  	for _, attachment := range couchDoc.attachments {
   626  		fileAttachments[attachment.Name] = fileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)}
   627  	}
   628  
   629  	attachmentJSONMap := map[string]interface{}{
   630  		"_attachments": fileAttachments,
   631  	}
   632  
   633  	//Add any data uploaded with the files
   634  	if couchDoc.jsonValue != nil {
   635  
   636  		//create a generic map
   637  		genericMap := make(map[string]interface{})
   638  
   639  		//unmarshal the data into the generic map
   640  		decoder := json.NewDecoder(bytes.NewBuffer(couchDoc.jsonValue))
   641  		decoder.UseNumber()
   642  		decodeErr := decoder.Decode(&genericMap)
   643  		if decodeErr != nil {
   644  			return *writeBuffer, "", errors.Wrap(decodeErr, "error decoding json data")
   645  		}
   646  
   647  		//add all key/values to the attachmentJSONMap
   648  		for jsonKey, jsonValue := range genericMap {
   649  			attachmentJSONMap[jsonKey] = jsonValue
   650  		}
   651  
   652  	}
   653  
   654  	filesForUpload, err := json.Marshal(attachmentJSONMap)
   655  	if err != nil {
   656  		return *writeBuffer, "", errors.Wrap(err, "error marshalling json data")
   657  	}
   658  
   659  	couchdbLogger.Debugf(string(filesForUpload))
   660  
   661  	//create the header for the JSON
   662  	header := make(textproto.MIMEHeader)
   663  	header.Set("Content-Type", "application/json")
   664  
   665  	part, err := writer.CreatePart(header)
   666  	if err != nil {
   667  		return *writeBuffer, defaultBoundary, errors.Wrap(err, "error creating multipart")
   668  	}
   669  
   670  	part.Write(filesForUpload)
   671  
   672  	for _, attachment := range couchDoc.attachments {
   673  
   674  		header := make(textproto.MIMEHeader)
   675  		part, err2 := writer.CreatePart(header)
   676  		if err2 != nil {
   677  			return *writeBuffer, defaultBoundary, errors.Wrap(err2, "error creating multipart")
   678  		}
   679  		part.Write(attachment.AttachmentBytes)
   680  
   681  	}
   682  
   683  	err = writer.Close()
   684  	if err != nil {
   685  		return *writeBuffer, defaultBoundary, errors.Wrap(err, "error closing multipart writer")
   686  	}
   687  
   688  	return *writeBuffer, defaultBoundary, nil
   689  
   690  }
   691  
   692  func getRevisionHeader(resp *http.Response) (string, error) {
   693  
   694  	if resp == nil {
   695  		return "", errors.New("no response received from CouchDB")
   696  	}
   697  
   698  	revision := resp.Header.Get("Etag")
   699  
   700  	if revision == "" {
   701  		return "", errors.New("no revision tag detected")
   702  	}
   703  
   704  	reg := regexp.MustCompile(`"([^"]*)"`)
   705  	revisionNoQuotes := reg.ReplaceAllString(revision, "${1}")
   706  	return revisionNoQuotes, nil
   707  
   708  }
   709  
   710  //readDoc method provides function to retrieve a document and its revision
   711  //from the database by id
   712  func (dbclient *couchDatabase) readDoc(id string) (*couchDoc, string, error) {
   713  	var couchDoc couchDoc
   714  	attachments := []*attachmentInfo{}
   715  	dbName := dbclient.dbName
   716  
   717  	couchdbLogger.Debugf("[%s] Entering ReadDoc()  id=[%s]", dbName, id)
   718  	if !utf8.ValidString(id) {
   719  		return nil, "", errors.Errorf("doc id [%x] not a valid utf8 string", id)
   720  	}
   721  
   722  	readURL, err := url.Parse(dbclient.couchInstance.url())
   723  	if err != nil {
   724  		couchdbLogger.Errorf("URL parse error: %s", err)
   725  		return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
   726  	}
   727  
   728  	query := readURL.Query()
   729  	query.Add("attachments", "true")
   730  
   731  	//get the number of retries
   732  	maxRetries := dbclient.couchInstance.conf.MaxRetries
   733  
   734  	resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, "ReadDoc", readURL, nil, "", "", maxRetries, true, &query, id)
   735  	if err != nil {
   736  		if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
   737  			couchdbLogger.Debugf("[%s] Document not found (404), returning nil value instead of 404 error", dbclient.dbName)
   738  			// non-existent document should return nil value instead of a 404 error
   739  			// for details see https://github.com/hyperledger-archives/fabric/issues/936
   740  			return nil, "", nil
   741  		}
   742  		couchdbLogger.Debugf("[%s] couchDBReturn=%v\n", dbclient.dbName, couchDBReturn)
   743  		return nil, "", err
   744  	}
   745  	defer closeResponseBody(resp)
   746  
   747  	//Get the media type from the Content-Type header
   748  	mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
   749  	if err != nil {
   750  		log.Fatal(err)
   751  	}
   752  
   753  	//Get the revision from header
   754  	revision, err := getRevisionHeader(resp)
   755  	if err != nil {
   756  		return nil, "", err
   757  	}
   758  
   759  	//check to see if the is multipart,  handle as attachment if multipart is detected
   760  	if strings.HasPrefix(mediaType, "multipart/") {
   761  		//Set up the multipart reader based on the boundary
   762  		multipartReader := multipart.NewReader(resp.Body, params["boundary"])
   763  		for {
   764  			p, err := multipartReader.NextPart()
   765  			if err == io.EOF {
   766  				break // processed all parts
   767  			}
   768  			if err != nil {
   769  				return nil, "", errors.Wrap(err, "error reading next multipart")
   770  			}
   771  
   772  			defer p.Close()
   773  
   774  			couchdbLogger.Debugf("[%s] part header=%s", dbclient.dbName, p.Header)
   775  			switch p.Header.Get("Content-Type") {
   776  			case "application/json":
   777  				partdata, err := ioutil.ReadAll(p)
   778  				if err != nil {
   779  					return nil, "", errors.Wrap(err, "error reading multipart data")
   780  				}
   781  				couchDoc.jsonValue = partdata
   782  			default:
   783  
   784  				//Create an attachment structure and load it
   785  				attachment := &attachmentInfo{}
   786  				attachment.ContentType = p.Header.Get("Content-Type")
   787  				contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";")
   788  				if strings.TrimSpace(contentDispositionParts[0]) == "attachment" {
   789  					switch p.Header.Get("Content-Encoding") {
   790  					case "gzip": //See if the part is gzip encoded
   791  
   792  						var respBody []byte
   793  
   794  						gr, err := gzip.NewReader(p)
   795  						if err != nil {
   796  							return nil, "", errors.Wrap(err, "error creating gzip reader")
   797  						}
   798  						respBody, err = ioutil.ReadAll(gr)
   799  						if err != nil {
   800  							return nil, "", errors.Wrap(err, "error reading gzip data")
   801  						}
   802  
   803  						couchdbLogger.Debugf("[%s] Retrieved attachment data", dbclient.dbName)
   804  						attachment.AttachmentBytes = respBody
   805  						attachment.Length = uint64(len(attachment.AttachmentBytes))
   806  						attachment.Name = p.FileName()
   807  						attachments = append(attachments, attachment)
   808  
   809  					default:
   810  
   811  						//retrieve the data,  this is not gzip
   812  						partdata, err := ioutil.ReadAll(p)
   813  						if err != nil {
   814  							return nil, "", errors.Wrap(err, "error reading multipart data")
   815  						}
   816  						couchdbLogger.Debugf("[%s] Retrieved attachment data", dbclient.dbName)
   817  						attachment.AttachmentBytes = partdata
   818  						attachment.Length = uint64(len(attachment.AttachmentBytes))
   819  						attachment.Name = p.FileName()
   820  						attachments = append(attachments, attachment)
   821  
   822  					} // end content-encoding switch
   823  				} // end if attachment
   824  			} // end content-type switch
   825  		} // for all multiparts
   826  
   827  		couchDoc.attachments = attachments
   828  
   829  		return &couchDoc, revision, nil
   830  	}
   831  
   832  	//handle as JSON document
   833  	couchDoc.jsonValue, err = ioutil.ReadAll(resp.Body)
   834  	if err != nil {
   835  		return nil, "", errors.Wrap(err, "error reading response body")
   836  	}
   837  
   838  	couchdbLogger.Debugf("[%s] Exiting ReadDoc()", dbclient.dbName)
   839  	return &couchDoc, revision, nil
   840  }
   841  
   842  //readDocRange method provides function to a range of documents based on the start and end keys
   843  //startKey and endKey can also be empty strings.  If startKey and endKey are empty, all documents are returned
   844  //This function provides a limit option to specify the max number of entries and is supplied by config.
   845  //Skip is reserved for possible future future use.
   846  func (dbclient *couchDatabase) readDocRange(startKey, endKey string, limit int32) ([]*queryResult, string, error) {
   847  	dbName := dbclient.dbName
   848  	couchdbLogger.Debugf("[%s] Entering ReadDocRange()  startKey=%s, endKey=%s", dbName, startKey, endKey)
   849  
   850  	var results []*queryResult
   851  
   852  	rangeURL, err := url.Parse(dbclient.couchInstance.url())
   853  	if err != nil {
   854  		couchdbLogger.Errorf("URL parse error: %s", err)
   855  		return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
   856  	}
   857  
   858  	queryParms := rangeURL.Query()
   859  	//Increment the limit by 1 to see if there are more qualifying records
   860  	queryParms.Set("limit", strconv.FormatInt(int64(limit+1), 10))
   861  	queryParms.Add("include_docs", "true")
   862  	queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb
   863  	queryParms.Add("attachments", "true")    // get the attachments as well
   864  
   865  	//Append the startKey if provided
   866  	if startKey != "" {
   867  		if startKey, err = encodeForJSON(startKey); err != nil {
   868  			return nil, "", err
   869  		}
   870  		queryParms.Add("startkey", "\""+startKey+"\"")
   871  	}
   872  
   873  	//Append the endKey if provided
   874  	if endKey != "" {
   875  		var err error
   876  		if endKey, err = encodeForJSON(endKey); err != nil {
   877  			return nil, "", err
   878  		}
   879  		queryParms.Add("endkey", "\""+endKey+"\"")
   880  	}
   881  
   882  	//get the number of retries
   883  	maxRetries := dbclient.couchInstance.conf.MaxRetries
   884  
   885  	resp, _, err := dbclient.handleRequest(http.MethodGet, "RangeDocRange", rangeURL, nil, "", "", maxRetries, true, &queryParms, "_all_docs")
   886  	if err != nil {
   887  		return nil, "", err
   888  	}
   889  	defer closeResponseBody(resp)
   890  
   891  	if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) {
   892  		dump, err2 := httputil.DumpResponse(resp, false)
   893  		if err2 != nil {
   894  			log.Fatal(err2)
   895  		}
   896  		// compact debug log by replacing carriage return / line feed with dashes to separate http headers
   897  		couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
   898  	}
   899  
   900  	//handle as JSON document
   901  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
   902  	if err != nil {
   903  		return nil, "", errors.Wrap(err, "error reading response body")
   904  	}
   905  
   906  	var jsonResponse = &rangeQueryResponse{}
   907  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
   908  	if err2 != nil {
   909  		return nil, "", errors.Wrap(err2, "error unmarshalling json data")
   910  	}
   911  
   912  	//if an additional record is found, then reduce the count by 1
   913  	//and populate the nextStartKey
   914  	if jsonResponse.TotalRows > limit {
   915  		jsonResponse.TotalRows = limit
   916  	}
   917  
   918  	couchdbLogger.Debugf("[%s] Total Rows: %d", dbclient.dbName, jsonResponse.TotalRows)
   919  
   920  	//Use the next endKey as the starting default for the nextStartKey
   921  	nextStartKey := endKey
   922  
   923  	for index, row := range jsonResponse.Rows {
   924  
   925  		var docMetadata = &docMetadata{}
   926  		err3 := json.Unmarshal(row.Doc, &docMetadata)
   927  		if err3 != nil {
   928  			return nil, "", errors.Wrap(err3, "error unmarshalling json data")
   929  		}
   930  
   931  		//if there is an extra row for the nextStartKey, then do not add the row to the result set
   932  		//and populate the nextStartKey variable
   933  		if int32(index) >= jsonResponse.TotalRows {
   934  			nextStartKey = docMetadata.ID
   935  			continue
   936  		}
   937  
   938  		if docMetadata.AttachmentsInfo != nil {
   939  
   940  			couchdbLogger.Debugf("[%s] Adding JSON document and attachments for id: %s", dbclient.dbName, docMetadata.ID)
   941  
   942  			attachments := []*attachmentInfo{}
   943  			for attachmentName, attachment := range docMetadata.AttachmentsInfo {
   944  				attachment.Name = attachmentName
   945  
   946  				attachments = append(attachments, attachment)
   947  			}
   948  
   949  			var addDocument = &queryResult{docMetadata.ID, row.Doc, attachments}
   950  			results = append(results, addDocument)
   951  
   952  		} else {
   953  
   954  			couchdbLogger.Debugf("[%s] Adding json docment for id: %s", dbclient.dbName, docMetadata.ID)
   955  
   956  			var addDocument = &queryResult{docMetadata.ID, row.Doc, nil}
   957  			results = append(results, addDocument)
   958  
   959  		}
   960  
   961  	}
   962  
   963  	couchdbLogger.Debugf("[%s] Exiting ReadDocRange()", dbclient.dbName)
   964  
   965  	return results, nextStartKey, nil
   966  
   967  }
   968  
   969  //deleteDoc method provides function to delete a document from the database by id
   970  func (dbclient *couchDatabase) deleteDoc(id, rev string) error {
   971  	dbName := dbclient.dbName
   972  
   973  	couchdbLogger.Debugf("[%s] Entering DeleteDoc()  id=%s", dbName, id)
   974  
   975  	deleteURL, err := url.Parse(dbclient.couchInstance.url())
   976  	if err != nil {
   977  		couchdbLogger.Errorf("URL parse error: %s", err)
   978  		return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
   979  	}
   980  
   981  	//get the number of retries
   982  	maxRetries := dbclient.couchInstance.conf.MaxRetries
   983  
   984  	//handle the request for saving document with a retry if there is a revision conflict
   985  	resp, couchDBReturn, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodDelete, dbName, "DeleteDoc",
   986  		deleteURL, nil, "", "", maxRetries, true, nil)
   987  
   988  	if err != nil {
   989  		if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
   990  			couchdbLogger.Debugf("[%s] Document not found (404), returning nil value instead of 404 error", dbclient.dbName)
   991  			// non-existent document should return nil value instead of a 404 error
   992  			// for details see https://github.com/hyperledger-archives/fabric/issues/936
   993  			return nil
   994  		}
   995  		return err
   996  	}
   997  	defer closeResponseBody(resp)
   998  
   999  	couchdbLogger.Debugf("[%s] Exiting DeleteDoc()", dbclient.dbName)
  1000  
  1001  	return nil
  1002  
  1003  }
  1004  
  1005  //queryDocuments method provides function for processing a query
  1006  func (dbclient *couchDatabase) queryDocuments(query string) ([]*queryResult, string, error) {
  1007  	dbName := dbclient.dbName
  1008  
  1009  	couchdbLogger.Debugf("[%s] Entering QueryDocuments()  query=%s", dbName, query)
  1010  
  1011  	var results []*queryResult
  1012  
  1013  	queryURL, err := url.Parse(dbclient.couchInstance.url())
  1014  	if err != nil {
  1015  		couchdbLogger.Errorf("URL parse error: %s", err)
  1016  		return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1017  	}
  1018  
  1019  	//get the number of retries
  1020  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1021  
  1022  	resp, _, err := dbclient.handleRequest(http.MethodPost, "QueryDocuments", queryURL, []byte(query), "", "", maxRetries, true, nil, "_find")
  1023  	if err != nil {
  1024  		return nil, "", err
  1025  	}
  1026  	defer closeResponseBody(resp)
  1027  
  1028  	if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) {
  1029  		dump, err2 := httputil.DumpResponse(resp, false)
  1030  		if err2 != nil {
  1031  			log.Fatal(err2)
  1032  		}
  1033  		// compact debug log by replacing carriage return / line feed with dashes to separate http headers
  1034  		couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
  1035  	}
  1036  
  1037  	//handle as JSON document
  1038  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
  1039  	if err != nil {
  1040  		return nil, "", errors.Wrap(err, "error reading response body")
  1041  	}
  1042  
  1043  	var jsonResponse = &queryResponse{}
  1044  
  1045  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
  1046  	if err2 != nil {
  1047  		return nil, "", errors.Wrap(err2, "error unmarshalling json data")
  1048  	}
  1049  
  1050  	if jsonResponse.Warning != "" {
  1051  		couchdbLogger.Warnf("The query [%s] caused the following warning: [%s]", query, jsonResponse.Warning)
  1052  	}
  1053  
  1054  	for _, row := range jsonResponse.Docs {
  1055  
  1056  		var docMetadata = &docMetadata{}
  1057  		err3 := json.Unmarshal(row, &docMetadata)
  1058  		if err3 != nil {
  1059  			return nil, "", errors.Wrap(err3, "error unmarshalling json data")
  1060  		}
  1061  
  1062  		// JSON Query results never have attachments
  1063  		// The If block below will never be executed
  1064  		if docMetadata.AttachmentsInfo != nil {
  1065  
  1066  			couchdbLogger.Debugf("[%s] Adding JSON docment and attachments for id: %s", dbclient.dbName, docMetadata.ID)
  1067  
  1068  			couchDoc, _, err := dbclient.readDoc(docMetadata.ID)
  1069  			if err != nil {
  1070  				return nil, "", err
  1071  			}
  1072  			var addDocument = &queryResult{id: docMetadata.ID, value: couchDoc.jsonValue, attachments: couchDoc.attachments}
  1073  			results = append(results, addDocument)
  1074  
  1075  		} else {
  1076  			couchdbLogger.Debugf("[%s] Adding json docment for id: %s", dbclient.dbName, docMetadata.ID)
  1077  			var addDocument = &queryResult{id: docMetadata.ID, value: row, attachments: nil}
  1078  
  1079  			results = append(results, addDocument)
  1080  
  1081  		}
  1082  	}
  1083  
  1084  	couchdbLogger.Debugf("[%s] Exiting QueryDocuments()", dbclient.dbName)
  1085  
  1086  	return results, jsonResponse.Bookmark, nil
  1087  
  1088  }
  1089  
  1090  // listIndex method lists the defined indexes for a database
  1091  func (dbclient *couchDatabase) listIndex() ([]*indexResult, error) {
  1092  
  1093  	//IndexDefinition contains the definition for a couchdb index
  1094  	type indexDefinition struct {
  1095  		DesignDocument string          `json:"ddoc"`
  1096  		Name           string          `json:"name"`
  1097  		Type           string          `json:"type"`
  1098  		Definition     json.RawMessage `json:"def"`
  1099  	}
  1100  
  1101  	//ListIndexResponse contains the definition for listing couchdb indexes
  1102  	type listIndexResponse struct {
  1103  		TotalRows int               `json:"total_rows"`
  1104  		Indexes   []indexDefinition `json:"indexes"`
  1105  	}
  1106  
  1107  	dbName := dbclient.dbName
  1108  	couchdbLogger.Debugf("[%s] Entering ListIndex()", dbName)
  1109  
  1110  	indexURL, err := url.Parse(dbclient.couchInstance.url())
  1111  	if err != nil {
  1112  		couchdbLogger.Errorf("URL parse error: %s", err)
  1113  		return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1114  	}
  1115  
  1116  	//get the number of retries
  1117  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1118  
  1119  	resp, _, err := dbclient.handleRequest(http.MethodGet, "ListIndex", indexURL, nil, "", "", maxRetries, true, nil, "_index")
  1120  	if err != nil {
  1121  		return nil, err
  1122  	}
  1123  	defer closeResponseBody(resp)
  1124  
  1125  	//handle as JSON document
  1126  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
  1127  	if err != nil {
  1128  		return nil, errors.Wrap(err, "error reading response body")
  1129  	}
  1130  
  1131  	var jsonResponse = &listIndexResponse{}
  1132  
  1133  	err2 := json.Unmarshal(jsonResponseRaw, jsonResponse)
  1134  	if err2 != nil {
  1135  		return nil, errors.Wrap(err2, "error unmarshalling json data")
  1136  	}
  1137  
  1138  	var results []*indexResult
  1139  
  1140  	for _, row := range jsonResponse.Indexes {
  1141  
  1142  		//if the DesignDocument does not begin with "_design/", then this is a system
  1143  		//level index and is not meaningful and cannot be edited or deleted
  1144  		designDoc := row.DesignDocument
  1145  		s := strings.SplitAfterN(designDoc, "_design/", 2)
  1146  		if len(s) > 1 {
  1147  			designDoc = s[1]
  1148  
  1149  			//Add the index definition to the results
  1150  			var addIndexResult = &indexResult{DesignDocument: designDoc, Name: row.Name, Definition: fmt.Sprintf("%s", row.Definition)}
  1151  			results = append(results, addIndexResult)
  1152  		}
  1153  
  1154  	}
  1155  
  1156  	couchdbLogger.Debugf("[%s] Exiting ListIndex()", dbclient.dbName)
  1157  
  1158  	return results, nil
  1159  
  1160  }
  1161  
  1162  // createIndex method provides a function creating an index
  1163  func (dbclient *couchDatabase) createIndex(indexdefinition string) (*createIndexResponse, error) {
  1164  	dbName := dbclient.dbName
  1165  
  1166  	couchdbLogger.Debugf("[%s] Entering CreateIndex()  indexdefinition=%s", dbName, indexdefinition)
  1167  
  1168  	//Test to see if this is a valid JSON
  1169  	if !isJSON(indexdefinition) {
  1170  		return nil, errors.New("JSON format is not valid")
  1171  	}
  1172  
  1173  	indexURL, err := url.Parse(dbclient.couchInstance.url())
  1174  	if err != nil {
  1175  		couchdbLogger.Errorf("URL parse error: %s", err)
  1176  		return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1177  	}
  1178  
  1179  	//get the number of retries
  1180  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1181  
  1182  	resp, _, err := dbclient.handleRequest(http.MethodPost, "CreateIndex", indexURL, []byte(indexdefinition), "", "", maxRetries, true, nil, "_index")
  1183  	if err != nil {
  1184  		return nil, err
  1185  	}
  1186  	defer closeResponseBody(resp)
  1187  
  1188  	if resp == nil {
  1189  		return nil, errors.New("invalid response received from CouchDB")
  1190  	}
  1191  
  1192  	//Read the response body
  1193  	respBody, err := ioutil.ReadAll(resp.Body)
  1194  	if err != nil {
  1195  		return nil, errors.Wrap(err, "error reading response body")
  1196  	}
  1197  
  1198  	couchDBReturn := &createIndexResponse{}
  1199  
  1200  	jsonBytes := []byte(respBody)
  1201  
  1202  	//unmarshal the response
  1203  	err = json.Unmarshal(jsonBytes, &couchDBReturn)
  1204  	if err != nil {
  1205  		return nil, errors.Wrap(err, "error unmarshalling json data")
  1206  	}
  1207  
  1208  	if couchDBReturn.Result == "created" {
  1209  
  1210  		couchdbLogger.Infof("Created CouchDB index [%s] in state database [%s] using design document [%s]", couchDBReturn.Name, dbclient.dbName, couchDBReturn.ID)
  1211  
  1212  		return couchDBReturn, nil
  1213  
  1214  	}
  1215  
  1216  	couchdbLogger.Infof("Updated CouchDB index [%s] in state database [%s] using design document [%s]", couchDBReturn.Name, dbclient.dbName, couchDBReturn.ID)
  1217  
  1218  	return couchDBReturn, nil
  1219  }
  1220  
  1221  // deleteIndex method provides a function deleting an index
  1222  func (dbclient *couchDatabase) deleteIndex(designdoc, indexname string) error {
  1223  	dbName := dbclient.dbName
  1224  
  1225  	couchdbLogger.Debugf("[%s] Entering DeleteIndex()  designdoc=%s  indexname=%s", dbName, designdoc, indexname)
  1226  
  1227  	indexURL, err := url.Parse(dbclient.couchInstance.url())
  1228  	if err != nil {
  1229  		couchdbLogger.Errorf("URL parse error: %s", err)
  1230  		return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1231  	}
  1232  
  1233  	//get the number of retries
  1234  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1235  
  1236  	resp, _, err := dbclient.handleRequest(http.MethodDelete, "DeleteIndex", indexURL, nil, "", "", maxRetries, true, nil, "_index", designdoc, "json", indexname)
  1237  	if err != nil {
  1238  		return err
  1239  	}
  1240  	defer closeResponseBody(resp)
  1241  
  1242  	return nil
  1243  
  1244  }
  1245  
  1246  //warmIndex method provides a function for warming a single index
  1247  func (dbclient *couchDatabase) warmIndex(designdoc, indexname string) error {
  1248  	dbName := dbclient.dbName
  1249  
  1250  	couchdbLogger.Debugf("[%s] Entering WarmIndex()  designdoc=%s  indexname=%s", dbName, designdoc, indexname)
  1251  
  1252  	indexURL, err := url.Parse(dbclient.couchInstance.url())
  1253  	if err != nil {
  1254  		couchdbLogger.Errorf("URL parse error: %s", err)
  1255  		return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1256  	}
  1257  
  1258  	queryParms := indexURL.Query()
  1259  	//Query parameter that allows the execution of the URL to return immediately
  1260  	//The update_after will cause the index update to run after the URL returns
  1261  	queryParms.Add("stale", "update_after")
  1262  
  1263  	//get the number of retries
  1264  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1265  
  1266  	resp, _, err := dbclient.handleRequest(http.MethodGet, "WarmIndex", indexURL, nil, "", "", maxRetries, true, &queryParms, "_design", designdoc, "_view", indexname)
  1267  	if err != nil {
  1268  		return err
  1269  	}
  1270  	defer closeResponseBody(resp)
  1271  
  1272  	return nil
  1273  
  1274  }
  1275  
  1276  //runWarmIndexAllIndexes is a wrapper for WarmIndexAllIndexes to catch and report any errors
  1277  func (dbclient *couchDatabase) runWarmIndexAllIndexes() {
  1278  
  1279  	err := dbclient.warmIndexAllIndexes()
  1280  	if err != nil {
  1281  		couchdbLogger.Errorf("Error detected during WarmIndexAllIndexes(): %+v", err)
  1282  	}
  1283  
  1284  }
  1285  
  1286  //warmIndexAllIndexes method provides a function for warming all indexes for a database
  1287  func (dbclient *couchDatabase) warmIndexAllIndexes() error {
  1288  
  1289  	couchdbLogger.Debugf("[%s] Entering WarmIndexAllIndexes()", dbclient.dbName)
  1290  
  1291  	//Retrieve all indexes
  1292  	listResult, err := dbclient.listIndex()
  1293  	if err != nil {
  1294  		return err
  1295  	}
  1296  
  1297  	//For each index definition, execute an index refresh
  1298  	for _, elem := range listResult {
  1299  
  1300  		err := dbclient.warmIndex(elem.DesignDocument, elem.Name)
  1301  		if err != nil {
  1302  			return err
  1303  		}
  1304  
  1305  	}
  1306  
  1307  	couchdbLogger.Debugf("[%s] Exiting WarmIndexAllIndexes()", dbclient.dbName)
  1308  
  1309  	return nil
  1310  
  1311  }
  1312  
  1313  //getDatabaseSecurity method provides function to retrieve the security config for a database
  1314  func (dbclient *couchDatabase) getDatabaseSecurity() (*databaseSecurity, error) {
  1315  	dbName := dbclient.dbName
  1316  
  1317  	couchdbLogger.Debugf("[%s] Entering GetDatabaseSecurity()", dbName)
  1318  
  1319  	securityURL, err := url.Parse(dbclient.couchInstance.url())
  1320  	if err != nil {
  1321  		couchdbLogger.Errorf("URL parse error: %s", err)
  1322  		return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1323  	}
  1324  
  1325  	//get the number of retries
  1326  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1327  
  1328  	resp, _, err := dbclient.handleRequest(http.MethodGet, "GetDatabaseSecurity", securityURL, nil, "", "", maxRetries, true, nil, "_security")
  1329  
  1330  	if err != nil {
  1331  		return nil, err
  1332  	}
  1333  	defer closeResponseBody(resp)
  1334  
  1335  	//handle as JSON document
  1336  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
  1337  	if err != nil {
  1338  		return nil, errors.Wrap(err, "error reading response body")
  1339  	}
  1340  
  1341  	var jsonResponse = &databaseSecurity{}
  1342  
  1343  	err2 := json.Unmarshal(jsonResponseRaw, jsonResponse)
  1344  	if err2 != nil {
  1345  		return nil, errors.Wrap(err2, "error unmarshalling json data")
  1346  	}
  1347  
  1348  	couchdbLogger.Debugf("[%s] Exiting GetDatabaseSecurity()", dbclient.dbName)
  1349  
  1350  	return jsonResponse, nil
  1351  
  1352  }
  1353  
  1354  //applyDatabaseSecurity method provides function to update the security config for a database
  1355  func (dbclient *couchDatabase) applyDatabaseSecurity(databaseSecurity *databaseSecurity) error {
  1356  	dbName := dbclient.dbName
  1357  
  1358  	couchdbLogger.Debugf("[%s] Entering ApplyDatabaseSecurity()", dbName)
  1359  
  1360  	securityURL, err := url.Parse(dbclient.couchInstance.url())
  1361  	if err != nil {
  1362  		couchdbLogger.Errorf("URL parse error: %s", err)
  1363  		return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1364  	}
  1365  
  1366  	//Ensure all of the arrays are initialized to empty arrays instead of nil
  1367  	if databaseSecurity.Admins.Names == nil {
  1368  		databaseSecurity.Admins.Names = make([]string, 0)
  1369  	}
  1370  	if databaseSecurity.Admins.Roles == nil {
  1371  		databaseSecurity.Admins.Roles = make([]string, 0)
  1372  	}
  1373  	if databaseSecurity.Members.Names == nil {
  1374  		databaseSecurity.Members.Names = make([]string, 0)
  1375  	}
  1376  	if databaseSecurity.Members.Roles == nil {
  1377  		databaseSecurity.Members.Roles = make([]string, 0)
  1378  	}
  1379  
  1380  	//get the number of retries
  1381  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1382  
  1383  	databaseSecurityJSON, err := json.Marshal(databaseSecurity)
  1384  	if err != nil {
  1385  		return errors.Wrap(err, "error unmarshalling json data")
  1386  	}
  1387  
  1388  	couchdbLogger.Debugf("[%s] Applying security to database: %s", dbclient.dbName, string(databaseSecurityJSON))
  1389  
  1390  	resp, _, err := dbclient.handleRequest(http.MethodPut, "ApplyDatabaseSecurity", securityURL, databaseSecurityJSON, "", "", maxRetries, true, nil, "_security")
  1391  
  1392  	if err != nil {
  1393  		return err
  1394  	}
  1395  	defer closeResponseBody(resp)
  1396  
  1397  	couchdbLogger.Debugf("[%s] Exiting ApplyDatabaseSecurity()", dbclient.dbName)
  1398  
  1399  	return nil
  1400  
  1401  }
  1402  
  1403  //batchRetrieveDocumentMetadata - batch method to retrieve document metadata for  a set of keys,
  1404  //including ID, couchdb revision number, and ledger version
  1405  func (dbclient *couchDatabase) batchRetrieveDocumentMetadata(keys []string) ([]*docMetadata, error) {
  1406  
  1407  	couchdbLogger.Debugf("[%s] Entering BatchRetrieveDocumentMetadata()  keys=%s", dbclient.dbName, keys)
  1408  
  1409  	batchRetrieveURL, err := url.Parse(dbclient.couchInstance.url())
  1410  	if err != nil {
  1411  		couchdbLogger.Errorf("URL parse error: %s", err)
  1412  		return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1413  	}
  1414  
  1415  	queryParms := batchRetrieveURL.Query()
  1416  
  1417  	// While BatchRetrieveDocumentMetadata() does not return the entire document,
  1418  	// for reads/writes, we do need to get document so that we can get the ledger version of the key.
  1419  	// TODO For blind writes we do not need to get the version, therefore when we bulk get
  1420  	// the revision numbers for the write keys that were not represented in read set
  1421  	// (the second time BatchRetrieveDocumentMetadata is called during block processing),
  1422  	// we could set include_docs to false to optimize the response.
  1423  	queryParms.Add("include_docs", "true")
  1424  
  1425  	keymap := make(map[string]interface{})
  1426  
  1427  	keymap["keys"] = keys
  1428  
  1429  	jsonKeys, err := json.Marshal(keymap)
  1430  	if err != nil {
  1431  		return nil, errors.Wrap(err, "error marshalling json data")
  1432  	}
  1433  
  1434  	//get the number of retries
  1435  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1436  
  1437  	resp, _, err := dbclient.handleRequest(http.MethodPost, "BatchRetrieveDocumentMetadata", batchRetrieveURL, jsonKeys, "", "", maxRetries, true, &queryParms, "_all_docs")
  1438  	if err != nil {
  1439  		return nil, err
  1440  	}
  1441  	defer closeResponseBody(resp)
  1442  
  1443  	if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) {
  1444  		dump, _ := httputil.DumpResponse(resp, false)
  1445  		// compact debug log by replacing carriage return / line feed with dashes to separate http headers
  1446  		couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
  1447  	}
  1448  
  1449  	//handle as JSON document
  1450  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
  1451  	if err != nil {
  1452  		return nil, errors.Wrap(err, "error reading response body")
  1453  	}
  1454  
  1455  	var jsonResponse = &batchRetrieveDocMetadataResponse{}
  1456  
  1457  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
  1458  	if err2 != nil {
  1459  		return nil, errors.Wrap(err2, "error unmarshalling json data")
  1460  	}
  1461  
  1462  	docMetadataArray := []*docMetadata{}
  1463  
  1464  	for _, row := range jsonResponse.Rows {
  1465  		docMetadata := &docMetadata{ID: row.ID, Rev: row.DocMetadata.Rev, Version: row.DocMetadata.Version}
  1466  		docMetadataArray = append(docMetadataArray, docMetadata)
  1467  	}
  1468  
  1469  	couchdbLogger.Debugf("[%s] Exiting BatchRetrieveDocumentMetadata()", dbclient.dbName)
  1470  
  1471  	return docMetadataArray, nil
  1472  
  1473  }
  1474  
  1475  //batchUpdateDocuments - batch method to batch update documents
  1476  func (dbclient *couchDatabase) batchUpdateDocuments(documents []*couchDoc) ([]*batchUpdateResponse, error) {
  1477  	dbName := dbclient.dbName
  1478  
  1479  	if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) {
  1480  		documentIdsString, err := printDocumentIds(documents)
  1481  		if err == nil {
  1482  			couchdbLogger.Debugf("[%s] Entering BatchUpdateDocuments()  document ids=[%s]", dbName, documentIdsString)
  1483  		} else {
  1484  			couchdbLogger.Debugf("[%s] Entering BatchUpdateDocuments()  Could not print document ids due to error: %+v", dbName, err)
  1485  		}
  1486  	}
  1487  
  1488  	batchUpdateURL, err := url.Parse(dbclient.couchInstance.url())
  1489  	if err != nil {
  1490  		couchdbLogger.Errorf("URL parse error: %s", err)
  1491  		return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url())
  1492  	}
  1493  
  1494  	documentMap := make(map[string]interface{})
  1495  
  1496  	var jsonDocumentMap []interface{}
  1497  
  1498  	for _, jsonDocument := range documents {
  1499  
  1500  		//create a document map
  1501  		var document = make(map[string]interface{})
  1502  
  1503  		//unmarshal the JSON component of the couchDoc into the document
  1504  		err = json.Unmarshal(jsonDocument.jsonValue, &document)
  1505  		if err != nil {
  1506  			return nil, errors.Wrap(err, "error unmarshalling json data")
  1507  		}
  1508  
  1509  		//iterate through any attachments
  1510  		if len(jsonDocument.attachments) > 0 {
  1511  
  1512  			//create a file attachment map
  1513  			fileAttachment := make(map[string]interface{})
  1514  
  1515  			//for each attachment, create a base64Attachment, name the attachment,
  1516  			//add the content type and base64 encode the attachment
  1517  			for _, attachment := range jsonDocument.attachments {
  1518  				fileAttachment[attachment.Name] = base64Attachment{attachment.ContentType,
  1519  					base64.StdEncoding.EncodeToString(attachment.AttachmentBytes)}
  1520  			}
  1521  
  1522  			//add attachments to the document
  1523  			document["_attachments"] = fileAttachment
  1524  
  1525  		}
  1526  
  1527  		//Append the document to the map of documents
  1528  		jsonDocumentMap = append(jsonDocumentMap, document)
  1529  
  1530  	}
  1531  
  1532  	//Add the documents to the "docs" item
  1533  	documentMap["docs"] = jsonDocumentMap
  1534  
  1535  	bulkDocsJSON, err := json.Marshal(documentMap)
  1536  	if err != nil {
  1537  		return nil, errors.Wrap(err, "error marshalling json data")
  1538  	}
  1539  
  1540  	//get the number of retries
  1541  	maxRetries := dbclient.couchInstance.conf.MaxRetries
  1542  
  1543  	resp, _, err := dbclient.handleRequest(http.MethodPost, "BatchUpdateDocuments", batchUpdateURL, bulkDocsJSON, "", "", maxRetries, true, nil, "_bulk_docs")
  1544  	if err != nil {
  1545  		return nil, err
  1546  	}
  1547  	defer closeResponseBody(resp)
  1548  
  1549  	if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) {
  1550  		dump, _ := httputil.DumpResponse(resp, false)
  1551  		// compact debug log by replacing carriage return / line feed with dashes to separate http headers
  1552  		couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
  1553  	}
  1554  
  1555  	//handle as JSON document
  1556  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
  1557  	if err != nil {
  1558  		return nil, errors.Wrap(err, "error reading response body")
  1559  	}
  1560  
  1561  	var jsonResponse = []*batchUpdateResponse{}
  1562  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
  1563  	if err2 != nil {
  1564  		return nil, errors.Wrap(err2, "error unmarshalling json data")
  1565  	}
  1566  
  1567  	couchdbLogger.Debugf("[%s] Exiting BatchUpdateDocuments() _bulk_docs response=[%s]", dbclient.dbName, string(jsonResponseRaw))
  1568  
  1569  	return jsonResponse, nil
  1570  
  1571  }
  1572  
  1573  //handleRequestWithRevisionRetry method is a generic http request handler with
  1574  //a retry for document revision conflict errors,
  1575  //which may be detected during saves or deletes that timed out from client http perspective,
  1576  //but which eventually succeeded in couchdb
  1577  func (dbclient *couchDatabase) handleRequestWithRevisionRetry(id, method, dbName, functionName string, connectURL *url.URL, data []byte, rev string,
  1578  	multipartBoundary string, maxRetries int, keepConnectionOpen bool, queryParms *url.Values) (*http.Response, *dbReturn, error) {
  1579  
  1580  	//Initialize a flag for the revision conflict
  1581  	revisionConflictDetected := false
  1582  	var resp *http.Response
  1583  	var couchDBReturn *dbReturn
  1584  	var errResp error
  1585  
  1586  	//attempt the http request for the max number of retries
  1587  	//In this case, the retry is to catch problems where a client timeout may miss a
  1588  	//successful CouchDB update and cause a document revision conflict on a retry in handleRequest
  1589  	for attempts := 0; attempts <= maxRetries; attempts++ {
  1590  
  1591  		//if the revision was not passed in, or if a revision conflict is detected on prior attempt,
  1592  		//query CouchDB for the document revision
  1593  		if rev == "" || revisionConflictDetected {
  1594  			rev = dbclient.getDocumentRevision(id)
  1595  		}
  1596  
  1597  		//handle the request for saving/deleting the couchdb data
  1598  		resp, couchDBReturn, errResp = dbclient.couchInstance.handleRequest(context.Background(), method, dbName, functionName, connectURL,
  1599  			data, rev, multipartBoundary, maxRetries, keepConnectionOpen, queryParms, id)
  1600  
  1601  		//If there was a 409 conflict error during the save/delete, log it and retry it.
  1602  		//Otherwise, break out of the retry loop
  1603  		if couchDBReturn != nil && couchDBReturn.StatusCode == 409 {
  1604  			couchdbLogger.Warningf("CouchDB document revision conflict detected, retrying. Attempt:%v", attempts+1)
  1605  			revisionConflictDetected = true
  1606  		} else {
  1607  			break
  1608  		}
  1609  	}
  1610  
  1611  	// return the handleRequest results
  1612  	return resp, couchDBReturn, errResp
  1613  }
  1614  
  1615  func (dbclient *couchDatabase) handleRequest(method, functionName string, connectURL *url.URL, data []byte, rev, multipartBoundary string,
  1616  	maxRetries int, keepConnectionOpen bool, queryParms *url.Values, pathElements ...string) (*http.Response, *dbReturn, error) {
  1617  
  1618  	return dbclient.couchInstance.handleRequest(context.Background(),
  1619  		method, dbclient.dbName, functionName, connectURL, data, rev, multipartBoundary,
  1620  		maxRetries, keepConnectionOpen, queryParms, pathElements...,
  1621  	)
  1622  }
  1623  
  1624  //handleRequest method is a generic http request handler.
  1625  // If it returns an error, it ensures that the response body is closed, else it is the
  1626  // callee's responsibility to close response correctly.
  1627  // Any http error or CouchDB error (4XX or 500) will result in a golang error getting returned
  1628  func (couchInstance *couchInstance) handleRequest(ctx context.Context, method, dbName, functionName string, connectURL *url.URL, data []byte, rev string,
  1629  	multipartBoundary string, maxRetries int, keepConnectionOpen bool, queryParms *url.Values, pathElements ...string) (*http.Response, *dbReturn, error) {
  1630  
  1631  	couchdbLogger.Debugf("Entering handleRequest()  method=%s  url=%v  dbName=%s", method, connectURL, dbName)
  1632  
  1633  	//create the return objects for couchDB
  1634  	var resp *http.Response
  1635  	var errResp error
  1636  	couchDBReturn := &dbReturn{}
  1637  	defer couchInstance.recordMetric(time.Now(), dbName, functionName, couchDBReturn)
  1638  
  1639  	//set initial wait duration for retries
  1640  	waitDuration := retryWaitTime * time.Millisecond
  1641  
  1642  	if maxRetries < 0 {
  1643  		return nil, nil, errors.New("number of retries must be zero or greater")
  1644  	}
  1645  
  1646  	requestURL := constructCouchDBUrl(connectURL, dbName, pathElements...)
  1647  
  1648  	if queryParms != nil {
  1649  		requestURL.RawQuery = queryParms.Encode()
  1650  	}
  1651  
  1652  	couchdbLogger.Debugf("Request URL: %s", requestURL)
  1653  
  1654  	//attempt the http request for the max number of retries
  1655  	// if maxRetries is 0, the database creation will be attempted once and will
  1656  	//    return an error if unsuccessful
  1657  	// if maxRetries is 3 (default), a maximum of 4 attempts (one attempt with 3 retries)
  1658  	//    will be made with warning entries for unsuccessful attempts
  1659  	for attempts := 0; attempts <= maxRetries; attempts++ {
  1660  
  1661  		//Set up a buffer for the payload data
  1662  		payloadData := new(bytes.Buffer)
  1663  
  1664  		payloadData.ReadFrom(bytes.NewReader(data))
  1665  
  1666  		//Create request based on URL for couchdb operation
  1667  		req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), payloadData)
  1668  		if err != nil {
  1669  			return nil, nil, errors.Wrap(err, "error creating http request")
  1670  		}
  1671  
  1672  		//set the request to close on completion if shared connections are not allowSharedConnection
  1673  		//Current CouchDB has a problem with zero length attachments, do not allow the connection to be reused.
  1674  		//Apache JIRA item for CouchDB   https://issues.apache.org/jira/browse/COUCHDB-3394
  1675  		if !keepConnectionOpen {
  1676  			req.Close = true
  1677  		}
  1678  
  1679  		//add content header for PUT
  1680  		if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete {
  1681  
  1682  			//If the multipartBoundary is not set, then this is a JSON and content-type should be set
  1683  			//to application/json.   Else, this is contains an attachment and needs to be multipart
  1684  			if multipartBoundary == "" {
  1685  				req.Header.Set("Content-Type", "application/json")
  1686  			} else {
  1687  				req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"")
  1688  			}
  1689  
  1690  			//check to see if the revision is set,  if so, pass as a header
  1691  			if rev != "" {
  1692  				req.Header.Set("If-Match", rev)
  1693  			}
  1694  		}
  1695  
  1696  		//add content header for PUT
  1697  		if method == http.MethodPut || method == http.MethodPost {
  1698  			req.Header.Set("Accept", "application/json")
  1699  		}
  1700  
  1701  		//add content header for GET
  1702  		if method == http.MethodGet {
  1703  			req.Header.Set("Accept", "multipart/related")
  1704  		}
  1705  
  1706  		//If username and password are set the use basic auth
  1707  		if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" {
  1708  			//req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW5w")
  1709  			req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password)
  1710  		}
  1711  
  1712  		//Execute http request
  1713  		resp, errResp = couchInstance.client.Do(req)
  1714  
  1715  		//check to see if the return from CouchDB is valid
  1716  		if invalidCouchDBReturn(resp, errResp) {
  1717  			continue
  1718  		}
  1719  
  1720  		//if there is no golang http error and no CouchDB 500 error, then drop out of the retry
  1721  		if errResp == nil && resp != nil && resp.StatusCode < 500 {
  1722  			// if this is an error, then populate the couchDBReturn
  1723  			if resp.StatusCode >= 400 {
  1724  				//Read the response body and close it for next attempt
  1725  				jsonError, err := ioutil.ReadAll(resp.Body)
  1726  				if err != nil {
  1727  					return nil, nil, errors.Wrap(err, "error reading response body")
  1728  				}
  1729  				defer closeResponseBody(resp)
  1730  
  1731  				errorBytes := []byte(jsonError)
  1732  				//Unmarshal the response
  1733  				err = json.Unmarshal(errorBytes, &couchDBReturn)
  1734  				if err != nil {
  1735  					return nil, nil, errors.Wrap(err, "error unmarshalling json data")
  1736  				}
  1737  			}
  1738  
  1739  			break
  1740  		}
  1741  
  1742  		// If the maxRetries is greater than 0, then log the retry info
  1743  		if maxRetries > 0 {
  1744  
  1745  			retryMessage := fmt.Sprintf("Retrying couchdb request in %s", waitDuration)
  1746  			if attempts == maxRetries {
  1747  				retryMessage = "Retries exhausted"
  1748  			}
  1749  
  1750  			//if this is an unexpected golang http error, log the error and retry
  1751  			if errResp != nil {
  1752  
  1753  				//Log the error with the retry count and continue
  1754  				couchdbLogger.Warningf("Attempt %d of %d returned error: %s. %s", attempts+1, maxRetries+1, errResp.Error(), retryMessage)
  1755  
  1756  				//otherwise this is an unexpected 500 error from CouchDB. Log the error and retry.
  1757  			} else {
  1758  				//Read the response body and close it for next attempt
  1759  				jsonError, err := ioutil.ReadAll(resp.Body)
  1760  				defer closeResponseBody(resp)
  1761  				if err != nil {
  1762  					return nil, nil, errors.Wrap(err, "error reading response body")
  1763  				}
  1764  
  1765  				errorBytes := []byte(jsonError)
  1766  				//Unmarshal the response
  1767  				err = json.Unmarshal(errorBytes, &couchDBReturn)
  1768  				if err != nil {
  1769  					return nil, nil, errors.Wrap(err, "error unmarshalling json data")
  1770  				}
  1771  
  1772  				//Log the 500 error with the retry count and continue
  1773  				couchdbLogger.Warningf("Attempt %d of %d returned Couch DB Error:%s,  Status Code:%v  Reason:%s. %s",
  1774  					attempts+1, maxRetries+1, couchDBReturn.Error, resp.Status, couchDBReturn.Reason, retryMessage)
  1775  
  1776  			}
  1777  			//if there are more retries remaining, sleep for specified sleep time, then retry
  1778  			if attempts < maxRetries {
  1779  				time.Sleep(waitDuration)
  1780  			}
  1781  
  1782  			//backoff, doubling the retry time for next attempt
  1783  			waitDuration *= 2
  1784  
  1785  		}
  1786  
  1787  	} // end retry loop
  1788  
  1789  	//if a golang http error is still present after retries are exhausted, return the error
  1790  	if errResp != nil {
  1791  		return nil, couchDBReturn, errors.Wrap(errResp, "http error calling couchdb")
  1792  	}
  1793  
  1794  	//This situation should not occur according to the golang spec.
  1795  	//if this error returned (errResp) from an http call, then the resp should be not nil,
  1796  	//this is a structure and StatusCode is an int
  1797  	//This is meant to provide a more graceful error if this should occur
  1798  	if invalidCouchDBReturn(resp, errResp) {
  1799  		return nil, nil, errors.New("unable to connect to CouchDB, check the hostname and port")
  1800  	}
  1801  
  1802  	//set the return code for the couchDB request
  1803  	couchDBReturn.StatusCode = resp.StatusCode
  1804  
  1805  	// check to see if the status code from couchdb is 400 or higher
  1806  	// response codes 4XX and 500 will be treated as errors -
  1807  	// golang error will be created from the couchDBReturn contents and both will be returned
  1808  	if resp.StatusCode >= 400 {
  1809  
  1810  		// if the status code is 400 or greater, log and return an error
  1811  		couchdbLogger.Debugf("Error handling CouchDB request. Error:%s,  Status Code:%v,  Reason:%s",
  1812  			couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason)
  1813  
  1814  		return nil, couchDBReturn, errors.Errorf("error handling CouchDB request. Error:%s,  Status Code:%v,  Reason:%s",
  1815  			couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason)
  1816  
  1817  	}
  1818  
  1819  	couchdbLogger.Debugf("Exiting handleRequest()")
  1820  
  1821  	//If no errors, then return the http response and the couchdb return object
  1822  	return resp, couchDBReturn, nil
  1823  }
  1824  
  1825  func (couchInstance *couchInstance) recordMetric(startTime time.Time, dbName, api string, couchDBReturn *dbReturn) {
  1826  	couchInstance.stats.observeProcessingTime(startTime, dbName, api, strconv.Itoa(couchDBReturn.StatusCode))
  1827  }
  1828  
  1829  //invalidCouchDBResponse checks to make sure either a valid response or error is returned
  1830  func invalidCouchDBReturn(resp *http.Response, errResp error) bool {
  1831  	if resp == nil && errResp == nil {
  1832  		return true
  1833  	}
  1834  	return false
  1835  }
  1836  
  1837  //isJSON tests a string to determine if a valid JSON
  1838  func isJSON(s string) bool {
  1839  	var js map[string]interface{}
  1840  	return json.Unmarshal([]byte(s), &js) == nil
  1841  }
  1842  
  1843  // encodePathElement uses Golang for url path encoding, additionally:
  1844  // '/' is replaced by %2F, otherwise path encoding will treat as path separator and ignore it
  1845  // '+' is replaced by %2B, otherwise path encoding will ignore it, while CouchDB will unencode the plus as a space
  1846  // Note that all other URL special characters have been tested successfully without need for special handling
  1847  func encodePathElement(str string) string {
  1848  
  1849  	u := &url.URL{}
  1850  	u.Path = str
  1851  	encodedStr := u.EscapedPath() // url encode using golang url path encoding rules
  1852  	encodedStr = strings.Replace(encodedStr, "/", "%2F", -1)
  1853  	encodedStr = strings.Replace(encodedStr, "+", "%2B", -1)
  1854  
  1855  	return encodedStr
  1856  }
  1857  
  1858  func encodeForJSON(str string) (string, error) {
  1859  	buf := &bytes.Buffer{}
  1860  	encoder := json.NewEncoder(buf)
  1861  	if err := encoder.Encode(str); err != nil {
  1862  		return "", errors.Wrap(err, "error encoding json data")
  1863  	}
  1864  	// Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127)
  1865  	buffer := buf.Bytes()
  1866  	return string(buffer[1 : len(buffer)-2]), nil
  1867  }
  1868  
  1869  // printDocumentIds is a convenience method to print readable log entries for arrays of pointers
  1870  // to couch document IDs
  1871  func printDocumentIds(documentPointers []*couchDoc) (string, error) {
  1872  
  1873  	documentIds := []string{}
  1874  
  1875  	for _, documentPointer := range documentPointers {
  1876  		docMetadata := &docMetadata{}
  1877  		err := json.Unmarshal(documentPointer.jsonValue, &docMetadata)
  1878  		if err != nil {
  1879  			return "", errors.Wrap(err, "error unmarshalling json data")
  1880  		}
  1881  		documentIds = append(documentIds, docMetadata.ID)
  1882  	}
  1883  	return strings.Join(documentIds, ","), nil
  1884  }