Skip to content

Commit 851cb6a

Browse files
author
Chris Elder
committed
[FAB-11308] Allow use of + char in namespace
The CouchDB implementation currently does not allow + chars in the database name. Add support in the CouchDB implementation layer for the + char be included as part of the database name. Add unit tests for databases containing a + char. Change-Id: I59001d1aed19066d65f6e230f41958cd2a8cbd73 Signed-off-by: Chris Elder <chris.elder@us.ibm.com>
1 parent a8eee90 commit 851cb6a

File tree

3 files changed

+72
-35
lines changed

3 files changed

+72
-35
lines changed

core/ledger/util/couchdb/couchdb.go

Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() error {
291291
logger.Errorf("URL parse error: %s", err)
292292
return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
293293
}
294-
connectURL.Path = dbclient.DBName
294+
connectURL = constructCouchDBUrl(connectURL, dbclient.DBName, "")
295295

296296
//get the number of retries
297297
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -367,7 +367,7 @@ func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) {
367367
logger.Errorf("URL parse error: %s", err)
368368
return nil, nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
369369
}
370-
connectURL.Path = dbclient.DBName
370+
connectURL = constructCouchDBUrl(connectURL, dbclient.DBName, "")
371371

372372
//get the number of retries
373373
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -456,7 +456,7 @@ func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) {
456456
logger.Errorf("URL parse error: %s", err)
457457
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
458458
}
459-
connectURL.Path = dbclient.DBName
459+
connectURL = constructCouchDBUrl(connectURL, dbclient.DBName, "")
460460

461461
//get the number of retries
462462
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -499,7 +499,7 @@ func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error)
499499
logger.Errorf("URL parse error: %s", err)
500500
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
501501
}
502-
connectURL.Path = dbclient.DBName + "/_ensure_full_commit"
502+
connectURL = constructCouchDBUrl(connectURL, dbclient.DBName, "_ensure_full_commit")
503503

504504
//get the number of retries
505505
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -562,9 +562,7 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
562562
return "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
563563
}
564564

565-
saveURL.Path = dbclient.DBName
566-
// id can contain a '/', so encode separately
567-
saveURL = &url.URL{Opaque: saveURL.String() + "/" + encodePathElement(id)}
565+
saveURL = constructCouchDBUrl(saveURL, dbclient.DBName, id)
568566

569567
logger.Debugf(" rev=%s", rev)
570568

@@ -616,7 +614,7 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc
616614

617615
//handle the request for saving document with a retry if there is a revision conflict
618616
resp, _, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodPut,
619-
*saveURL, data, rev, defaultBoundary, maxRetries, keepConnectionOpen)
617+
saveURL.String(), data, rev, defaultBoundary, maxRetries, keepConnectionOpen)
620618

621619
if err != nil {
622620
return "", err
@@ -762,9 +760,8 @@ func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) {
762760
logger.Errorf("URL parse error: %s", err)
763761
return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
764762
}
765-
readURL.Path = dbclient.DBName
766-
// id can contain a '/', so encode separately
767-
readURL = &url.URL{Opaque: readURL.String() + "/" + encodePathElement(id)}
763+
764+
readURL = constructCouchDBUrl(readURL, dbclient.DBName, id)
768765

769766
query := readURL.Query()
770767
query.Add("attachments", "true")
@@ -895,7 +892,7 @@ func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip
895892
logger.Errorf("URL parse error: %s", err)
896893
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
897894
}
898-
rangeURL.Path = dbclient.DBName + "/_all_docs"
895+
rangeURL = constructCouchDBUrl(rangeURL, dbclient.DBName, "_all_docs")
899896

900897
queryParms := rangeURL.Query()
901898
queryParms.Set("limit", strconv.Itoa(limit))
@@ -1001,17 +998,14 @@ func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error {
1001998
logger.Errorf("URL parse error: %s", err)
1002999
return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
10031000
}
1004-
1005-
deleteURL.Path = dbclient.DBName
1006-
// id can contain a '/', so encode separately
1007-
deleteURL = &url.URL{Opaque: deleteURL.String() + "/" + encodePathElement(id)}
1001+
deleteURL = constructCouchDBUrl(deleteURL, dbclient.DBName, id)
10081002

10091003
//get the number of retries
10101004
maxRetries := dbclient.CouchInstance.conf.MaxRetries
10111005

10121006
//handle the request for saving document with a retry if there is a revision conflict
10131007
resp, couchDBReturn, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodDelete,
1014-
*deleteURL, nil, "", "", maxRetries, true)
1008+
deleteURL.String(), nil, "", "", maxRetries, true)
10151009

