diff --git a/core/ledger/kvledger/txmgmt/statedb/commontests/test_common.go b/core/ledger/kvledger/txmgmt/statedb/commontests/test_common.go index 58067618300..9b22c58bcfd 100644 --- a/core/ledger/kvledger/txmgmt/statedb/commontests/test_common.go +++ b/core/ledger/kvledger/txmgmt/statedb/commontests/test_common.go @@ -51,9 +51,8 @@ func TestBasicRW(t *testing.T, dbProvider statedb.VersionedDBProvider) { vv, _ := db.GetState("ns1", "key1") testutil.AssertEquals(t, vv, &vv1) - //TODO re-enable test after Couch version wrapper is added - //vv, _ = db.GetState("ns2", "key4") - //testutil.AssertEquals(t, vv, &vv4) + vv, _ = db.GetState("ns2", "key4") + testutil.AssertEquals(t, vv, &vv4) sp, err := db.GetLatestSavePoint() testutil.AssertNoError(t, err, "") @@ -91,9 +90,8 @@ func TestMultiDBBasicRW(t *testing.T, dbProvider statedb.VersionedDBProvider) { testutil.AssertNoError(t, err, "") testutil.AssertEquals(t, sp, savePoint1) - //TODO re-enable test after Couch version wrapper is added - //vv, _ = db2.GetState("ns1", "key1") - //testutil.AssertEquals(t, vv, &vv3) + vv, _ = db2.GetState("ns1", "key1") + testutil.AssertEquals(t, vv, &vv3) sp, err = db2.GetLatestSavePoint() testutil.AssertNoError(t, err, "") @@ -222,4 +220,23 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) { queryResult3, err := itr.Next() testutil.AssertNoError(t, err, "") testutil.AssertNil(t, queryResult3) + + // query with fields + itr, err = db.ExecuteQuery("{\"selector\":{\"owner\":\"jerry\"},\"fields\": [\"owner\", \"asset_name\", \"color\", \"size\"]}") + testutil.AssertNoError(t, err, "") + + // verify one jerry result + queryResult1, err = itr.Next() + testutil.AssertNoError(t, err, "") + testutil.AssertNotNil(t, queryResult1) + versionedQueryRecord = queryResult1.(*statedb.VersionedQueryRecord) + stringRecord = string(versionedQueryRecord.Record) + bFoundJerry = strings.Contains(stringRecord, "jerry") + testutil.AssertEquals(t, bFoundJerry, true) + + // verify no more results + queryResult2, err = itr.Next() + testutil.AssertNoError(t, err, "") + testutil.AssertNil(t, queryResult2) + } diff --git a/core/ledger/kvledger/txmgmt/statedb/statecouchdb/query_wrapper.go b/core/ledger/kvledger/txmgmt/statedb/statecouchdb/query_wrapper.go new file mode 100644 index 00000000000..ed761b0fc96 --- /dev/null +++ b/core/ledger/kvledger/txmgmt/statedb/statecouchdb/query_wrapper.go @@ -0,0 +1,139 @@ +package statecouchdb + +import ( + "encoding/json" + "fmt" + "strings" +) + +//ApplyQueryWrapper parses the query string passed to CouchDB +//the wrapper prepends the wrapper "data." to all fields specified in the query +//All fields in the selector must have "data." prepended to the field names +//Fields listed in fields key will have "data." prepended +//Fields in the sort key will have "data." prepended +func ApplyQueryWrapper(queryString string) []byte { + + //create a generic map for the query json + jsonQuery := make(map[string]interface{}) + + //unmarshal the selected json into the generic map + json.Unmarshal([]byte(queryString), &jsonQuery) + + //iterate through the JSON query + for jsonKey, jsonValue := range jsonQuery { + + //create a case for the data types found in the JSON query + switch jsonQueryPart := jsonValue.(type) { + + //if the type is an array, then this is either the "fields" or "sort" part of the query + case []interface{}: + + //check to see if this is a "fields" or "sort" array + //if jsonKey == jsonQueryFields || jsonKey == jsonQuerySort { + if jsonKey == jsonQueryFields { + + //iterate through the names and add the data wrapper for each field + for itemKey, fieldName := range jsonQueryPart { + + //add "data" wrapper to each field definition + jsonQueryPart[itemKey] = fmt.Sprintf("%v.%v", dataWrapper, fieldName) + } + + //Add the "_id" and "version" fields, these are needed by default + if jsonKey == jsonQueryFields { + + jsonQueryPart = append(jsonQueryPart, "_id") + jsonQueryPart = append(jsonQueryPart, "version") + + //Overwrite the query fields if the "_id" field has been added + jsonQuery[jsonQueryFields] = jsonQueryPart + } + + } + + if jsonKey == jsonQuerySort { + + //iterate through the names and add the data wrapper for each field + for sortItemKey, sortField := range jsonQueryPart { + + //create a case for the data types found in the JSON query + switch sortFieldType := sortField.(type) { + + //if the type is string, then this is a simple array of field names. + //Add the datawrapper to the field name + case string: + + //simple case, update the existing array item with the updated name + jsonQueryPart[sortItemKey] = fmt.Sprintf("%v.%v", dataWrapper, sortField) + + case interface{}: + + //this case is a little more complicated. Here we need to + //iterate over the mapped field names since this is an array of objects + //example: {"fieldname":"desc"} + for key, itemValue := range sortField.(map[string]interface{}) { + //delete the mapping for the field definition, since we have to change the + //value of the key + delete(sortField.(map[string]interface{}), key) + + //add the key back into the map with the field name wrapped with then "data" wrapper + sortField.(map[string]interface{})[fmt.Sprintf("%v.%v", dataWrapper, key)] = itemValue + } + + default: + + logger.Debugf("The type %v was not recognized as a valid sort field type.", sortFieldType) + + } + + } + } + + case interface{}: + + //if this is the "selector", the field names need to be mapped with the + //data wrapper + if jsonKey == jsonQuerySelector { + + processSelector(jsonQueryPart.(map[string]interface{})) + + } + + default: + + logger.Debugf("The value %v was not recognized as a valid selector field.", jsonKey) + + } + } + + //Marshal the updated json query + editedQuery, _ := json.Marshal(jsonQuery) + + logger.Debugf("Rewritten query with data wrapper: %s", editedQuery) + + return editedQuery + +} + +//processSelector is a recursion function for traversing the selector part of the query +func processSelector(selectorFragment map[string]interface{}) { + + //iterate through the top level definitions + for itemKey, itemValue := range selectorFragment { + + //check to see if the itemKey starts with a $. If so, this indicates an operator + if strings.HasPrefix(fmt.Sprintf("%s", itemKey), "$") { + + processSelector(itemValue.(map[string]interface{})) + + } else { + + //delete the mapping for the field definition, since we have to change the + //value of the key + delete(selectorFragment, itemKey) + //add the key back into the map with the field name wrapped with then "data" wrapper + selectorFragment[fmt.Sprintf("%v.%v", dataWrapper, itemKey)] = itemValue + + } + } +} diff --git a/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb.go b/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb.go index 24fbadb727d..fe2a44739b1 100644 --- a/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb.go +++ b/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" "sync" @@ -37,6 +38,11 @@ var compositeKeySep = []byte{0x00} var lastKeyIndicator = byte(0x01) var savePointKey = []byte{0x00} +var dataWrapper = "data" +var jsonQuerySort = "sort" +var jsonQueryFields = "fields" +var jsonQuerySelector = "selector" + // VersionedDBProvider implements interface VersionedDBProvider type VersionedDBProvider struct { couchInstance *couchdb.CouchInstance @@ -135,9 +141,56 @@ func (vdb *VersionedDB) GetState(namespace string, key string) (*statedb.Version } } - ver := version.NewHeight(1, 1) //TODO - version hardcoded to 1 is a temporary value for the prototype + //remove the data wrapper and return the value and version + returnValue, returnVersion := removeDataWrapper(docBytes) + + return &statedb.VersionedValue{Value: returnValue, Version: &returnVersion}, nil +} + +func removeDataWrapper(wrappedValue []byte) ([]byte, version.Height) { + + //initialize the return value + returnValue := []byte{} + + //initialize a default return version + returnVersion := version.NewHeight(0, 0) + + //if this is a JSON, then remove the data wrapper + if couchdb.IsJSON(string(wrappedValue)) { + + //create a generic map for the json + jsonResult := make(map[string]interface{}) + + //unmarshal the selected json into the generic map + json.Unmarshal(wrappedValue, &jsonResult) + + //place the result json in the data key + returnMap := jsonResult[dataWrapper] + + //marshal the mapped data. this wrappers the result in a key named "data" + returnValue, _ = json.Marshal(returnMap) + + //create an array containing the blockNum and txNum + versionArray := strings.Split(fmt.Sprintf("%s", jsonResult["version"]), ":") + + //convert the blockNum from String to unsigned int + blockNum, _ := strconv.ParseUint(versionArray[0], 10, 64) + + //convert the txNum from String to unsigned int + txNum, _ := strconv.ParseUint(versionArray[1], 10, 64) + + //create the version based on the blockNum and txNum + returnVersion = version.NewHeight(blockNum, txNum) + + } else { + + //this is a binary, so decode the value and version from the binary + returnValue, returnVersion = statedb.DecodeValue(wrappedValue) + + } + + return returnValue, *returnVersion - return &statedb.VersionedValue{Value: docBytes, Version: ver}, nil } // GetStateMultipleKeys implements method in VersionedDB interface @@ -181,7 +234,7 @@ func (vdb *VersionedDB) ExecuteQuery(query string) (statedb.ResultsIterator, err //TODO - limit is currently set at 1000, eventually this will need to be changed //to reflect a config option and potentially return an exception if the threshold is exceeded // skip (paging) is not utilized by fabric - queryResult, err := vdb.db.QueryDocuments(query, 1000, 0) + queryResult, err := vdb.db.QueryDocuments(string(ApplyQueryWrapper(query)), 1000, 0) if err != nil { logger.Debugf("Error calling QueryDocuments(): %s\n", err.Error()) return nil, err @@ -219,7 +272,7 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version if couchdb.IsJSON(string(vv.Value)) { // SaveDoc using couchdb client and use JSON format - rev, err := vdb.db.SaveDoc(string(compositeKey), "", vv.Value, nil) + rev, err := vdb.db.SaveDoc(string(compositeKey), "", addVersionAndChainCodeID(vv.Value, ns, vv.Version), nil) if err != nil { logger.Errorf("Error during Commit(): %s\n", err.Error()) return err @@ -232,7 +285,7 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version //Create an attachment structure and load the bytes attachment := &couchdb.Attachment{} - attachment.AttachmentBytes = vv.Value + attachment.AttachmentBytes = statedb.EncodeValue(vv.Value, vv.Version) attachment.ContentType = "application/octet-stream" attachment.Name = "valueBytes" @@ -240,7 +293,7 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version attachments = append(attachments, *attachment) // SaveDoc using couchdb client and use attachment to persist the binary data - rev, err := vdb.db.SaveDoc(string(compositeKey), "", nil, attachments) + rev, err := vdb.db.SaveDoc(string(compositeKey), "", addVersionAndChainCodeID(nil, ns, vv.Version), attachments) if err != nil { logger.Errorf("Error during Commit(): %s\n", err.Error()) return err @@ -248,7 +301,6 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version if rev != "" { logger.Debugf("Saved document revision number: %s\n", rev) } - } } } @@ -263,6 +315,33 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version return nil } +//addVersionAndChainCodeID adds keys for version and chaincodeID to the JSON value +func addVersionAndChainCodeID(value []byte, chaincodeID string, version *version.Height) []byte { + + //create a version mapping + jsonMap := map[string]interface{}{"version": fmt.Sprintf("%v:%v", version.BlockNum, version.TxNum)} + + //add the chaincodeID + jsonMap["chaincodeid"] = chaincodeID + + //Add the wrapped data if the value is not null + if value != nil { + + //create a new genericMap + rawJSON := (*json.RawMessage)(&value) + + //add the rawJSON to the map + jsonMap[dataWrapper] = rawJSON + + } + + //marshal the data to a byte array + returnJSON, _ := json.Marshal(jsonMap) + + return returnJSON + +} + // Savepoint docid (key) for couchdb const savepointDocID = "statedb_savepoint" @@ -379,10 +458,12 @@ func (scanner *kvScanner) Next() (statedb.QueryResult, error) { _, key := splitCompositeKey([]byte(selectedKV.ID)) - //TODO - change hardcoded version (1,1) when version header is available in CouchDB + //remove the data wrapper and return the value and version + returnValue, returnVersion := removeDataWrapper(selectedKV.Value) + return &statedb.VersionedKV{ CompositeKey: statedb.CompositeKey{Namespace: scanner.namespace, Key: key}, - VersionedValue: statedb.VersionedValue{Value: selectedKV.Value, Version: version.NewHeight(1, 1)}}, nil + VersionedValue: statedb.VersionedValue{Value: returnValue, Version: &returnVersion}}, nil } func (scanner *kvScanner) Close() { @@ -410,12 +491,14 @@ func (scanner *queryScanner) Next() (statedb.QueryResult, error) { namespace, key := splitCompositeKey([]byte(selectedResultRecord.ID)) - //TODO - change hardcoded version (1,1) when version support is available in CouchDB + //remove the data wrapper and return the value and version + returnValue, returnVersion := removeDataWrapper(selectedResultRecord.Value) + return &statedb.VersionedQueryRecord{ Namespace: namespace, Key: key, - Version: version.NewHeight(1, 1), - Record: selectedResultRecord.Value}, nil + Version: &returnVersion, + Record: returnValue}, nil } func (scanner *queryScanner) Close() { diff --git a/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb_test.go b/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb_test.go index 9b1aece3813..e688e0836c7 100644 --- a/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb_test.go +++ b/core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb_test.go @@ -20,7 +20,9 @@ import ( "os" "testing" + "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/statedb" "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/statedb/commontests" + "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/version" "github.com/hyperledger/fabric/core/ledger/ledgerconfig" "github.com/hyperledger/fabric/core/ledger/testutil" "github.com/spf13/viper" @@ -36,7 +38,6 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -//TODO add wrapper for version in couchdb to resolve final tests in TestBasicRW and TestMultiDBBasicRW func TestBasicRW(t *testing.T) { if ledgerconfig.IsCouchDBEnabled() == true { @@ -81,19 +82,17 @@ func TestIterator(t *testing.T) { } } -/* TODO re-visit after adding version wrapper in couchdb func TestEncodeDecodeValueAndVersion(t *testing.T) { - testValueAndVersionEncodeing(t, []byte("value1"), version.NewHeight(1, 2)) - testValueAndVersionEncodeing(t, []byte{}, version.NewHeight(50, 50)) + testValueAndVersionEncoding(t, []byte("value1"), version.NewHeight(1, 2)) + testValueAndVersionEncoding(t, []byte{}, version.NewHeight(50, 50)) } -func testValueAndVersionEncodeing(t *testing.T, value []byte, version *version.Height) { - encodedValue := encodeValue(value, version) - val, ver := decodeValue(encodedValue) +func testValueAndVersionEncoding(t *testing.T, value []byte, version *version.Height) { + encodedValue := statedb.EncodeValue(value, version) + val, ver := statedb.DecodeValue(encodedValue) testutil.AssertEquals(t, val, value) testutil.AssertEquals(t, ver, version) } -*/ func TestCompositeKey(t *testing.T) { if ledgerconfig.IsCouchDBEnabled() == true { @@ -113,7 +112,6 @@ func testCompositeKey(t *testing.T, ns string, key string) { } // The following tests are unique to couchdb, they are not used in leveldb - // query test func TestQuery(t *testing.T) { if ledgerconfig.IsCouchDBEnabled() == true { diff --git a/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb.go b/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb.go index 2b4b08f9315..713c3032bf3 100644 --- a/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb.go +++ b/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb.go @@ -90,7 +90,7 @@ func (vdb *versionedDB) GetState(namespace string, key string) (*statedb.Version if dbVal == nil { return nil, nil } - val, ver := decodeValue(dbVal) + val, ver := statedb.DecodeValue(dbVal) return &statedb.VersionedValue{Value: val, Version: ver}, nil } @@ -144,7 +144,7 @@ func (vdb *versionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version if vv.Value == nil { dbBatch.Delete(compositeKey) } else { - dbBatch.Put(compositeKey, encodeValue(vv.Value, vv.Version)) + dbBatch.Put(compositeKey, statedb.EncodeValue(vv.Value, vv.Version)) } } } @@ -165,20 +165,6 @@ func (vdb *versionedDB) GetLatestSavePoint() (*version.Height, error) { return version, nil } -func encodeValue(value []byte, version *version.Height) []byte { - encodedValue := version.ToBytes() - if value != nil { - encodedValue = append(encodedValue, value...) - } - return encodedValue -} - -func decodeValue(encodedValue []byte) ([]byte, *version.Height) { - version, n := version.NewHeightFromBytes(encodedValue) - value := encodedValue[n:] - return value, version -} - func constructCompositeKey(ns string, key string) []byte { return append(append([]byte(ns), compositeKeySep...), []byte(key)...) } @@ -206,7 +192,7 @@ func (scanner *kvScanner) Next() (statedb.QueryResult, error) { dbValCopy := make([]byte, len(dbVal)) copy(dbValCopy, dbVal) _, key := splitCompositeKey(dbKey) - value, version := decodeValue(dbValCopy) + value, version := statedb.DecodeValue(dbValCopy) return &statedb.VersionedKV{ CompositeKey: statedb.CompositeKey{Namespace: scanner.namespace, Key: key}, VersionedValue: statedb.VersionedValue{Value: value, Version: version}}, nil diff --git a/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb_test.go b/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb_test.go index 1f40205bf8a..3e784d45041 100644 --- a/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb_test.go +++ b/core/ledger/kvledger/txmgmt/statedb/stateleveldb/stateleveldb_test.go @@ -20,6 +20,7 @@ import ( "os" "testing" + "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/statedb" "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/statedb/commontests" "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/version" "github.com/hyperledger/fabric/core/ledger/testutil" @@ -61,8 +62,8 @@ func TestEncodeDecodeValueAndVersion(t *testing.T) { } func testValueAndVersionEncodeing(t *testing.T, value []byte, version *version.Height) { - encodedValue := encodeValue(value, version) - val, ver := decodeValue(encodedValue) + encodedValue := statedb.EncodeValue(value, version) + val, ver := statedb.DecodeValue(encodedValue) testutil.AssertEquals(t, val, value) testutil.AssertEquals(t, ver, version) } diff --git a/core/ledger/kvledger/txmgmt/statedb/util.go b/core/ledger/kvledger/txmgmt/statedb/util.go new file mode 100644 index 00000000000..23e1c37da7b --- /dev/null +++ b/core/ledger/kvledger/txmgmt/statedb/util.go @@ -0,0 +1,35 @@ +/* +Copyright IBM Corp. 2017 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statedb + +import "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/version" + +//EncodeValue appends the value to the version, allows storage of version and value in binary form +func EncodeValue(value []byte, version *version.Height) []byte { + encodedValue := version.ToBytes() + if value != nil { + encodedValue = append(encodedValue, value...) + } + return encodedValue +} + +//DecodeValue separates the version and value from a binary value +func DecodeValue(encodedValue []byte) ([]byte, *version.Height) { + version, n := version.NewHeightFromBytes(encodedValue) + value := encodedValue[n:] + return value, version +} diff --git a/core/ledger/kvledger/txmgmt/statedb/util_test.go b/core/ledger/kvledger/txmgmt/statedb/util_test.go new file mode 100644 index 00000000000..df1c84ede6a --- /dev/null +++ b/core/ledger/kvledger/txmgmt/statedb/util_test.go @@ -0,0 +1,54 @@ +/* +Copyright IBM Corp. 2016 All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statedb + +import ( + "testing" + + "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/version" + "github.com/hyperledger/fabric/core/ledger/testutil" +) + +// TestEncodeString tests encoding and decoding a string value +func TestEncodeDecodeString(t *testing.T) { + + bytesString1 := []byte("value1") + version1 := version.NewHeight(1, 1) + + encodedValue := EncodeValue(bytesString1, version1) + decodedValue, decodedVersion := DecodeValue(encodedValue) + + testutil.AssertEquals(t, decodedValue, bytesString1) + + testutil.AssertEquals(t, decodedVersion, version1) + +} + +// TestEncodeDecodeJSON tests encoding and decoding a JSON value +func TestEncodeDecodeJSON(t *testing.T) { + + bytesJSON2 := []byte(`{"asset_name":"marble1","color":"blue","size":"35","owner":"jerry"}`) + version2 := version.NewHeight(1, 1) + + encodedValue := EncodeValue(bytesJSON2, version2) + decodedValue, decodedVersion := DecodeValue(encodedValue) + + testutil.AssertEquals(t, decodedValue, bytesJSON2) + + testutil.AssertEquals(t, decodedVersion, version2) + +} diff --git a/core/ledger/kvledger/txmgmt/version/version.go b/core/ledger/kvledger/txmgmt/version/version.go index a28937df566..928d5e14ec7 100644 --- a/core/ledger/kvledger/txmgmt/version/version.go +++ b/core/ledger/kvledger/txmgmt/version/version.go @@ -16,9 +16,7 @@ limitations under the License. package version -import ( - "github.com/hyperledger/fabric/core/ledger/util" -) +import "github.com/hyperledger/fabric/core/ledger/util" // Height represents the height of a transaction in blockchain type Height struct { diff --git a/core/ledger/util/couchdb/couchdb.go b/core/ledger/util/couchdb/couchdb.go index c3df29a8e62..58c815c7349 100644 --- a/core/ledger/util/couchdb/couchdb.go +++ b/core/ledger/util/couchdb/couchdb.go @@ -382,7 +382,7 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, bytesDoc []byte, a } else { // there are attachments //attachments are included, create the multipart definition - multipartData, multipartBoundary, err3 := createAttachmentPart(*data, attachments, defaultBoundary) + multipartData, multipartBoundary, err3 := createAttachmentPart(bytesDoc, attachments, defaultBoundary) if err3 != nil { return "", err3 } @@ -414,10 +414,13 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, bytesDoc []byte, a } -func createAttachmentPart(data bytes.Buffer, attachments []Attachment, defaultBoundary string) (bytes.Buffer, string, error) { +func createAttachmentPart(data []byte, attachments []Attachment, defaultBoundary string) (bytes.Buffer, string, error) { + + //Create a buffer for writing the result + writeBuffer := new(bytes.Buffer) // read the attachment and save as an attachment - writer := multipart.NewWriter(&data) + writer := multipart.NewWriter(writeBuffer) //retrieve the boundary for the multipart defaultBoundary = writer.Boundary() @@ -431,6 +434,21 @@ func createAttachmentPart(data bytes.Buffer, attachments []Attachment, defaultBo attachmentJSONMap := map[string]interface{}{ "_attachments": fileAttachments} + //Add any data uploaded with the files + if data != nil { + + //create a generic map + genericMap := make(map[string]interface{}) + //unmarshal the data into the generic map + json.Unmarshal(data, &genericMap) + + //add all key/values to the attachmentJSONMap + for jsonKey, jsonValue := range genericMap { + attachmentJSONMap[jsonKey] = jsonValue + } + + } + filesForUpload, _ := json.Marshal(attachmentJSONMap) logger.Debugf(string(filesForUpload)) @@ -440,7 +458,7 @@ func createAttachmentPart(data bytes.Buffer, attachments []Attachment, defaultBo part, err := writer.CreatePart(header) if err != nil { - return data, defaultBoundary, err + return *writeBuffer, defaultBoundary, err } part.Write(filesForUpload) @@ -450,7 +468,7 @@ func createAttachmentPart(data bytes.Buffer, attachments []Attachment, defaultBo header := make(textproto.MIMEHeader) part, err2 := writer.CreatePart(header) if err2 != nil { - return data, defaultBoundary, err2 + return *writeBuffer, defaultBoundary, err2 } part.Write(attachment.AttachmentBytes) @@ -458,10 +476,10 @@ func createAttachmentPart(data bytes.Buffer, attachments []Attachment, defaultBo err = writer.Close() if err != nil { - return data, defaultBoundary, err + return *writeBuffer, defaultBoundary, err } - return data, defaultBoundary, nil + return *writeBuffer, defaultBoundary, nil }