github.com/adnan-c/fabric_e2e_couchdb@v0.6.1-preview.0.20170228180935-21ce6b23cf91/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/json"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"log"
    27  	"mime"
    28  	"mime/multipart"
    29  	"net/http"
    30  	"net/http/httputil"
    31  	"net/textproto"
    32  	"net/url"
    33  	"regexp"
    34  	"strconv"
    35  	"strings"
    36  	"unicode/utf8"
    37  
    38  	logging "github.com/op/go-logging"
    39  )
    40  
    41  var logger = logging.MustGetLogger("couchdb")
    42  
    43  // DBOperationResponse is body for successful database calls.
    44  type DBOperationResponse struct {
    45  	Ok  bool
    46  	id  string
    47  	rev string
    48  }
    49  
    50  // DBInfo is body for database information.
    51  type DBInfo struct {
    52  	DbName    string `json:"db_name"`
    53  	UpdateSeq string `json:"update_seq"`
    54  	Sizes     struct {
    55  		File     int `json:"file"`
    56  		External int `json:"external"`
    57  		Active   int `json:"active"`
    58  	} `json:"sizes"`
    59  	PurgeSeq int `json:"purge_seq"`
    60  	Other    struct {
    61  		DataSize int `json:"data_size"`
    62  	} `json:"other"`
    63  	DocDelCount       int    `json:"doc_del_count"`
    64  	DocCount          int    `json:"doc_count"`
    65  	DiskSize          int    `json:"disk_size"`
    66  	DiskFormatVersion int    `json:"disk_format_version"`
    67  	DataSize          int    `json:"data_size"`
    68  	CompactRunning    bool   `json:"compact_running"`
    69  	InstanceStartTime string `json:"instance_start_time"`
    70  }
    71  
    72  //ConnectionInfo is a structure for capturing the database info and version
    73  type ConnectionInfo struct {
    74  	Couchdb string `json:"couchdb"`
    75  	Version string `json:"version"`
    76  	Vendor  struct {
    77  		Name string `json:"name"`
    78  	} `json:"vendor"`
    79  }
    80  
    81  //RangeQueryResponse is used for processing REST range query responses from CouchDB
    82  type RangeQueryResponse struct {
    83  	TotalRows int `json:"total_rows"`
    84  	Offset    int `json:"offset"`
    85  	Rows      []struct {
    86  		ID    string `json:"id"`
    87  		Key   string `json:"key"`
    88  		Value struct {
    89  			Rev string `json:"rev"`
    90  		} `json:"value"`
    91  		Doc json.RawMessage `json:"doc"`
    92  	} `json:"rows"`
    93  }
    94  
    95  //QueryResponse is used for processing REST query responses from CouchDB
    96  type QueryResponse struct {
    97  	Warning string            `json:"warning"`
    98  	Docs    []json.RawMessage `json:"docs"`
    99  }
   100  
   101  //Doc is used for capturing if attachments are return in the query from CouchDB
   102  type Doc struct {
   103  	ID          string          `json:"_id"`
   104  	Rev         string          `json:"_rev"`
   105  	Attachments json.RawMessage `json:"_attachments"`
   106  }
   107  
   108  //DocID is a minimal structure for capturing the ID from a query result
   109  type DocID struct {
   110  	ID string `json:"_id"`
   111  }
   112  
   113  //QueryResult is used for returning query results from CouchDB
   114  type QueryResult struct {
   115  	ID          string
   116  	Value       []byte
   117  	Attachments []Attachment
   118  }
   119  
   120  //CouchConnectionDef contains parameters
   121  type CouchConnectionDef struct {
   122  	URL      string
   123  	Username string
   124  	Password string
   125  }
   126  
   127  //CouchInstance represents a CouchDB instance
   128  type CouchInstance struct {
   129  	conf CouchConnectionDef //connection configuration
   130  }
   131  
   132  //CouchDatabase represents a database within a CouchDB instance
   133  type CouchDatabase struct {
   134  	couchInstance CouchInstance //connection configuration
   135  	dbName        string
   136  }
   137  
   138  //DBReturn contains an error reported by CouchDB
   139  type DBReturn struct {
   140  	StatusCode int    `json:"status_code"`
   141  	Error      string `json:"error"`
   142  	Reason     string `json:"reason"`
   143  }
   144  
   145  //Attachment contains the definition for an attached file for couchdb
   146  type Attachment struct {
   147  	Name            string
   148  	ContentType     string
   149  	Length          uint64
   150  	AttachmentBytes []byte
   151  }
   152  
   153  //DocRev returns the Id and revision for a couchdb document
   154  type DocRev struct {
   155  	Id  string `json:"_id"`
   156  	Rev string `json:"_rev"`
   157  }
   158  
   159  //FileDetails defines the structure needed to send an attachment to couchdb
   160  type FileDetails struct {
   161  	Follows     bool   `json:"follows"`
   162  	ContentType string `json:"content_type"`
   163  	Length      int    `json:"length"`
   164  }
   165  
   166  //CouchDoc defines the structure for a JSON document value
   167  type CouchDoc struct {
   168  	JSONValue   []byte
   169  	Attachments []Attachment
   170  }
   171  
   172  //CreateConnectionDefinition for a new client connection
   173  func CreateConnectionDefinition(couchDBAddress, username, password string) (*CouchConnectionDef, error) {
   174  
   175  	logger.Debugf("Entering CreateConnectionDefinition()")
   176  
   177  	//connectURL := fmt.Sprintf("%s//%s", "http:", couchDBAddress)
   178  	//connectURL := couchDBAddress
   179  
   180  	connectURL := &url.URL{
   181  		Host:   couchDBAddress,
   182  		Scheme: "http",
   183  	}
   184  
   185  	//parse the constructed URL to verify no errors
   186  	finalURL, err := url.Parse(connectURL.String())
   187  	if err != nil {
   188  		logger.Errorf("URL parse error: %s", err.Error())
   189  		return nil, err
   190  	}
   191  
   192  	logger.Debugf("Created database configuration  URL=[%s]", finalURL.String())
   193  	logger.Debugf("Exiting CreateConnectionDefinition()")
   194  
   195  	//return an object containing the connection information
   196  	return &CouchConnectionDef{finalURL.String(), username, password}, nil
   197  }
   198  
   199  //CreateDatabaseIfNotExist method provides function to create database
   200  func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() (*DBOperationResponse, error) {
   201  
   202  	logger.Debugf("Entering CreateDatabaseIfNotExist()")
   203  
   204  	dbInfo, couchDBReturn, err := dbclient.GetDatabaseInfo()
   205  	if err != nil {
   206  		if couchDBReturn == nil || couchDBReturn.StatusCode != 404 {
   207  			return nil, err
   208  		}
   209  	}
   210  
   211  	if dbInfo == nil && couchDBReturn.StatusCode == 404 {
   212  
   213  		logger.Debugf("Database %s does not exist.", dbclient.dbName)
   214  
   215  		connectURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   216  		if err != nil {
   217  			logger.Errorf("URL parse error: %s", err.Error())
   218  			return nil, err
   219  		}
   220  		connectURL.Path = dbclient.dbName
   221  
   222  		//process the URL with a PUT, creates the database
   223  		resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPut, connectURL.String(), nil, "", "")
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  		defer resp.Body.Close()
   228  
   229  		//Get the response from the create REST call
   230  		dbResponse := &DBOperationResponse{}
   231  		json.NewDecoder(resp.Body).Decode(&dbResponse)
   232  
   233  		if dbResponse.Ok == true {
   234  			logger.Debugf("Created database %s ", dbclient.dbName)
   235  		}
   236  
   237  		logger.Debugf("Exiting CreateDatabaseIfNotExist()")
   238  
   239  		return dbResponse, nil
   240  
   241  	}
   242  
   243  	logger.Debugf("Database %s already exists", dbclient.dbName)
   244  
   245  	logger.Debugf("Exiting CreateDatabaseIfNotExist()")
   246  
   247  	return nil, nil
   248  
   249  }
   250  
   251  //GetDatabaseInfo method provides function to retrieve database information
   252  func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) {
   253  
   254  	connectURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   255  	if err != nil {
   256  		logger.Errorf("URL parse error: %s", err.Error())
   257  		return nil, nil, err
   258  	}
   259  	connectURL.Path = dbclient.dbName
   260  
   261  	resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "")
   262  	if err != nil {
   263  		return nil, couchDBReturn, err
   264  	}
   265  	defer resp.Body.Close()
   266  
   267  	dbResponse := &DBInfo{}
   268  	json.NewDecoder(resp.Body).Decode(&dbResponse)
   269  
   270  	// trace the database info response
   271  	if logger.IsEnabledFor(logging.DEBUG) {
   272  		dbResponseJSON, err := json.Marshal(dbResponse)
   273  		if err == nil {
   274  			logger.Debugf("GetDatabaseInfo() dbResponseJSON: %s", dbResponseJSON)
   275  		}
   276  	}
   277  
   278  	return dbResponse, couchDBReturn, nil
   279  
   280  }
   281  
   282  //VerifyConnection method provides function to verify the connection information
   283  func (couchInstance *CouchInstance) VerifyConnection() (*ConnectionInfo, *DBReturn, error) {
   284  
   285  	connectURL, err := url.Parse(couchInstance.conf.URL)
   286  	if err != nil {
   287  		logger.Errorf("URL parse error: %s", err.Error())
   288  		return nil, nil, err
   289  	}
   290  	connectURL.Path = "/"
   291  
   292  	resp, couchDBReturn, err := couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "")
   293  	if err != nil {
   294  		return nil, couchDBReturn, err
   295  	}
   296  	defer resp.Body.Close()
   297  
   298  	dbResponse := &ConnectionInfo{}
   299  	errJSON := json.NewDecoder(resp.Body).Decode(&dbResponse)
   300  	if errJSON != nil {
   301  		return nil, nil, errJSON
   302  	}
   303  
   304  	// trace the database info response
   305  	if logger.IsEnabledFor(logging.DEBUG) {
   306  		dbResponseJSON, err := json.Marshal(dbResponse)
   307  		if err == nil {
   308  			logger.Debugf("VerifyConnection() dbResponseJSON: %s", dbResponseJSON)
   309  		}
   310  	}
   311  
   312  	return dbResponse, couchDBReturn, nil
   313  
   314  }
   315  
   316  //DropDatabase provides method to drop an existing database
   317  func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) {
   318  
   319  	logger.Debugf("Entering DropDatabase()")
   320  
   321  	connectURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   322  	if err != nil {
   323  		logger.Errorf("URL parse error: %s", err.Error())
   324  		return nil, err
   325  	}
   326  	connectURL.Path = dbclient.dbName
   327  
   328  	resp, _, err := dbclient.couchInstance.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "")
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  	defer resp.Body.Close()
   333  
   334  	dbResponse := &DBOperationResponse{}
   335  	json.NewDecoder(resp.Body).Decode(&dbResponse)
   336  
   337  	if dbResponse.Ok == true {
   338  		logger.Debugf("Dropped database %s ", dbclient.dbName)
   339  	}
   340  
   341  	logger.Debugf("Exiting DropDatabase()")
   342  
   343  	if dbResponse.Ok == true {
   344  
   345  		return dbResponse, nil
   346  
   347  	}
   348  
   349  	return dbResponse, fmt.Errorf("Error dropping database")
   350  
   351  }
   352  
   353  // EnsureFullCommit calls _ensure_full_commit for explicit fsync
   354  func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error) {
   355  
   356  	logger.Debugf("Entering EnsureFullCommit()")
   357  
   358  	connectURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   359  	if err != nil {
   360  		logger.Errorf("URL parse error: %s", err.Error())
   361  		return nil, err
   362  	}
   363  	connectURL.Path = dbclient.dbName + "/_ensure_full_commit"
   364  
   365  	resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPost, connectURL.String(), nil, "", "")
   366  	if err != nil {
   367  		logger.Errorf("Failed to invoke _ensure_full_commit Error: %s\n", err.Error())
   368  		return nil, err
   369  	}
   370  	defer resp.Body.Close()
   371  
   372  	dbResponse := &DBOperationResponse{}
   373  	json.NewDecoder(resp.Body).Decode(&dbResponse)
   374  
   375  	if dbResponse.Ok == true {
   376  		logger.Debugf("_ensure_full_commit database %s ", dbclient.dbName)
   377  	}
   378  
   379  	logger.Debugf("Exiting EnsureFullCommit()")
   380  
   381  	if dbResponse.Ok == true {
   382  
   383  		return dbResponse, nil
   384  
   385  	}
   386  
   387  	return dbResponse, fmt.Errorf("Error syncing database")
   388  }
   389  
   390  //SaveDoc method provides a function to save a document, id and byte array
   391  func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc) (string, error) {
   392  
   393  	logger.Debugf("Entering SaveDoc()  id=[%s]", id)
   394  	if !utf8.ValidString(id) {
   395  		return "", fmt.Errorf("doc id [%x] not a valid utf8 string", id)
   396  	}
   397  
   398  	saveURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   399  	if err != nil {
   400  		logger.Errorf("URL parse error: %s", err.Error())
   401  		return "", err
   402  	}
   403  
   404  	saveURL.Path = dbclient.dbName
   405  	// id can contain a '/', so encode separately
   406  	saveURL = &url.URL{Opaque: saveURL.String() + "/" + encodePathElement(id)}
   407  
   408  	if rev == "" {
   409  
   410  		//See if the document already exists, we need the rev for save
   411  		_, revdoc, err2 := dbclient.ReadDoc(id)
   412  		if err2 != nil {
   413  			//set the revision to indicate that the document was not found
   414  			rev = ""
   415  		} else {
   416  			//set the revision to the rev returned from the document read
   417  			rev = revdoc
   418  		}
   419  	}
   420  
   421  	logger.Debugf("  rev=%s", rev)
   422  
   423  	//Set up a buffer for the data to be pushed to couchdb
   424  	data := new(bytes.Buffer)
   425  
   426  	//Set up a default boundary for use by multipart if sending attachments
   427  	defaultBoundary := ""
   428  
   429  	//check to see if attachments is nil, if so, then this is a JSON only
   430  	if couchDoc.Attachments == nil {
   431  
   432  		//Test to see if this is a valid JSON
   433  		if IsJSON(string(couchDoc.JSONValue)) != true {
   434  			return "", fmt.Errorf("JSON format is not valid")
   435  		}
   436  
   437  		// if there are no attachments, then use the bytes passed in as the JSON
   438  		data.ReadFrom(bytes.NewReader(couchDoc.JSONValue))
   439  
   440  	} else { // there are attachments
   441  
   442  		//attachments are included, create the multipart definition
   443  		multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc, defaultBoundary)
   444  		if err3 != nil {
   445  			return "", err3
   446  		}
   447  
   448  		//Set the data buffer to the data from the create multi-part data
   449  		data.ReadFrom(&multipartData)
   450  
   451  		//Set the default boundary to the value generated in the multipart creation
   452  		defaultBoundary = multipartBoundary
   453  
   454  	}
   455  
   456  	//handle the request for saving the JSON or attachments
   457  	resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPut, saveURL.String(), data, rev, defaultBoundary)
   458  	if err != nil {
   459  		return "", err
   460  	}
   461  	defer resp.Body.Close()
   462  
   463  	//get the revision and return
   464  	revision, err := getRevisionHeader(resp)
   465  	if err != nil {
   466  		return "", err
   467  	}
   468  
   469  	logger.Debugf("Exiting SaveDoc()")
   470  
   471  	return revision, nil
   472  
   473  }
   474  
   475  func createAttachmentPart(couchDoc *CouchDoc, defaultBoundary string) (bytes.Buffer, string, error) {
   476  
   477  	//Create a buffer for writing the result
   478  	writeBuffer := new(bytes.Buffer)
   479  
   480  	// read the attachment and save as an attachment
   481  	writer := multipart.NewWriter(writeBuffer)
   482  
   483  	//retrieve the boundary for the multipart
   484  	defaultBoundary = writer.Boundary()
   485  
   486  	fileAttachments := map[string]FileDetails{}
   487  
   488  	for _, attachment := range couchDoc.Attachments {
   489  		fileAttachments[attachment.Name] = FileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)}
   490  	}
   491  
   492  	attachmentJSONMap := map[string]interface{}{
   493  		"_attachments": fileAttachments}
   494  
   495  	//Add any data uploaded with the files
   496  	if couchDoc.JSONValue != nil {
   497  
   498  		//create a generic map
   499  		genericMap := make(map[string]interface{})
   500  		//unmarshal the data into the generic map
   501  		json.Unmarshal(couchDoc.JSONValue, &genericMap)
   502  
   503  		//add all key/values to the attachmentJSONMap
   504  		for jsonKey, jsonValue := range genericMap {
   505  			attachmentJSONMap[jsonKey] = jsonValue
   506  		}
   507  
   508  	}
   509  
   510  	filesForUpload, _ := json.Marshal(attachmentJSONMap)
   511  	logger.Debugf(string(filesForUpload))
   512  
   513  	//create the header for the JSON
   514  	header := make(textproto.MIMEHeader)
   515  	header.Set("Content-Type", "application/json")
   516  
   517  	part, err := writer.CreatePart(header)
   518  	if err != nil {
   519  		return *writeBuffer, defaultBoundary, err
   520  	}
   521  
   522  	part.Write(filesForUpload)
   523  
   524  	for _, attachment := range couchDoc.Attachments {
   525  
   526  		header := make(textproto.MIMEHeader)
   527  		part, err2 := writer.CreatePart(header)
   528  		if err2 != nil {
   529  			return *writeBuffer, defaultBoundary, err2
   530  		}
   531  		part.Write(attachment.AttachmentBytes)
   532  
   533  	}
   534  
   535  	err = writer.Close()
   536  	if err != nil {
   537  		return *writeBuffer, defaultBoundary, err
   538  	}
   539  
   540  	return *writeBuffer, defaultBoundary, nil
   541  
   542  }
   543  
   544  func getRevisionHeader(resp *http.Response) (string, error) {
   545  
   546  	revision := resp.Header.Get("Etag")
   547  
   548  	if revision == "" {
   549  		return "", fmt.Errorf("No revision tag detected")
   550  	}
   551  
   552  	reg := regexp.MustCompile(`"([^"]*)"`)
   553  	revisionNoQuotes := reg.ReplaceAllString(revision, "${1}")
   554  	return revisionNoQuotes, nil
   555  
   556  }
   557  
   558  //ReadDoc method provides function to retrieve a document from the database by id
   559  func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) {
   560  	var couchDoc CouchDoc
   561  	logger.Debugf("Entering ReadDoc()  id=[%s]", id)
   562  	if !utf8.ValidString(id) {
   563  		return nil, "", fmt.Errorf("doc id [%x] not a valid utf8 string", id)
   564  	}
   565  
   566  	readURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   567  	if err != nil {
   568  		logger.Errorf("URL parse error: %s", err.Error())
   569  		return nil, "", err
   570  	}
   571  	readURL.Path = dbclient.dbName
   572  	// id can contain a '/', so encode separately
   573  	readURL = &url.URL{Opaque: readURL.String() + "/" + encodePathElement(id)}
   574  
   575  	query := readURL.Query()
   576  	query.Add("attachments", "true")
   577  
   578  	readURL.RawQuery = query.Encode()
   579  
   580  	resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "")
   581  	if err != nil {
   582  		if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
   583  			logger.Debug("Document not found (404), returning nil value instead of 404 error")
   584  			// non-existent document should return nil value instead of a 404 error
   585  			// for details see https://github.com/hyperledger-archives/fabric/issues/936
   586  			return nil, "", nil
   587  		}
   588  		logger.Debugf("couchDBReturn=%v\n", couchDBReturn)
   589  		return nil, "", err
   590  	}
   591  	defer resp.Body.Close()
   592  
   593  	//Get the media type from the Content-Type header
   594  	mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
   595  	if err != nil {
   596  		log.Fatal(err)
   597  	}
   598  
   599  	//Get the revision from header
   600  	revision, err := getRevisionHeader(resp)
   601  	if err != nil {
   602  		return nil, "", err
   603  	}
   604  
   605  	//check to see if the is multipart,  handle as attachment if multipart is detected
   606  	if strings.HasPrefix(mediaType, "multipart/") {
   607  		//Set up the multipart reader based on the boundary
   608  		multipartReader := multipart.NewReader(resp.Body, params["boundary"])
   609  		for {
   610  			p, err := multipartReader.NextPart()
   611  			if err == io.EOF {
   612  				break // processed all parts
   613  			}
   614  			if err != nil {
   615  				return nil, "", err
   616  			}
   617  
   618  			defer p.Close()
   619  
   620  			logger.Debugf("part header=%s", p.Header)
   621  			switch p.Header.Get("Content-Type") {
   622  			case "application/json":
   623  				partdata, err := ioutil.ReadAll(p)
   624  				if err != nil {
   625  					return nil, "", err
   626  				}
   627  				couchDoc.JSONValue = partdata
   628  			default:
   629  
   630  				//Create an attachment structure and load it
   631  				attachment := Attachment{}
   632  				attachment.ContentType = p.Header.Get("Content-Type")
   633  				contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";")
   634  				if strings.TrimSpace(contentDispositionParts[0]) == "attachment" {
   635  					switch p.Header.Get("Content-Encoding") {
   636  					case "gzip": //See if the part is gzip encoded
   637  
   638  						var respBody []byte
   639  
   640  						gr, err := gzip.NewReader(p)
   641  						if err != nil {
   642  							return nil, "", err
   643  						}
   644  						respBody, err = ioutil.ReadAll(gr)
   645  						if err != nil {
   646  							return nil, "", err
   647  						}
   648  
   649  						logger.Debugf("Retrieved attachment data")
   650  						attachment.AttachmentBytes = respBody
   651  						attachment.Name = p.FileName()
   652  						couchDoc.Attachments = append(couchDoc.Attachments, attachment)
   653  
   654  					default:
   655  
   656  						//retrieve the data,  this is not gzip
   657  						partdata, err := ioutil.ReadAll(p)
   658  						if err != nil {
   659  							return nil, "", err
   660  						}
   661  						logger.Debugf("Retrieved attachment data")
   662  						attachment.AttachmentBytes = partdata
   663  						attachment.Name = p.FileName()
   664  						couchDoc.Attachments = append(couchDoc.Attachments, attachment)
   665  
   666  					} // end content-encoding switch
   667  				} // end if attachment
   668  			} // end content-type switch
   669  		} // for all multiparts
   670  
   671  		return &couchDoc, revision, nil
   672  	}
   673  
   674  	//handle as JSON document
   675  	couchDoc.JSONValue, err = ioutil.ReadAll(resp.Body)
   676  	if err != nil {
   677  		return nil, "", err
   678  	}
   679  
   680  	logger.Debugf("Exiting ReadDoc()")
   681  	return &couchDoc, revision, nil
   682  }
   683  
   684  //ReadDocRange method provides function to a range of documents based on the start and end keys
   685  //startKey and endKey can also be empty strings.  If startKey and endKey are empty, all documents are returned
   686  //TODO This function provides a limit option to specify the max number of entries.   This will
   687  //need to be added to configuration options.  Skip will not be used by Fabric since a consistent
   688  //result set is required
   689  func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip int) (*[]QueryResult, error) {
   690  
   691  	logger.Debugf("Entering ReadDocRange()  startKey=%s, endKey=%s", startKey, endKey)
   692  
   693  	var results []QueryResult
   694  
   695  	rangeURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   696  	if err != nil {
   697  		logger.Errorf("URL parse error: %s", err.Error())
   698  		return nil, err
   699  	}
   700  	rangeURL.Path = dbclient.dbName + "/_all_docs"
   701  
   702  	queryParms := rangeURL.Query()
   703  	queryParms.Set("limit", strconv.Itoa(limit))
   704  	queryParms.Add("skip", strconv.Itoa(skip))
   705  	queryParms.Add("include_docs", "true")
   706  	queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb
   707  
   708  	//Append the startKey if provided
   709  
   710  	if startKey != "" {
   711  		var err error
   712  		if startKey, err = encodeForJSON(startKey); err != nil {
   713  			return nil, err
   714  		}
   715  		queryParms.Add("startkey", "\""+startKey+"\"")
   716  	}
   717  
   718  	//Append the endKey if provided
   719  	if endKey != "" {
   720  		var err error
   721  		if endKey, err = encodeForJSON(endKey); err != nil {
   722  			return nil, err
   723  		}
   724  		queryParms.Add("endkey", "\""+endKey+"\"")
   725  	}
   726  
   727  	rangeURL.RawQuery = queryParms.Encode()
   728  
   729  	resp, _, err := dbclient.couchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "")
   730  	if err != nil {
   731  		return nil, err
   732  	}
   733  	defer resp.Body.Close()
   734  
   735  	if logger.IsEnabledFor(logging.DEBUG) {
   736  		dump, err2 := httputil.DumpResponse(resp, true)
   737  		if err2 != nil {
   738  			log.Fatal(err2)
   739  		}
   740  		logger.Debugf("%s", dump)
   741  	}
   742  
   743  	//handle as JSON document
   744  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
   745  	if err != nil {
   746  		return nil, err
   747  	}
   748  
   749  	var jsonResponse = &RangeQueryResponse{}
   750  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
   751  	if err2 != nil {
   752  		return nil, err2
   753  	}
   754  
   755  	logger.Debugf("Total Rows: %d", jsonResponse.TotalRows)
   756  
   757  	for _, row := range jsonResponse.Rows {
   758  
   759  		var jsonDoc = &Doc{}
   760  		err3 := json.Unmarshal(row.Doc, &jsonDoc)
   761  		if err3 != nil {
   762  			return nil, err3
   763  		}
   764  
   765  		if jsonDoc.Attachments != nil {
   766  
   767  			logger.Debugf("Adding JSON document and attachments for id: %s", jsonDoc.ID)
   768  
   769  			couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID)
   770  			if err != nil {
   771  				return nil, err
   772  			}
   773  
   774  			var addDocument = &QueryResult{jsonDoc.ID, couchDoc.JSONValue, couchDoc.Attachments}
   775  			results = append(results, *addDocument)
   776  
   777  		} else {
   778  
   779  			logger.Debugf("Adding json docment for id: %s", jsonDoc.ID)
   780  
   781  			var addDocument = &QueryResult{jsonDoc.ID, row.Doc, nil}
   782  			results = append(results, *addDocument)
   783  
   784  		}
   785  
   786  	}
   787  
   788  	logger.Debugf("Exiting ReadDocRange()")
   789  
   790  	return &results, nil
   791  
   792  }
   793  
   794  //DeleteDoc method provides function to delete a document from the database by id
   795  func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error {
   796  
   797  	logger.Debugf("Entering DeleteDoc()  id=%s", id)
   798  
   799  	deleteURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   800  	if err != nil {
   801  		logger.Errorf("URL parse error: %s", err.Error())
   802  		return err
   803  	}
   804  
   805  	deleteURL.Path = dbclient.dbName
   806  	// id can contain a '/', so encode separately
   807  	deleteURL = &url.URL{Opaque: deleteURL.String() + "/" + encodePathElement(id)}
   808  
   809  	if rev == "" {
   810  
   811  		//See if the document already exists, we need the rev for delete
   812  		_, revdoc, err2 := dbclient.ReadDoc(id)
   813  		if err2 != nil {
   814  			//set the revision to indicate that the document was not found
   815  			rev = ""
   816  		} else {
   817  			//set the revision to the rev returned from the document read
   818  			rev = revdoc
   819  		}
   820  	}
   821  
   822  	logger.Debugf("  rev=%s", rev)
   823  
   824  	resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodDelete, deleteURL.String(), nil, rev, "")
   825  	if err != nil {
   826  		fmt.Printf("couchDBReturn=%v", couchDBReturn)
   827  		if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
   828  			logger.Debug("Document not found (404), returning nil value instead of 404 error")
   829  			// non-existent document should return nil value instead of a 404 error
   830  			// for details see https://github.com/hyperledger-archives/fabric/issues/936
   831  			return nil
   832  		}
   833  		return err
   834  	}
   835  	defer resp.Body.Close()
   836  
   837  	logger.Debugf("Exiting DeleteDoc()")
   838  
   839  	return nil
   840  
   841  }
   842  
   843  //QueryDocuments method provides function for processing a query
   844  func (dbclient *CouchDatabase) QueryDocuments(query string, limit, skip int) (*[]QueryResult, error) {
   845  
   846  	logger.Debugf("Entering QueryDocuments()  query=%s", query)
   847  
   848  	var results []QueryResult
   849  
   850  	queryURL, err := url.Parse(dbclient.couchInstance.conf.URL)
   851  	if err != nil {
   852  		logger.Errorf("URL parse error: %s", err.Error())
   853  		return nil, err
   854  	}
   855  
   856  	queryURL.Path = dbclient.dbName + "/_find"
   857  
   858  	queryParms := queryURL.Query()
   859  	queryParms.Set("limit", strconv.Itoa(limit))
   860  	queryParms.Add("skip", strconv.Itoa(skip))
   861  
   862  	queryURL.RawQuery = queryParms.Encode()
   863  
   864  	//Set up a buffer for the data to be pushed to couchdb
   865  	data := new(bytes.Buffer)
   866  
   867  	data.ReadFrom(bytes.NewReader([]byte(query)))
   868  
   869  	resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPost, queryURL.String(), data, "", "")
   870  	if err != nil {
   871  		return nil, err
   872  	}
   873  	defer resp.Body.Close()
   874  
   875  	if logger.IsEnabledFor(logging.DEBUG) {
   876  		dump, err2 := httputil.DumpResponse(resp, true)
   877  		if err2 != nil {
   878  			log.Fatal(err2)
   879  		}
   880  		logger.Debugf("%s", dump)
   881  	}
   882  
   883  	//handle as JSON document
   884  	jsonResponseRaw, err := ioutil.ReadAll(resp.Body)
   885  	if err != nil {
   886  		return nil, err
   887  	}
   888  
   889  	var jsonResponse = &QueryResponse{}
   890  
   891  	err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse)
   892  	if err2 != nil {
   893  		return nil, err2
   894  	}
   895  
   896  	for _, row := range jsonResponse.Docs {
   897  
   898  		var jsonDoc = &Doc{}
   899  		err3 := json.Unmarshal(row, &jsonDoc)
   900  		if err3 != nil {
   901  			return nil, err3
   902  		}
   903  
   904  		if jsonDoc.Attachments != nil {
   905  
   906  			logger.Debugf("Adding JSON docment and attachments for id: %s", jsonDoc.ID)
   907  
   908  			couchDoc, _, err := dbclient.ReadDoc(jsonDoc.ID)
   909  			if err != nil {
   910  				return nil, err
   911  			}
   912  			var addDocument = &QueryResult{ID: jsonDoc.ID, Value: couchDoc.JSONValue, Attachments: couchDoc.Attachments}
   913  			results = append(results, *addDocument)
   914  
   915  		} else {
   916  			logger.Debugf("Adding json docment for id: %s", jsonDoc.ID)
   917  			var addDocument = &QueryResult{ID: jsonDoc.ID, Value: row, Attachments: nil}
   918  
   919  			results = append(results, *addDocument)
   920  
   921  		}
   922  	}
   923  	logger.Debugf("Exiting QueryDocuments()")
   924  
   925  	return &results, nil
   926  
   927  }
   928  
   929  //handleRequest method is a generic http request handler
   930  func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data io.Reader, rev string, multipartBoundary string) (*http.Response, *DBReturn, error) {
   931  
   932  	logger.Debugf("Entering handleRequest()  method=%s  url=%v", method, connectURL)
   933  
   934  	//Create request based on URL for couchdb operation
   935  	req, err := http.NewRequest(method, connectURL, data)
   936  	if err != nil {
   937  		return nil, nil, err
   938  	}
   939  
   940  	//add content header for PUT
   941  	if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete {
   942  
   943  		//If the multipartBoundary is not set, then this is a JSON and content-type should be set
   944  		//to application/json.   Else, this is contains an attachment and needs to be multipart
   945  		if multipartBoundary == "" {
   946  			req.Header.Set("Content-Type", "application/json")
   947  		} else {
   948  			req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"")
   949  		}
   950  
   951  		//check to see if the revision is set,  if so, pass as a header
   952  		if rev != "" {
   953  			req.Header.Set("If-Match", rev)
   954  		}
   955  	}
   956  
   957  	//add content header for PUT
   958  	if method == http.MethodPut || method == http.MethodPost {
   959  		req.Header.Set("Accept", "application/json")
   960  	}
   961  
   962  	//add content header for GET
   963  	if method == http.MethodGet {
   964  		req.Header.Set("Accept", "multipart/related")
   965  	}
   966  
   967  	//If username and password are set the use basic auth
   968  	if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" {
   969  		req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password)
   970  	}
   971  
   972  	if logger.IsEnabledFor(logging.DEBUG) {
   973  		dump, _ := httputil.DumpRequestOut(req, false)
   974  		// compact debug log by replacing carriage return / line feed with dashes to separate http headers
   975  		logger.Debugf("HTTP Request: %s", bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1))
   976  	}
   977  
   978  	//Create the http client
   979  	client := &http.Client{}
   980  
   981  	transport := &http.Transport{Proxy: http.ProxyFromEnvironment}
   982  	transport.DisableCompression = false
   983  	client.Transport = transport
   984  
   985  	//Execute http request
   986  	resp, err := client.Do(req)
   987  	if err != nil {
   988  		return nil, nil, err
   989  	}
   990  
   991  	//create the return object for couchDB
   992  	couchDBReturn := &DBReturn{}
   993  
   994  	//set the return code for the couchDB request
   995  	couchDBReturn.StatusCode = resp.StatusCode
   996  
   997  	//check to see if the status code is 400 or higher
   998  	//in this case, the http request succeeded but CouchDB is reporing an error
   999  	if resp.StatusCode >= 400 {
  1000  
  1001  		jsonError, err := ioutil.ReadAll(resp.Body)
  1002  		if err != nil {
  1003  			return nil, nil, err
  1004  		}
  1005  
  1006  		logger.Debugf("Couch DB error  status code=%v  error=%s", resp.StatusCode, jsonError)
  1007  
  1008  		errorBytes := []byte(jsonError)
  1009  
  1010  		json.Unmarshal(errorBytes, &couchDBReturn)
  1011  
  1012  		return nil, couchDBReturn, fmt.Errorf("Couch DB Error: %s", couchDBReturn.Reason)
  1013  
  1014  	}
  1015  
  1016  	logger.Debugf("Exiting handleRequest()")
  1017  
  1018  	//If no errors, then return the results
  1019  	return resp, couchDBReturn, nil
  1020  }
  1021  
  1022  //IsJSON tests a string to determine if a valid JSON
  1023  func IsJSON(s string) bool {
  1024  	var js map[string]interface{}
  1025  	return json.Unmarshal([]byte(s), &js) == nil
  1026  }
  1027  
  1028  // encodePathElement uses Golang for encoding and in addition, replaces a '/' by %2F.
  1029  // Otherwise, in the regular encoding, a '/' is treated as a path separator in the url
  1030  func encodePathElement(str string) string {
  1031  	u := &url.URL{}
  1032  	u.Path = str
  1033  	encodedStr := u.String()
  1034  	encodedStr = strings.Replace(encodedStr, "/", "%2F", -1)
  1035  	return encodedStr
  1036  }
  1037  
  1038  func encodeForJSON(str string) (string, error) {
  1039  	buf := &bytes.Buffer{}
  1040  	encoder := json.NewEncoder(buf)
  1041  	if err := encoder.Encode(str); err != nil {
  1042  		return "", err
  1043  	}
  1044  	// Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127)
  1045  	buffer := buf.Bytes()
  1046  	return string(buffer[1 : len(buffer)-2]), nil
  1047  }