10161010
if err != nil {
10171011
if couchDBReturn != nil && couchDBReturn.StatusCode == 404 {
@@ -1042,8 +1036,7 @@ func (dbclient *CouchDatabase) QueryDocuments(query string) (*[]QueryResult, err
10421036
logger.Errorf("URL parse error: %s", err)
10431037
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
10441038
}
1045-
1046-
queryURL.Path = dbclient.DBName + "/_find"
1039+
queryURL = constructCouchDBUrl(queryURL, dbclient.DBName, "_find")
10471040

10481041
//get the number of retries
10491042
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -1132,8 +1125,7 @@ func (dbclient *CouchDatabase) ListIndex() ([]*IndexResult, error) {
11321125
logger.Errorf("URL parse error: %s", err)
11331126
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
11341127
}
1135-
1136-
indexURL.Path = dbclient.DBName + "/_index/"
1128+
indexURL = constructCouchDBUrl(indexURL, dbclient.DBName, "_index")
11371129

11381130
//get the number of retries
11391131
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -1196,8 +1188,7 @@ func (dbclient *CouchDatabase) CreateIndex(indexdefinition string) (*CreateIndex
11961188
logger.Errorf("URL parse error: %s", err)
11971189
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
11981190
}
1199-
1200-
indexURL.Path = dbclient.DBName + "/_index"
1191+
indexURL = constructCouchDBUrl(indexURL, dbclient.DBName, "_index")
12011192

12021193
//get the number of retries
12031194
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -1251,8 +1242,7 @@ func (dbclient *CouchDatabase) DeleteIndex(designdoc, indexname string) error {
12511242
logger.Errorf("URL parse error: %s", err)
12521243
return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
12531244
}
1254-
1255-
indexURL.Path = dbclient.DBName + "/_index/" + designdoc + "/json/" + indexname
1245+
indexURL = constructCouchDBUrl(indexURL, dbclient.DBName, "_index", designdoc, "json", indexname)
12561246

12571247
//get the number of retries
12581248
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -1279,7 +1269,7 @@ func (dbclient *CouchDatabase) WarmIndex(designdoc, indexname string) error {
12791269
}
12801270

12811271
//URL to execute the view function associated with the index
1282-
indexURL.Path = dbclient.DBName + "/_design/" + designdoc + "/_view/" + indexname
1272+
indexURL = constructCouchDBUrl(indexURL, dbclient.DBName, "_design", designdoc, "_view", indexname)
12831273

12841274
queryParms := indexURL.Query()
12851275
//Query parameter that allows the execution of the URL to return immediately
@@ -1347,8 +1337,7 @@ func (dbclient *CouchDatabase) GetDatabaseSecurity() (*DatabaseSecurity, error)
13471337
logger.Errorf("URL parse error: %s", err)
13481338
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
13491339
}
1350-
1351-
securityURL.Path = dbclient.DBName + "/_security"
1340+
securityURL = constructCouchDBUrl(securityURL, dbclient.DBName, "_security")
13521341

13531342
//get the number of retries
13541343
maxRetries := dbclient.CouchInstance.conf.MaxRetries
@@ -1390,8 +1379,7 @@ func (dbclient *CouchDatabase) ApplyDatabaseSecurity(databaseSecurity *DatabaseS
13901379
logger.Errorf("URL parse error: %s", err)
13911380
return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
13921381
}
1393-
1394-
securityURL.Path = dbclient.DBName + "/_security"
1382+
securityURL = constructCouchDBUrl(securityURL, dbclient.DBName, "_security")
13951383

13961384
//Ensure all of the arrays are initialized to empty arrays instead of nil
13971385
if databaseSecurity.Admins.Names == nil {
@@ -1441,7 +1429,7 @@ func (dbclient *CouchDatabase) BatchRetrieveDocumentMetadata(keys []string) ([]*
14411429
logger.Errorf("URL parse error: %s", err)
14421430
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
14431431
}
1444-
batchRetrieveURL.Path = dbclient.DBName + "/_all_docs"
1432+
batchRetrieveURL = constructCouchDBUrl(batchRetrieveURL, dbclient.DBName, "_all_docs")
14451433

14461434
queryParms := batchRetrieveURL.Query()
14471435

@@ -1521,7 +1509,7 @@ func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*B
15211509
logger.Errorf("URL parse error: %s", err)
15221510
return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.CouchInstance.conf.URL)
15231511
}
1524-
batchUpdateURL.Path = dbclient.DBName + "/_bulk_docs"
1512+
batchUpdateURL = constructCouchDBUrl(batchUpdateURL, dbclient.DBName, "_bulk_docs")
15251513

15261514
documentMap := make(map[string]interface{})
15271515

