SEBA-667 Implement model filtering

Change-Id: Idfb15da5092e33833e7930dc2325dab2d6d71025
diff --git a/commands/backup.go b/commands/backup.go
index 3ee7abe..db6e51a 100644
--- a/commands/backup.go
+++ b/commands/backup.go
@@ -223,7 +223,8 @@
 	// STEP 4: Wait for completion
 
 	flags := GM_UNTIL_ENACTED | GM_UNTIL_FOUND | GM_UNTIL_STATUS | Ternary_uint32(options.Quiet, GM_QUIET, 0)
-	conn, completed_backupop, err := FindModelWithRetry(conn, descriptor, "BackupOperation", "uuid", backupop["uuid"].(string), flags)
+	queries := map[string]string{"uuid": backupop["uuid"].(string)}
+	conn, completed_backupop, err := FindModelWithRetry(conn, descriptor, "BackupOperation", queries, flags)
 	if err != nil {
 		return err
 	}
diff --git a/commands/funcmap.go b/commands/funcmap.go
index 0342653..7a2bdf2 100644
--- a/commands/funcmap.go
+++ b/commands/funcmap.go
@@ -69,7 +69,12 @@
 	return desc, name, nil
 }
 
-func GetEnumValue(val *dynamic.Message, name string) string {
-	return val.FindFieldDescriptorByName(name).GetEnumType().
-		FindValueByNumber(val.GetFieldByName(name).(int32)).GetName()
+func GetEnumValue(msg *dynamic.Message, name string) string {
+	return msg.FindFieldDescriptorByName(name).GetEnumType().
+		FindValueByNumber(msg.GetFieldByName(name).(int32)).GetName()
+}
+
+func SetEnumValue(msg *dynamic.Message, name string, value string) {
+	eValue := msg.FindFieldDescriptorByName(name).GetEnumType().FindValueByName(value)
+	msg.SetFieldByName(name, eValue.GetNumber())
 }
diff --git a/commands/models.go b/commands/models.go
index f7a1963..16e4dc4 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -17,9 +17,7 @@
 package commands
 
 import (
-	"context"
 	"fmt"
-	"github.com/fullstorydev/grpcurl"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/jhump/protoreflect/dynamic"
 	"github.com/opencord/cordctl/format"
@@ -35,9 +33,10 @@
 
 type ModelList struct {
 	OutputOptions
-	ShowHidden      bool `long:"showhidden" description:"Show hidden fields in default output"`
-	ShowFeedback    bool `long:"showfeedback" description:"Show feedback fields in default output"`
-	ShowBookkeeping bool `long:"showbookkeeping" description:"Show bookkeeping fields in default output"`
+	ShowHidden      bool   `long:"showhidden" description:"Show hidden fields in default output"`
+	ShowFeedback    bool   `long:"showfeedback" description:"Show feedback fields in default output"`
+	ShowBookkeeping bool   `long:"showbookkeeping" description:"Show bookkeeping fields in default output"`
+	Filter          string `long:"filter" description:"Comma-separated list of filters"`
 	Args            struct {
 		ModelName ModelNameString
 	} `positional-args:"yes" required:"yes"`
@@ -107,37 +106,25 @@
 		return err
 	}
 
-	method := "xos.xos/List" + string(options.Args.ModelName)
+	var models []*dynamic.Message
 
-	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
-	defer cancel()
-
-	headers := GenerateHeaders()
-
-	h := &RpcEventHandler{}
-	err = grpcurl.InvokeRPC(ctx, descriptor, conn, method, headers, h, h.GetParams)
+	queries, err := CommaSeparatedQueryToMap(options.Filter)
 	if err != nil {
 		return err
 	}
 
-	if h.Status != nil && h.Status.Err() != nil {
-		return h.Status.Err()
+	if len(queries) == 0 {
+		models, err = ListModels(conn, descriptor, string(options.Args.ModelName))
+	} else {
+		models, err = FilterModels(conn, descriptor, string(options.Args.ModelName), queries)
 	}
-
-	d, err := dynamic.AsDynamicMessage(h.Response)
-	if err != nil {
-		return err
-	}
-
-	items, err := d.TryGetFieldByName("items")
 	if err != nil {
 		return err
 	}
 
 	field_names := make(map[string]bool)
-	data := make([]map[string]interface{}, len(items.([]interface{})))
-	for i, item := range items.([]interface{}) {
-		val := item.(*dynamic.Message)
+	data := make([]map[string]interface{}, len(models))
+	for i, val := range models {
 		data[i] = make(map[string]interface{})
 		for _, field_desc := range val.GetKnownFields() {
 			field_name := field_desc.GetName()
diff --git a/commands/orm.go b/commands/orm.go
index 8788e8a..8a4108e 100644
--- a/commands/orm.go
+++ b/commands/orm.go
@@ -21,17 +21,134 @@
 	"errors"
 	"fmt"
 	"github.com/fullstorydev/grpcurl"
+	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/protoc-gen-go/descriptor"
+	"github.com/jhump/protoreflect/desc"
 	"github.com/jhump/protoreflect/dynamic"
 	"google.golang.org/grpc"
+	"io"
+	"strconv"
 	"strings"
 	"time"
 )
 
+// Flags for calling the *WithRetry methods
 const GM_QUIET = 1
 const GM_UNTIL_FOUND = 2
 const GM_UNTIL_ENACTED = 4
 const GM_UNTIL_STATUS = 8
 
+type QueryEventHandler struct {
+	RpcEventHandler
+	Elements map[string]string
+	Model    *desc.MessageDescriptor
+	Kind     string
+	EOF      bool
+}
+
+// Separate the operator from the query value.
+// For example,
+//    "==foo"  --> "EQUAL", "foo"
+func DecodeOperator(query string) (string, string, bool, error) {
+	if strings.HasPrefix(query, "!=") {
+		return query[2:], "EQUAL", true, nil
+	} else if strings.HasPrefix(query, "==") {
+		return "", "", false, errors.New("Operator == is now allowed. Suggest using = instead.")
+	} else if strings.HasPrefix(query, "=") {
+		return query[1:], "EQUAL", false, nil
+	} else if strings.HasPrefix(query, ">=") {
+		return query[2:], "GREATER_THAN_OR_EQUAL", false, nil
+	} else if strings.HasPrefix(query, ">") {
+		return query[1:], "GREATER_THAN", false, nil
+	} else if strings.HasPrefix(query, "<=") {
+		return query[2:], "LESS_THAN_OR_EQUAL", false, nil
+	} else if strings.HasPrefix(query, "<") {
+		return query[1:], "LESS_THAN", false, nil
+	} else {
+		return query, "EQUAL", false, nil
+	}
+}
+
+// Generate the parameters for Query messages.
+func (h *QueryEventHandler) GetParams(msg proto.Message) error {
+	dmsg, err := dynamic.AsDynamicMessage(msg)
+	if err != nil {
+		return err
+	}
+
+	//fmt.Printf("MessageName: %s\n", dmsg.XXX_MessageName())
+
+	if h.EOF {
+		return io.EOF
+	}
+
+	// Get the MessageType for the `elements` field
+	md := dmsg.GetMessageDescriptor()
+	elements_fld := md.FindFieldByName("elements")
+	elements_mt := elements_fld.GetMessageType()
+
+	for field_name, element := range h.Elements {
+		value, operator, invert, err := DecodeOperator(element)
+		if err != nil {
+			return err
+		}
+
+		nm := dynamic.NewMessage(elements_mt)
+
+		field_type := h.Model.FindFieldByName(field_name).GetType()
+		if field_type == descriptor.FieldDescriptorProto_TYPE_INT32 {
+			i, _ := strconv.ParseInt(value, 10, 32)
+			nm.SetFieldByName("iValue", int32(i))
+		} else if field_type == descriptor.FieldDescriptorProto_TYPE_UINT32 {
+			i, _ := strconv.ParseInt(value, 10, 32)
+			nm.SetFieldByName("iValue", uint32(i))
+		} else {
+			nm.SetFieldByName("sValue", value)
+		}
+
+		nm.SetFieldByName("name", field_name)
+		nm.SetFieldByName("invert", invert)
+		SetEnumValue(nm, "operator", operator)
+		dmsg.AddRepeatedFieldByName("elements", nm)
+	}
+
+	SetEnumValue(dmsg, "kind", h.Kind)
+
+	h.EOF = true
+
+	return nil
+}
+
+// Take a string list of queries and turns it into a map of queries
+func QueryStringsToMap(query_args []string) (map[string]string, error) {
+	queries := make(map[string]string)
+	for _, query_str := range query_args {
+		query_str := strings.Trim(query_str, " ")
+		operator_pos := -1
+		for i, ch := range query_str {
+			if (ch == '!') || (ch == '=') || (ch == '>') || (ch == '<') {
+				operator_pos = i
+				break
+			}
+		}
+		if operator_pos == -1 {
+			return nil, fmt.Errorf("Illegal query string %s", query_str)
+		}
+		queries[query_str[:operator_pos]] = query_str[operator_pos:]
+	}
+	return queries, nil
+}
+
+// Take a string of comma-separated queries and turn it into a map of queries
+func CommaSeparatedQueryToMap(query_str string) (map[string]string, error) {
+	if query_str == "" {
+		return nil, nil
+	}
+
+	query_strings := strings.Split(query_str, ",")
+	return QueryStringsToMap(query_strings)
+}
+
 // Return a list of all available model names
 func GetModelNames(source grpcurl.DescriptorSource) (map[string]bool, error) {
 	models := make(map[string]bool)
@@ -181,15 +298,21 @@
 	}
 }
 
-// Get a model from XOS given a fieldName/fieldValue
-func FindModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fieldName string, fieldValue string) (*dynamic.Message, error) {
+func ItemsToDynamicMessageList(items interface{}) []*dynamic.Message {
+	result := make([]*dynamic.Message, len(items.([]interface{})))
+	for i, item := range items.([]interface{}) {
+		result[i] = item.(*dynamic.Message)
+	}
+	return result
+}
+
+// List all objects of a given model
+func ListModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string) ([]*dynamic.Message, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
 	defer cancel()
 
 	headers := GenerateHeaders()
 
-	// TODO(smbaker): Implement filter the right way
-
 	h := &RpcEventHandler{}
 	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.List"+modelName, headers, h, h.GetParams)
 	if err != nil {
@@ -210,20 +333,74 @@
 		return nil, err
 	}
 
-	for _, item := range items.([]interface{}) {
-		val := item.(*dynamic.Message)
+	return ItemsToDynamicMessageList(items), nil
+}
 
-		if val.GetFieldByName(fieldName).(string) == fieldValue {
-			return val, nil
-		}
+// Filter models based on field values
+//   queries is a map of <field_name> to <operator><query>
+//   For example,
+//     map[string]string{"name": "==mysite"}
+func FilterModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
 
+	headers := GenerateHeaders()
+
+	model_descriptor, err := descriptor.FindSymbol("xos." + modelName)
+	if err != nil {
+		return nil, err
+	}
+	model_md, ok := model_descriptor.(*desc.MessageDescriptor)
+	if !ok {
+		return nil, errors.New("Failed to convert model to a messagedescriptor")
 	}
 
-	return nil, errors.New("rpc error: code = NotFound")
+	h := &QueryEventHandler{
+		RpcEventHandler: RpcEventHandler{
+			Fields: map[string]map[string]interface{}{"xos.Query": map[string]interface{}{"kind": 0}},
+		},
+		Elements: queries,
+		Model:    model_md,
+		Kind:     "DEFAULT",
+	}
+	err = grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Filter"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return nil, err
+	}
+
+	if h.Status != nil && h.Status.Err() != nil {
+		return nil, h.Status.Err()
+	}
+
+	d, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return nil, err
+	}
+
+	items, err := d.TryGetFieldByName("items")
+	if err != nil {
+		return nil, err
+	}
+
+	return ItemsToDynamicMessageList(items), nil
+}
+
+// Get a model from XOS given a fieldName/fieldValue
+func FindModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) (*dynamic.Message, error) {
+	models, err := FilterModels(conn, descriptor, modelName, queries)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(models) == 0 {
+		return nil, errors.New("rpc error: code = NotFound")
+	}
+
+	return models[0], nil
 }
 
 // Find a model, but retry under a variety of circumstances
