github.com/tenywen/fabric@v1.0.0-beta.0.20170620030522-a5b1ed380643/core/ledger/kvledger/txmgmt/statedb/statecouchdb/query_wrapper.go (about)

     1  /*
     2  Copyright IBM Corp. 2016 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 statecouchdb
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"reflect"
    24  )
    25  
    26  const dataWrapper = "data"
    27  const jsonQueryFields = "fields"
    28  const jsonQuerySelector = "selector"
    29  const jsonQueryUseIndex = "use_index"
    30  const jsonQueryLimit = "limit"
    31  const jsonQuerySkip = "skip"
    32  
    33  var validOperators = []string{"$and", "$or", "$not", "$nor", "$all", "$elemMatch",
    34  	"$lt", "$lte", "$eq", "$ne", "$gte", "$gt", "$exits", "$type", "$in", "$nin",
    35  	"$size", "$mod", "$regex"}
    36  
    37  /*
    38  ApplyQueryWrapper parses the query string passed to CouchDB
    39  the wrapper prepends the wrapper "data." to all fields specified in the query
    40  All fields in the selector must have "data." prepended to the field names
    41  Fields listed in fields key will have "data." prepended
    42  Fields in the sort key will have "data." prepended
    43  
    44  - The query will be scoped to the chaincodeid
    45  
    46  - limit be added to the query and is based on config
    47  - skip is defaulted to 0 and is currently not used, this is for future paging implementation
    48  
    49  In the example a contextID of "marble" is assumed.
    50  
    51  Example:
    52  
    53  Source Query:
    54  {"selector":{"owner": {"$eq": "tom"}},
    55  "fields": ["owner", "asset_name", "color", "size"],
    56  "sort": ["size", "color"]}
    57  
    58  Result Wrapped Query:
    59  {"selector":{"$and":[{"chaincodeid":"marble"},{"data.owner":{"$eq":"tom"}}]},
    60  "fields": ["data.owner","data.asset_name","data.color","data.size","_id","version"],
    61  "sort":["data.size","data.color"],"limit":10,"skip":0}
    62  
    63  */
    64  func ApplyQueryWrapper(namespace, queryString string, queryLimit, querySkip int) (string, error) {
    65  
    66  	//create a generic map for the query json
    67  	jsonQueryMap := make(map[string]interface{})
    68  
    69  	//unmarshal the selected json into the generic map
    70  	decoder := json.NewDecoder(bytes.NewBuffer([]byte(queryString)))
    71  	decoder.UseNumber()
    72  	err := decoder.Decode(&jsonQueryMap)
    73  	if err != nil {
    74  		return "", err
    75  	}
    76  
    77  	//traverse through the json query and wrap any field names
    78  	processAndWrapQuery(jsonQueryMap)
    79  
    80  	//if "fields" are specified in the query, then add the "_id", "version" and "chaincodeid" fields
    81  	if jsonValue, ok := jsonQueryMap[jsonQueryFields]; ok {
    82  		//check to see if this is an interface map
    83  		if reflect.TypeOf(jsonValue).String() == "[]interface {}" {
    84  
    85  			//Add the "_id", "version" and "chaincodeid" fields,  these are needed by default
    86  			jsonQueryMap[jsonQueryFields] = append(jsonValue.([]interface{}),
    87  				"_id", "version", "chaincodeid")
    88  		}
    89  	}
    90  
    91  	//Check to see if the "selector" is specified in the query
    92  	if jsonValue, ok := jsonQueryMap[jsonQuerySelector]; ok {
    93  		//if the "selector" is found, then add the "$and" clause and the namespace filter
    94  		setNamespaceInSelector(namespace, jsonValue, jsonQueryMap)
    95  	} else {
    96  		//if the "selector" is not found, then add a default namespace filter
    97  		setDefaultNamespaceInSelector(namespace, jsonQueryMap)
    98  	}
    99  
   100  	//Add limit
   101  	jsonQueryMap[jsonQueryLimit] = queryLimit
   102  
   103  	//Add skip
   104  	jsonQueryMap[jsonQuerySkip] = querySkip
   105  
   106  	//Marshal the updated json query
   107  	editedQuery, _ := json.Marshal(jsonQueryMap)
   108  
   109  	logger.Debugf("Rewritten query with data wrapper: %s", editedQuery)
   110  
   111  	return string(editedQuery), nil
   112  
   113  }
   114  
   115  //setNamespaceInSelector adds an additional hierarchy in the "selector"
   116  //{"owner": {"$eq": "tom"}}
   117  //would be mapped as (assuming a namespace of "marble"):
   118  //{"$and":[{"chaincodeid":"marble"},{"data.owner":{"$eq":"tom"}}]}
   119  func setNamespaceInSelector(namespace, jsonValue interface{},
   120  	jsonQueryMap map[string]interface{}) {
   121  
   122  	//create a array to store the parts of the query
   123  	var queryParts = make([]interface{}, 0)
   124  
   125  	//Add the namespace filter to filter on the chaincodeid
   126  	namespaceFilter := make(map[string]interface{})
   127  	namespaceFilter["chaincodeid"] = namespace
   128  
   129  	//Add the context filter and the existing selector value
   130  	queryParts = append(queryParts, namespaceFilter, jsonValue)
   131  
   132  	//Create a new mapping for the new query structure
   133  	mappedSelector := make(map[string]interface{})
   134  
   135  	//Specify the "$and" operator for the parts of the query
   136  	mappedSelector["$and"] = queryParts
   137  
   138  	//Set the new mapped selector to the query selector
   139  	jsonQueryMap[jsonQuerySelector] = mappedSelector
   140  
   141  }
   142  
   143  //setDefaultNamespaceInSelector adds an default namespace filter in "selector"
   144  //If no selector is specified, the following is mapped to the "selector"
   145  //assuming a namespace of "marble"
   146  //{"chaincodeid":"marble"}
   147  func setDefaultNamespaceInSelector(namespace string, jsonQueryMap map[string]interface{}) {
   148  
   149  	//Add the context filter to filter on the chaincodeid
   150  	namespaceFilter := make(map[string]interface{})
   151  	namespaceFilter["chaincodeid"] = namespace
   152  
   153  	//Set the new mapped selector to the query selector
   154  	jsonQueryMap[jsonQuerySelector] = namespaceFilter
   155  }
   156  
   157  func processAndWrapQuery(jsonQueryMap map[string]interface{}) {
   158  
   159  	//iterate through the JSON query
   160  	for jsonKey, jsonValue := range jsonQueryMap {
   161  
   162  		//create a case for the data types found in the JSON query
   163  		switch jsonValueType := jsonValue.(type) {
   164  
   165  		case string:
   166  			//intercept the string case and prevent the []interface{} case from
   167  			//incorrectly processing the string
   168  
   169  		case float64:
   170  			//intercept the float64 case and prevent the []interface{} case from
   171  			//incorrectly processing the float64
   172  
   173  		case json.Number:
   174  			//intercept the Number case and prevent the []interface{} case from
   175  			//incorrectly processing the float64
   176  
   177  		//if the type is an array, then iterate through the items
   178  		case []interface{}:
   179  
   180  			//iterate the items in the array
   181  			for itemKey, itemValue := range jsonValueType {
   182  
   183  				switch itemValue.(type) {
   184  
   185  				case string:
   186  
   187  					//if this is not "use_index", the wrap the string
   188  					if jsonKey != jsonQueryUseIndex {
   189  						//This is a simple string, so wrap the field and replace in the array
   190  						jsonValueType[itemKey] = fmt.Sprintf("%v.%v", dataWrapper, itemValue)
   191  					}
   192  
   193  				case []interface{}:
   194  
   195  					//This is a array, so traverse to the next level
   196  					processAndWrapQuery(itemValue.(map[string]interface{}))
   197  
   198  				case interface{}:
   199  
   200  					//process this part as a map
   201  					processInterfaceMap(itemValue.(map[string]interface{}))
   202  
   203  				}
   204  			}
   205  
   206  		case interface{}:
   207  
   208  			//process this part as a map
   209  			processInterfaceMap(jsonValue.(map[string]interface{}))
   210  
   211  		}
   212  	}
   213  }
   214  
   215  //processInterfaceMap processes an interface map and wraps field names or traverses
   216  //the next level of the json query
   217  func processInterfaceMap(jsonFragment map[string]interface{}) {
   218  
   219  	//create a copy of the jsonFragment for iterating
   220  	var bufferFragment = make(map[string]interface{})
   221  	for keyVal, itemVal := range jsonFragment {
   222  		bufferFragment[keyVal] = itemVal
   223  	}
   224  
   225  	//iterate the item in the map
   226  	for keyVal, itemVal := range bufferFragment {
   227  
   228  		//check to see if the key is an operator
   229  		if arrayContains(validOperators, keyVal) {
   230  
   231  			//if this is an operator, traverse the next level of the json query
   232  			processAndWrapQuery(jsonFragment)
   233  
   234  		} else {
   235  
   236  			//if this is not an operator, this is a field name and needs to be wrapped
   237  			wrapFieldName(jsonFragment, keyVal, itemVal)
   238  
   239  		}
   240  	}
   241  }
   242  
   243  //wrapFieldName "wraps" the field name with the data wrapper, and replaces the key in the json fragment
   244  func wrapFieldName(jsonFragment map[string]interface{}, key string, value interface{}) {
   245  
   246  	//delete the mapping for the field definition, since we have to change the
   247  	//value of the key
   248  	delete(jsonFragment, key)
   249  
   250  	//add the key back into the map with the field name wrapped with then "data" wrapper
   251  	jsonFragment[fmt.Sprintf("%v.%v", dataWrapper, key)] = value
   252  
   253  }
   254  
   255  //arrayContains is a function to detect if a soure array of strings contains the selected string
   256  //for this application, it is used to determine if a string is a valid CouchDB operator
   257  func arrayContains(sourceArray []string, selectItem string) bool {
   258  	set := make(map[string]struct{}, len(sourceArray))
   259  	for _, s := range sourceArray {
   260  		set[s] = struct{}{}
   261  	}
   262  	_, ok := set[selectItem]
   263  	return ok
   264  }