diff --git a/utils/consts.go b/utils/consts.go index 802749ae9..9ce413acb 100755 --- a/utils/consts.go +++ b/utils/consts.go @@ -525,6 +525,9 @@ const ( Event = "Event" EmptyString = "" AgentRequest = "AgentRequest" + DynamicDataPrefix = "~" + AttrValueSep = "=" + ANDSep = "&" ) // Migrator Action diff --git a/utils/dataconverter.go b/utils/dataconverter.go index 374b494c8..1f6f7e084 100644 --- a/utils/dataconverter.go +++ b/utils/dataconverter.go @@ -80,6 +80,14 @@ func NewDataConverter(params string) ( } } +func NewDataConverterMustCompile(params string) (conv DataConverter) { + var err error + if conv, err = NewDataConverter(params); err != nil { + panic(fmt.Sprintf("parsing: <%s>, error: %s", params, err.Error())) + } + return +} + func NewDurationSecondsConverter(params string) ( hdlr DataConverter, err error) { return new(DurationSecondsConverter), nil diff --git a/utils/rsrfield.go b/utils/rsrfield.go index c1c003d72..044266f26 100644 --- a/utils/rsrfield.go +++ b/utils/rsrfield.go @@ -218,6 +218,15 @@ func NewRSRFilter(fltrVal string) (rsrFltr *RSRFilter, err error) { return rsrFltr, nil } +// NewRSRFilterMustCompile is used mostly in tests +func NewRSRFilterMustCompile(fltrVal string) (rsrFltr *RSRFilter) { + var err error + if rsrFltr, err = NewRSRFilter(fltrVal); err != nil { + panic(fmt.Sprintf("parsing <%s>, err: %s", fltrVal, err.Error())) + } + return +} + // One filter rule type RSRFilter struct { filterRule string // Value in raw format diff --git a/utils/rsrfield_test.go b/utils/rsrfield_test.go index d2253a47d..3e4af5b28 100644 --- a/utils/rsrfield_test.go +++ b/utils/rsrfield_test.go @@ -71,11 +71,6 @@ func TestNewRSRField1(t *testing.T) { } else if !reflect.DeepEqual(expRSRField, rsrField) { t.Errorf("Expecting: %+v, received: %+v", expRSRField, rsrField) } - // Separator escaped - rulesStr = `~sip_redirected_to:s\/sip:\+49(\d+)@/0$1/` - if rsrField, err := NewRSRField(rulesStr); err == nil { - t.Errorf("Parse error, field rule does not contain correct number of separators, received: %v", rsrField) - } // One extra separator but escaped rulesStr = `~sip_redirected_to:s/sip:\+49(\d+)\/@/0$1/` expRSRField3 := &RSRField{Id: "sip_redirected_to", Rules: rulesStr, diff --git a/utils/rsrparser.go b/utils/rsrparser.go new file mode 100644 index 000000000..07b749407 --- /dev/null +++ b/utils/rsrparser.go @@ -0,0 +1,135 @@ +/* +Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ + +package utils + +import ( + "fmt" + "regexp" + "strings" +) + +func NewRSRParser(parserRules, inRuleSep string) (rsrParser *RSRParser, err error) { + if len(parserRules) == 0 { + return + } + rsrParser = &RSRParser{Rules: parserRules} + if strings.HasSuffix(parserRules, FILTER_VAL_END) { // Has filter, populate the var + fltrStart := strings.LastIndex(parserRules, FILTER_VAL_START) + if fltrStart < 1 { + return nil, fmt.Errorf("invalid RSRFilter start rule in string: <%s>", parserRules) + } + fltrVal := parserRules[fltrStart+1 : len(parserRules)-1] + rsrParser.filters, err = ParseRSRFilters(fltrVal, inRuleSep) + if err != nil { + return nil, fmt.Errorf("Invalid FilterValue in string: %s, err: %s", fltrVal, err.Error()) + } + parserRules = parserRules[:fltrStart] // Take the filter part out before compiling further + } + if idxConverters := strings.Index(parserRules, "{*"); idxConverters != -1 { // converters in the string + if !strings.HasSuffix(parserRules, "}") { + return nil, + fmt.Errorf("invalid converter terminator in rule: <%s>", + parserRules) + } + convertersStr := parserRules[idxConverters+1 : len(parserRules)-1] // strip also {} + convsSplt := strings.Split(convertersStr, inRuleSep) + rsrParser.converters = make(DataConverters, len(convsSplt)) + for i, convStr := range convsSplt { + if conv, err := NewDataConverter(convStr); err != nil { + return nil, + fmt.Errorf("invalid converter value in string: <%s>, err: %s", + convStr, err.Error()) + } else { + rsrParser.converters[i] = conv + } + } + parserRules = parserRules[:idxConverters] + } + if !strings.HasPrefix(parserRules, DynamicDataPrefix) { // special case when RSR is defined as static attribute=value + var staticHdr, staticVal string + if splt := strings.Split(parserRules, AttrValueSep); len(splt) == 2 { // using '='' as separator since ':' is often use in date/time fields + staticHdr, staticVal = splt[0], splt[1] // strip the separator + if strings.HasSuffix(staticVal, AttrValueSep) { // if value ends with sep, strip it since it is a part of the definition syntax + staticVal = staticVal[:len(staticVal)-1] + } + } else if len(splt) > 2 { + return nil, fmt.Errorf("invalid RSRField static rules: <%s>", parserRules) + } else { + staticVal = splt[0] // no attribute name + } + rsrParser.attrName = staticHdr + rsrParser.attrValue = staticVal + return + } + // dynamic content via attributeNames + spltRgxp := regexp.MustCompile(`:s\/`) + spltRules := spltRgxp.Split(parserRules, -1) + rsrParser.attrName = spltRules[0][1:] // in form ~hdr_name + if len(spltRules) > 1 { + rulesRgxp := regexp.MustCompile(`(?:(.+[^\\])\/(.*[^\\])*\/){1,}`) + for _, ruleStr := range spltRules[1:] { // :s/ already removed through split + fmt.Printf("ruleStr: %s\n", ruleStr) + allMatches := rulesRgxp.FindStringSubmatch(ruleStr) + if len(allMatches) != 3 { + return nil, fmt.Errorf("not enough members in Search&Replace, ruleStr: <%s>, matches: %v, ", ruleStr, allMatches) + } + if srRegexp, err := regexp.Compile(allMatches[1]); err != nil { + return nil, fmt.Errorf("invalid Search&Replace subfield rule: <%s>", allMatches[1]) + } else { + rsrParser.rsrRules = append(rsrParser.rsrRules, &ReSearchReplace{SearchRegexp: srRegexp, ReplaceTemplate: allMatches[2]}) + } + } + } + return +} + +// RSRParser is a parser for data coming from various sources +type RSRParser struct { + Rules string // Rules container holding the string rules, public so it can be stored + + attrName string // instruct extracting info out of header in event + attrValue string // if populated, enforces parsing always to this value + rsrRules []*ReSearchReplace // rules to use when parsing value + converters DataConverters // set of converters to apply on output + filters RSRFilters // The value to compare when used as filter +} + +func NewRSRParsers(parsersRules, rlsSep, inRlSep string) (prsrs RSRParsers, err error) { + if parsersRules == "" { + return + } + return NewRSRParsersFromSlice(strings.Split(parsersRules, rlsSep), inRlSep) +} + +func NewRSRParsersFromSlice(parsersRules []string, inRlSep string) (prsrs RSRParsers, err error) { + prsrs = make(RSRParsers, len(parsersRules)) + for i, rlStr := range parsersRules { + if rsrPrsr, err := NewRSRParser(rlStr, inRlSep); err != nil { + return nil, err + } else if rsrPrsr == nil { + return nil, fmt.Errorf("emtpy RSRParser in rule: <%s>", rlStr) + } else { + prsrs[i] = rsrPrsr + } + } + return +} + +// RSRParsers is a set of RSRParser +type RSRParsers []*RSRParser diff --git a/utils/rsrparser_test.go b/utils/rsrparser_test.go new file mode 100644 index 000000000..b4bdeda5d --- /dev/null +++ b/utils/rsrparser_test.go @@ -0,0 +1,56 @@ +/* +Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ +package utils + +import ( + "reflect" + "regexp" + "testing" +) + +func TestNewRSRParsers(t *testing.T) { + ruleStr := `Value1;Heade2=Value2;~Header3(Val3&!Val4);~Header4:s/^a/${1}b/{*duration_seconds&*round:2}(b&c);Value5{*duration_seconds&*round:2}` + eRSRParsers := RSRParsers{ + &RSRParser{Rules: "Value1", attrValue: "Value1"}, + &RSRParser{Rules: "Heade2=Value2", attrName: "Heade2", attrValue: "Value2"}, + &RSRParser{Rules: "~Header3(Val3&!Val4)", attrName: "Header3", + filters: RSRFilters{NewRSRFilterMustCompile("Val3"), + NewRSRFilterMustCompile("!Val4")}}, + + &RSRParser{Rules: "~Header4:s/^a/${1}b/{*duration_seconds&*round:2}(b&c)", attrName: "Header4", + rsrRules: []*ReSearchReplace{ + &ReSearchReplace{ + SearchRegexp: regexp.MustCompile(`^a`), + ReplaceTemplate: "${1}b"}}, + converters: DataConverters{NewDataConverterMustCompile("*duration_seconds"), + NewDataConverterMustCompile("*round:2")}, + filters: RSRFilters{NewRSRFilterMustCompile("b"), + NewRSRFilterMustCompile("c")}, + }, + + &RSRParser{Rules: "Value5{*duration_seconds&*round:2}", attrValue: "Value5", + converters: DataConverters{NewDataConverterMustCompile("*duration_seconds"), + NewDataConverterMustCompile("*round:2")}, + }, + } + if rsrParsers, err := NewRSRParsers(ruleStr, INFIELD_SEP, ANDSep); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if !reflect.DeepEqual(eRSRParsers, rsrParsers) { + t.Errorf("expecting: %+v, received: %+v", eRSRParsers, rsrParsers) + } +}