-func FindModelWithRetry(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fieldName string, fieldValue string, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
+func FindModelWithRetry(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string, flags uint32) (*grpc.ClientConn, *dynamic.Message, error) {
 	quiet := (flags & GM_QUIET) != 0
 	until_found := (flags & GM_UNTIL_FOUND) != 0
 	until_enacted := (flags & GM_UNTIL_ENACTED) != 0
@@ -239,7 +416,7 @@
 			}
 		}
 
-		model, err := FindModel(conn, descriptor, modelName, fieldName, fieldValue)
+		model, err := FindModel(conn, descriptor, modelName, queries)
 		if err != nil {
 			if strings.Contains(err.Error(), "rpc error: code = Unavailable") ||
 				strings.Contains(err.Error(), "rpc error: code = Internal desc = stream terminated by RST_STREAM") {
@@ -282,6 +459,8 @@
 	}
 }
 
+// Takes a *dynamic.Message and turns it into a map of fields to interfaces
+//    TODO: Might be more useful to convert the values to strings and ints
 func MessageToMap(d *dynamic.Message) map[string]interface{} {
 	fields := make(map[string]interface{})
 	for _, field_desc := range d.GetKnownFields() {
@@ -291,6 +470,7 @@
 	return fields
 }
 
+// Returns True if a message has been enacted
 func IsEnacted(d *dynamic.Message) bool {
 	enacted := d.GetFieldByName("enacted").(float64)
 	updated := d.GetFieldByName("updated").(float64)