@@ -1606,7 +1594,7 @@ func (dbclient *CouchDatabase) BatchUpdateDocuments(documents []*CouchDoc) ([]*B
16061594
//a retry for document revision conflict errors,
16071595
//which may be detected during saves or deletes that timed out from client http perspective,
16081596
//but which eventually succeeded in couchdb
1609-
func (dbclient *CouchDatabase) handleRequestWithRevisionRetry(id, method string, connectURL url.URL, data []byte, rev string,
1597+
func (dbclient *CouchDatabase) handleRequestWithRevisionRetry(id, method, connectURL string, data []byte, rev string,
16101598
multipartBoundary string, maxRetries int, keepConnectionOpen bool) (*http.Response, *DBReturn, error) {
16111599

16121600
//Initialize a flag for the revision conflict
@@ -1627,7 +1615,7 @@ func (dbclient *CouchDatabase) handleRequestWithRevisionRetry(id, method string,
16271615
}
16281616

16291617
//handle the request for saving/deleting the couchdb data
1630-
resp, couchDBReturn, errResp = dbclient.CouchInstance.handleRequest(method, connectURL.String(),
1618+
resp, couchDBReturn, errResp = dbclient.CouchInstance.handleRequest(method, connectURL,
16311619
data, rev, multipartBoundary, maxRetries, keepConnectionOpen)
16321620

16331621
//If there was a 409 conflict error during the save/delete, log it and retry it.

core/ledger/util/couchdb/couchdb_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"encoding/json"
1111
"fmt"
1212
"net/http"
13+
"net/url"
1314
"os"
1415
"strings"
1516
"testing"
@@ -1624,3 +1625,35 @@ func TestDatabaseSecuritySettings(t *testing.T) {
16241625
testutil.AssertEquals(t, len(databaseSecurity.Members.Names), 0)
16251626

16261627
}
1628+
1629+
func TestURLWithSpecialCharacters(t *testing.T) {
1630+
1631+
database := "testdb+with+plus_sign"
1632+
err := cleanup(database)
1633+
testutil.AssertNoError(t, err, fmt.Sprintf("Error when trying to cleanup Error: %s", err))
1634+
defer cleanup(database)
1635+
1636+
// parse a contructed URL
1637+
finalURL, err := url.Parse("http://127.0.0.1:5984")
1638+
testutil.AssertNoError(t, err, "error thrown while parsing couchdb url")
1639+
1640+
// test the constructCouchDBUrl function with multiple path elements
1641+
couchdbURL := constructCouchDBUrl(finalURL, database, "_index", "designdoc", "json", "indexname")
1642+
testutil.AssertEquals(t, couchdbURL.String(), "http://127.0.0.1:5984/testdb%2Bwith%2Bplus_sign/_index/designdoc/json/indexname")
1643+
1644+
//create a new instance and database object --------------------------------------------------------
1645+
couchInstance, err := CreateCouchInstance(couchDBDef.URL, couchDBDef.Username, couchDBDef.Password,
1646+
couchDBDef.MaxRetries, couchDBDef.MaxRetriesOnStartup, couchDBDef.RequestTimeout)
1647+
testutil.AssertNoError(t, err, fmt.Sprintf("Error when trying to create couch instance"))
1648+
db := CouchDatabase{CouchInstance: couchInstance, DBName: database}
1649+
1650+
//create a new database
1651+
errdb := db.CreateDatabaseIfNotExist()
1652+
testutil.AssertNoError(t, errdb, fmt.Sprintf("Error when trying to create database"))
1653+
1654+
dbInfo, _, errInfo := db.GetDatabaseInfo()
1655+
testutil.AssertNoError(t, errInfo, fmt.Sprintf("Error when trying to get database info"))
1656+
1657+
testutil.AssertEquals(t, dbInfo.DbName, database)
1658+
1659+
}

core/ledger/util/couchdb/couchdbutil.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ SPDX-License-Identifier: Apache-2.0
66
package couchdb
77

88
import (
9+
"bytes"
910
"encoding/hex"
1011
"net/http"
12+
"net/url"
1113
"regexp"
1214
"strconv"
1315
"strings"
@@ -17,7 +19,7 @@ import (
1719
"github.com/pkg/errors"
1820
)
1921

20-
var expectedDatabaseNamePattern = `[a-z][a-z0-9.$_()-]*`
22+
var expectedDatabaseNamePattern = `[a-z][a-z0-9.$_()+-]*`
2123
var maxLength = 238
2224

2325
// To restrict the length of couchDB database name to the
@@ -138,6 +140,20 @@ func CreateSystemDatabasesIfNotExist(couchInstance *CouchInstance) error {
138140

139141
}
140142

143+
// constructCouchDBUrl constructs a couchDB url with encoding for the database name
144+
// and all path elements
145+
func constructCouchDBUrl(connectURL *url.URL, dbName string, pathElements ...string) *url.URL {
146+
var buffer bytes.Buffer
147+
buffer.WriteString(connectURL.String())
148+
buffer.WriteString("/")
149+
buffer.WriteString(encodePathElement(dbName))
150+
for _, pathElement := range pathElements {
151+
buffer.WriteString("/")
152+
buffer.WriteString(encodePathElement(pathElement))
153+
}
154+
return &url.URL{Opaque: buffer.String()}
155+
}
156+
141157
// ConstructMetadataDBName truncates the db name to couchdb allowed length to
142158
// construct the metadataDBName
143159
func ConstructMetadataDBName(dbName string) string {

0 commit comments

Comments
 (0)