github.com/inklabsfoundation/inkchain@v0.17.1-0.20181025012015-c3cef8062f19/core/ledger/util/couchdb/couchdb.go (about)

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