github.com/ewagmig/fabric@v2.1.1+incompatible/core/ledger/util/couchdb/couchdb.go (about)

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