diff --git a/agents/libhttpagent.go b/agents/libhttpagent.go index a660e15d2..635b2a0d4 100644 --- a/agents/libhttpagent.go +++ b/agents/libhttpagent.go @@ -162,7 +162,11 @@ func (hU *httpXmlDP) FieldAsInterface(fldPath []string) (data any, err error) { } //convert workPath to HierarchyPath hrPath := utils.HierarchyPath(workPath) - elmnt := xmlquery.FindOne(hU.xmlDoc, hrPath.AsString("/", false)) + var elmnt *xmlquery.Node + elmnt, err = xmlquery.Query(hU.xmlDoc, hrPath.AsString("/", false)) + if err != nil { + return nil, err + } if elmnt == nil { return } diff --git a/config/config_it_test.go b/config/config_it_test.go index 0247eed8b..ab745ca21 100644 --- a/config/config_it_test.go +++ b/config/config_it_test.go @@ -466,7 +466,6 @@ func testCGRConfigReloadERs(t *testing.T) { Flags: flagsDefault, Fields: content, CacheDumpFields: []*FCTemplate{}, - XmlRootPath: utils.HierarchyPath{utils.EmptyString}, }, { ID: "file_reader1", @@ -479,7 +478,6 @@ func testCGRConfigReloadERs(t *testing.T) { Flags: flags, Fields: content, CacheDumpFields: []*FCTemplate{}, - XmlRootPath: utils.HierarchyPath{utils.EmptyString}, }, }, } @@ -858,7 +856,7 @@ func testCgrCfgV1ReloadConfigSection(t *testing.T) { "Tenant": nil, "Timezone": "", "Type": "*none", - "XmlRootPath": []any{utils.EmptyString}, + "XmlRootPath": nil, }, map[string]any{ "CacheDumpFields": []any{}, @@ -879,7 +877,7 @@ func testCgrCfgV1ReloadConfigSection(t *testing.T) { "Tenant": nil, "Timezone": "", "Type": "*file_csv", - "XmlRootPath": []any{utils.EmptyString}, + "XmlRootPath": nil, "Fields": content, }, }, diff --git a/config/config_test.go b/config/config_test.go index 62b482e38..a177e756f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1691,7 +1691,7 @@ func TestCgrCdfEventReader(t *testing.T) { ConcurrentReqs: 1024, SourcePath: "/var/spool/cgrates/ers/in", ProcessedPath: "/var/spool/cgrates/ers/out", - XmlRootPath: utils.HierarchyPath{utils.EmptyString}, + XmlRootPath: nil, Tenant: nil, Timezone: utils.EmptyString, Filters: []string{}, @@ -1743,7 +1743,7 @@ func TestCgrCfgEventReaderDefault(t *testing.T) { ConcurrentReqs: 1024, SourcePath: "/var/spool/cgrates/ers/in", ProcessedPath: "/var/spool/cgrates/ers/out", - XmlRootPath: utils.HierarchyPath{utils.EmptyString}, + XmlRootPath: nil, Tenant: nil, Timezone: utils.EmptyString, Filters: nil, diff --git a/config/configsanity.go b/config/configsanity.go index 3a4c0b3eb..b71e625c1 100644 --- a/config/configsanity.go +++ b/config/configsanity.go @@ -20,6 +20,7 @@ package config import ( "fmt" + "math" "os" "strings" @@ -512,6 +513,26 @@ func (cfg *CGRConfig) checkConfigSanity() error { if field.Type != utils.META_NONE && field.Path == utils.EmptyString { return fmt.Errorf("<%s> %s for %s at %s", utils.ERs, utils.NewErrMandatoryIeMissing(utils.Path), rdr.ID, field.Tag) } + + // The following sanity check prevents a "slice bounds out of range" panic. + if rdr.Type == utils.MetaFileXML && !utils.IsSliceMember([]string{utils.META_NONE, utils.META_CONSTANT, + utils.META_FILLER, utils.MetaRemoteHost}, field.Type) && len(field.Value) != 0 { + + // Retrieve the number of elements of the parser rule with the fewest elements. + minRuleLength := math.MaxInt + for _, rule := range field.Value { + if ruleLen := len(strings.Split(rule.AttrName(), utils.NestingSep)); minRuleLength > ruleLen { + minRuleLength = ruleLen + } + } + + if len(rdr.XmlRootPath) >= minRuleLength { + return fmt.Errorf("<%s> %s for reader %s at %s", + utils.ERs, + "len of xml_root_path elements cannot be equal to or exceed the number of value rule elements", + rdr.ID, field.Tag) + } + } } } } diff --git a/config/erscfg_test.go b/config/erscfg_test.go index fbc48f4bc..4b1c5d984 100644 --- a/config/erscfg_test.go +++ b/config/erscfg_test.go @@ -113,7 +113,7 @@ func TestEventReaderLoadFromJSON(t *testing.T) { ConcurrentReqs: 1024, SourcePath: "/var/spool/cgrates/ers/in", ProcessedPath: "/var/spool/cgrates/ers/out", - XmlRootPath: utils.HierarchyPath{utils.EmptyString}, + XmlRootPath: nil, Tenant: nil, Timezone: utils.EmptyString, Filters: []string{}, @@ -152,7 +152,7 @@ func TestEventReaderLoadFromJSON(t *testing.T) { ConcurrentReqs: 1024, SourcePath: "/tmp/ers/in", ProcessedPath: "/tmp/ers/out", - XmlRootPath: utils.HierarchyPath{utils.EmptyString}, + XmlRootPath: nil, Tenant: nil, Timezone: utils.EmptyString, Filters: nil, @@ -270,7 +270,7 @@ func TestERsCfgAsMapInterface(t *testing.T) { "source_path": "/var/spool/cgrates/ers/in", "tenant": "", "timezone": "", - "xml_root_path": []string{""}, + "xml_root_path": []string{}, "cache_dump_fields": []map[string]any{}, "concurrent_requests": 1024, "type": "*none", @@ -320,7 +320,7 @@ func TestERsCfgAsMapInterface(t *testing.T) { "source_path": "/tmp/ers/in", "tenant": "", "timezone": "", - "xml_root_path": []string{""}, + "xml_root_path": []string{}, }, }, } diff --git a/config/xmldp.go b/config/xmldp.go index d78f8db3e..eb43ed6a9 100644 --- a/config/xmldp.go +++ b/config/xmldp.go @@ -44,6 +44,9 @@ type XmlProvider struct { // String is part of engine.utils.DataProvider interface // when called, it will display the already parsed values out of cache func (xP *XmlProvider) String() string { + + // TODO: Find a proper way to display xP as string. Right now it only has + // unexported fields, this will return an empty string. return utils.ToJSON(xP) } @@ -62,11 +65,11 @@ func (xP *XmlProvider) FieldAsInterface(fldPath []string) (data any, err error) for i := range relPath { if sIdx := strings.Index(relPath[i], "["); sIdx != -1 { slctrStr = relPath[i][sIdx:] - if slctrStr[len(slctrStr)-1:] != "]" { + if slctrStr[len(slctrStr)-1] != ']' { return nil, fmt.Errorf("filter rule <%s> needs to end in ]", slctrStr) } relPath[i] = relPath[i][:sIdx] - if slctrStr[1:2] != "@" { + if slctrStr[1] != '@' { i, err := strconv.Atoi(slctrStr[1 : len(slctrStr)-1]) if err != nil { return nil, err @@ -100,7 +103,10 @@ func (xP *XmlProvider) RemoteHost() net.Addr { // returns utils.ErrNotFound if the element is not found in the node // Make the method exportable until we remove the ers func ElementText(xmlElement *xmlquery.Node, elmntPath string) (string, error) { - elmnt := xmlquery.FindOne(xmlElement, elmntPath) + elmnt, err := xmlquery.Query(xmlElement, elmntPath) + if err != nil { + return "", err + } if elmnt == nil { return "", utils.ErrNotFound } diff --git a/data/conf/samples/ers_internal/cgrates.json b/data/conf/samples/ers_internal/cgrates.json index 14357ce01..da313a220 100644 --- a/data/conf/samples/ers_internal/cgrates.json +++ b/data/conf/samples/ers_internal/cgrates.json @@ -109,7 +109,7 @@ {"tag": "HDRExtra3", "path": "*cgreq.HDRExtra3", "type": "*variable", "value": "~*req.6", "mandatory": true}, {"tag": "HDRExtra2", "path": "*cgreq.HDRExtra2", "type": "*variable", "value": "~*req.6", "mandatory": true}, {"tag": "HDRExtra1", "path": "*cgreq.HDRExtra1", "type": "*variable", "value": "~*req.6", "mandatory": true}, - ], + ] }, { "id": "init_session", @@ -220,6 +220,29 @@ {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true} ], }, + { + "id": "XML_EmptyRoot", + "run_delay": "-1", + "type": "*file_xml", + "source_path": "/tmp/xmlErs2/in", + "flags": ["*cdrs","*log"], + "processed_path": "/tmp/xmlErs2/out", + "xml_root_path": "", + "fields":[ + {"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true}, + {"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true}, + {"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true}, + {"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true}, + {"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true}, + {"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true}, + {"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true}, + {"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true}, + {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true} + ] + }, { "id": "FWV1", "run_delay": "-1", diff --git a/data/conf/samples/ers_mongo/cgrates.json b/data/conf/samples/ers_mongo/cgrates.json index 21b9f3eea..896725100 100644 --- a/data/conf/samples/ers_mongo/cgrates.json +++ b/data/conf/samples/ers_mongo/cgrates.json @@ -219,7 +219,30 @@ {"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.startTime", "mandatory": true}, {"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true}, {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true} - ], + ] + }, + { + "id": "XML_EmptyRoot", + "run_delay": "-1", + "type": "*file_xml", + "source_path": "/tmp/xmlErs2/in", + "flags": ["*cdrs","*log"], + "processed_path": "/tmp/xmlErs2/out", + "xml_root_path": "", + "fields":[ + {"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true}, + {"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true}, + {"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true}, + {"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true}, + {"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true}, + {"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true}, + {"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true}, + {"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true}, + {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true} + ] }, { "id": "FWV1", diff --git a/data/conf/samples/ers_mysql/cgrates.json b/data/conf/samples/ers_mysql/cgrates.json index e1e3d674d..fe7da8de7 100644 --- a/data/conf/samples/ers_mysql/cgrates.json +++ b/data/conf/samples/ers_mysql/cgrates.json @@ -216,7 +216,30 @@ {"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.startTime", "mandatory": true}, {"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true}, {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true} - ], + ] + }, + { + "id": "XML_EmptyRoot", + "run_delay": "-1", + "type": "*file_xml", + "source_path": "/tmp/xmlErs2/in", + "flags": ["*cdrs","*log"], + "processed_path": "/tmp/xmlErs2/out", + "xml_root_path": "", + "fields":[ + {"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true}, + {"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true}, + {"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true}, + {"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true}, + {"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true}, + {"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true}, + {"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true}, + {"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true}, + {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true} + ] }, { "id": "FWV1", diff --git a/data/conf/samples/ers_postgres/cgrates.json b/data/conf/samples/ers_postgres/cgrates.json index 5eb366552..b320b95d9 100644 --- a/data/conf/samples/ers_postgres/cgrates.json +++ b/data/conf/samples/ers_postgres/cgrates.json @@ -213,7 +213,30 @@ {"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.startTime", "mandatory": true}, {"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true}, {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.broadWorksCDR.cdrData.basicModule.releaseTime;~*req.broadWorksCDR.cdrData.basicModule.answerTime", "mandatory": true} - ], + ] + }, + { + "id": "XML_EmptyRoot", + "run_delay": "-1", + "type": "*file_xml", + "source_path": "/tmp/xmlErs2/in", + "flags": ["*cdrs","*log"], + "processed_path": "/tmp/xmlErs2/out", + "xml_root_path": "", + "fields":[ + {"tag": "ToR", "path": "*cgreq.ToR", "type": "*variable", "value": "~*req.root.partial_cdr.ToR", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr.TestCase", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "_", "mandatory": true}, + {"tag": "OriginID", "path": "*cgreq.OriginID", "type": "*composed", "value": "~*req.root.partial_cdr[1].ID", "mandatory": true}, + {"tag": "RequestType", "path": "*cgreq.RequestType", "type": "*variable", "value": "~*req.root.partial_cdr.RequestType", "mandatory": true}, + {"tag": "Tenant", "path": "*cgreq.Tenant", "type": "*variable", "value": "~*req.root.partial_cdr.Tenant", "mandatory": true}, + {"tag": "Category", "path": "*cgreq.Category", "type": "*constant", "value": "call", "mandatory": true}, + {"tag": "Account", "path": "*cgreq.Account", "type": "*variable", "value": "~*req.root.partial_cdr.Account", "mandatory": true}, + {"tag": "Destination", "path": "*cgreq.Destination", "type": "*variable", "value": "~*req.root.partial_cdr.Destination", "mandatory": true}, + {"tag": "SetupTime", "path": "*cgreq.SetupTime", "type": "*variable", "value": "~*req.root.partial_cdr.SetupTime", "mandatory": true}, + {"tag": "AnswerTime", "path": "*cgreq.AnswerTime", "type": "*variable", "value": "~*req.root.partial_cdr.AnswerTime", "mandatory": true}, + {"tag": "Usage", "path": "*cgreq.Usage", "type": "*usage_difference", "value": "~*req.root.partial_cdr.ReleaseTime;~*req.root.partial_cdr.AnswerTime", "mandatory": true} + ] }, { "id": "FWV1", diff --git a/ers/filexml.go b/ers/filexml.go index 569a4e56d..b7b646c64 100644 --- a/ers/filexml.go +++ b/ers/filexml.go @@ -115,7 +115,7 @@ func (rdr *XMLFileER) Serve() (err error) { } // processFile is called for each file in a directory and dispatches erEvents from it -func (rdr *XMLFileER) processFile(fPath, fName string) (err error) { +func (rdr *XMLFileER) processFile(fPath, fName string) error { if cap(rdr.conReqs) != 0 { // 0 goes for no limit processFile := <-rdr.conReqs // Queue here for maxOpenFiles defer func() { rdr.conReqs <- processFile }() @@ -123,16 +123,21 @@ func (rdr *XMLFileER) processFile(fPath, fName string) (err error) { absPath := path.Join(fPath, fName) utils.Logger.Info( fmt.Sprintf("<%s> parsing <%s>", utils.ERs, absPath)) - var file *os.File - if file, err = os.Open(absPath); err != nil { - return - } - defer file.Close() - doc, err := xmlquery.Parse(file) + file, err := os.Open(absPath) + if err != nil { + return err + } + defer file.Close() + var doc *xmlquery.Node + doc, err = xmlquery.Parse(file) + if err != nil { + return err + } + var xmlElmts []*xmlquery.Node + xmlElmts, err = xmlquery.QueryAll(doc, rdr.Config().XmlRootPath.AsString("/", true)) if err != nil { return err } - xmlElmts := xmlquery.Find(doc, rdr.Config().XmlRootPath.AsString("/", true)) rowNr := 0 // This counts the rows in the file, not really number of CDRs evsPosted := 0 timeStart := time.Now() @@ -167,12 +172,12 @@ func (rdr *XMLFileER) processFile(fPath, fName string) (err error) { // Finished with file, move it to processed folder outPath := path.Join(rdr.Config().ProcessedPath, fName) if err = os.Rename(absPath, outPath); err != nil { - return + return err } } utils.Logger.Info( fmt.Sprintf("%s finished processing file <%s>. Total records processed: %d, events posted: %d, run duration: %s", utils.ERs, absPath, rowNr, evsPosted, time.Now().Sub(timeStart))) - return + return nil } diff --git a/ers/filexml_it_test.go b/ers/filexml_it_test.go index 0cdccf769..1822217e2 100644 --- a/ers/filexml_it_test.go +++ b/ers/filexml_it_test.go @@ -49,6 +49,8 @@ var ( testXMLITLoadTPFromFolder, testXMLITHandleCdr1File, testXmlITAnalyseCDRs, + testXMLITEmptyRootPathCase, + testXMLITEmptyRootPathCaseCheckCDRs, testXMLITCleanupFiles, testXMLITKillEngine, } @@ -79,7 +81,7 @@ func testXMLITCreateCdrDirs(t *testing.T) { "/tmp/cdrs/out", "/tmp/ers_with_filters/in", "/tmp/ers_with_filters/out", "/tmp/xmlErs/in", "/tmp/xmlErs/out", "/tmp/fwvErs/in", "/tmp/fwvErs/out", "/tmp/partErs1/in", "/tmp/partErs1/out", "/tmp/partErs2/in", "/tmp/partErs2/out", - "/tmp/flatstoreErs/in", "/tmp/flatstoreErs/out"} { + "/tmp/flatstoreErs/in", "/tmp/flatstoreErs/out", "/tmp/xmlErs2/in", "/tmp/xmlErs2/out"} { if err := os.RemoveAll(dir); err != nil { t.Fatal("Error removing folder: ", dir, err) } @@ -297,11 +299,55 @@ func testXmlITAnalyseCDRs(t *testing.T) { } } +var xmlPartialCDR = ` + + + 1 + 1001 + 1002 + cgrates.org + 20231011125833.158 + + + 2 + *postpaid + *voice + + + EmptyRootCDR + 3 + ignored + 20231011125800 + 20231011125801 + +` + +func testXMLITEmptyRootPathCase(t *testing.T) { + fileName := "partial-cdr.xml" + tmpFilePath := path.Join("/tmp", fileName) + if err := os.WriteFile(tmpFilePath, []byte(xmlPartialCDR), 0644); err != nil { + t.Fatal(err.Error()) + } + if err := os.Rename(tmpFilePath, path.Join("/tmp/xmlErs2/in", fileName)); err != nil { + t.Fatal("Error moving file to processing directory: ", err) + } + time.Sleep(100 * time.Millisecond) +} + +func testXMLITEmptyRootPathCaseCheckCDRs(t *testing.T) { + var reply []*engine.ExternalCDR + if err := xmlRPC.Call(utils.APIerSv2GetCDRs, utils.RPCCDRsFilter{OriginIDs: []string{"EmptyRootCDR_2"}}, &reply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(reply) != 3 { + t.Error("Unexpected number of CDRs returned:", len(reply)) + } +} + func testXMLITCleanupFiles(t *testing.T) { for _, dir := range []string{"/tmp/ers", "/tmp/ers2", "/tmp/init_session", "/tmp/terminate_session", "/tmp/cdrs", "/tmp/ers_with_filters", "/tmp/xmlErs", "/tmp/fwvErs", - "/tmp/partErs1", "/tmp/partErs2", "tmp/flatstoreErs"} { + "/tmp/partErs1", "/tmp/partErs2", "/tmp/flatstoreErs", "/tmp/xmlErs2"} { if err := os.RemoveAll(dir); err != nil { t.Fatal("Error removing folder: ", dir, err) } diff --git a/utils/coreutils.go b/utils/coreutils.go index a4d719a6d..ffb6b2d3b 100644 --- a/utils/coreutils.go +++ b/utils/coreutils.go @@ -625,9 +625,12 @@ func TimeIs0h(t time.Time) bool { } func ParseHierarchyPath(path string, sep string) HierarchyPath { + if path == EmptyString { + return nil + } if sep == EmptyString { for _, sep = range []string{"/", NestingSep} { - if idx := strings.Index(path, sep); idx != -1 { + if strings.Contains(path, sep) { break } } @@ -639,22 +642,24 @@ func ParseHierarchyPath(path string, sep string) HierarchyPath { // HierarchyPath is used in various places to represent various path hierarchies (eg: in Diameter groups, XML trees) type HierarchyPath []string -func (h HierarchyPath) AsString(sep string, prefix bool) string { - if len(h) == 0 { - return EmptyString +// AsString converts HierarchyPath to a string. +func (hP HierarchyPath) AsString(sep string, prefix bool) string { + var strHP strings.Builder + + // If prefix is true and the HierarchyPath slice is empty, sep will be returned. + if prefix { + strHP.WriteString(sep) } - retStr := EmptyString - for idx, itm := range h { - if idx == 0 { - if prefix { - retStr += sep - } - } else { - retStr += sep + if len(hP) == 0 { + return strHP.String() + } + for i, elem := range hP { + if i != 0 { + strHP.WriteString(sep) } - retStr += itm + strHP.WriteString(elem) } - return retStr + return strHP.String() } // Mask a number of characters in the suffix of the destination