/* 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 Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package engine import ( "fmt" "github.com/nyaruka/phonenumbers" "github.com/cgrates/birpc/context" "github.com/cgrates/cgrates/utils" ) func NewDynamicDP(ctx *context.Context, resConns, stsConns, actsConns, trdConns, rnkCOnns []string, tenant string, initialDP utils.DataProvider) *DynamicDP { return &DynamicDP{ resConns: resConns, stsConns: stsConns, actsConns: actsConns, trdConns: trdConns, rnkConns: rnkCOnns, tenant: tenant, initialDP: initialDP, cache: utils.MapStorage{}, ctx: ctx, } } type DynamicDP struct { resConns []string stsConns []string actsConns []string trdConns []string rnkConns []string tenant string initialDP utils.DataProvider cache utils.MapStorage ctx *context.Context } func (dDP *DynamicDP) String() string { return dDP.initialDP.String() } func (dDP *DynamicDP) FieldAsString(fldPath []string) (string, error) { val, err := dDP.FieldAsInterface(fldPath) if err != nil { return "", err } return utils.IfaceAsString(val), nil } var initialDPPrefixes = utils.NewStringSet([]string{ utils.MetaReq, utils.MetaVars, utils.MetaCgreq, utils.MetaCgrep, utils.MetaRep, utils.MetaAct, utils.MetaEC, utils.MetaUCH, utils.MetaOpts, utils.MetaHdr, utils.MetaTrl, utils.MetaCfg, utils.MetaTenant, utils.MetaTmp}) func (dDP *DynamicDP) FieldAsInterface(fldPath []string) (val any, err error) { if len(fldPath) == 0 { return nil, utils.ErrNotFound } if initialDPPrefixes.Has(fldPath[0]) { return dDP.initialDP.FieldAsInterface(fldPath) } val, err = dDP.cache.FieldAsInterface(fldPath) if err == utils.ErrNotFound { // in case not found in cache try to populate it return dDP.fieldAsInterface(fldPath) } return } func (dDP *DynamicDP) fieldAsInterface(fldPath []string) (val any, err error) { if len(fldPath) < 2 { return nil, fmt.Errorf("invalid fieldname <%s>", fldPath) } switch fldPath[0] { case utils.MetaAccounts: // sample of fieldName : ~*accounts.1001.Balances[Concrete1].Units // split the field name in 3 parts // fieldNameType (~*accounts), accountID(1001) and queried part (Balances.Balances[Concrete1].Units) var account utils.Account if err = connMgr.Call(dDP.ctx, dDP.actsConns, utils.AccountSv1GetAccount, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &account); err != nil { return } //construct dataProvider from account and set it further dp := utils.NewObjectDP(account) dDP.cache.Set(fldPath[:2], dp) return dp.FieldAsInterface(fldPath[2:]) case utils.MetaResources: // sample of fieldName : ~*resources.ResourceID.Field var reply utils.ResourceWithConfig if err := connMgr.Call(dDP.ctx, dDP.resConns, utils.ResourceSv1GetResourceWithConfig, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &reply); err != nil { return nil, err } dp := utils.NewObjectDP(&reply) dDP.cache.Set(fldPath[:2], dp) return dp.FieldAsInterface(fldPath[2:]) case utils.MetaStats: // sample of fieldName : ~*stats.StatID.*acd var statValues map[string]*utils.Decimal if err := connMgr.Call(dDP.ctx, dDP.stsConns, utils.StatSv1GetQueueDecimalMetrics, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &statValues); err != nil { return nil, err } for k, v := range statValues { dDP.cache.Set([]string{utils.MetaStats, fldPath[1], k}, v) } return dDP.cache.FieldAsInterface(fldPath) case utils.MetaTrends: //sample of fieldName : ~*trends.TrendID.Metrics.*acd.Value var trendSum utils.TrendSummary if err := connMgr.Call(context.TODO(), dDP.trdConns, utils.TrendSv1GetTrendSummary, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &trendSum); err != nil { return nil, err } dp := utils.NewObjectDP(trendSum) dDP.cache.Set(fldPath[:2], dp) return dp.FieldAsInterface(fldPath[2:]) case utils.MetaRankings: // sample of fieldName : ~*rankings.RankingID.SortedStatIDs[0] var rankingSum utils.RankingSummary if err := connMgr.Call(context.TODO(), dDP.rnkConns, utils.RankingSv1GetRankingSummary, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &rankingSum); err != nil { return nil, err } dp := utils.NewObjectDP(rankingSum) dDP.cache.Set(fldPath[:2], dp) return dp.FieldAsInterface(fldPath[2:]) case utils.MetaLibPhoneNumber: // sample of fieldName ~*libphonenumber.<~*req.Destination> // or ~*libphonenumber.<~*req.Destination>.Carrier dp, err := newLibPhoneNumberDP(fldPath[1]) if err != nil { return nil, err } dDP.cache.Set(fldPath[:2], dp) return dp.FieldAsInterface(fldPath[2:]) default: // in case of constant we give an empty DataProvider ( empty navigable map ) } return nil, utils.ErrNotFound } func newLibPhoneNumberDP(number string) (dp utils.DataProvider, err error) { num, err := phonenumbers.ParseAndKeepRawInput(number, utils.EmptyString) if err != nil { return nil, err } return &libphonenumberDP{pNumber: num, cache: make(utils.MapStorage)}, nil } type libphonenumberDP struct { pNumber *phonenumbers.PhoneNumber cache utils.MapStorage } func (dDP *libphonenumberDP) String() string { return dDP.pNumber.String() } func (dDP *libphonenumberDP) FieldAsString(fldPath []string) (string, error) { val, err := dDP.FieldAsInterface(fldPath) if err != nil { return "", err } return utils.IfaceAsString(val), nil } func (dDP *libphonenumberDP) FieldAsInterface(fldPath []string) (val any, err error) { if len(fldPath) == 0 { dDP.setDefaultFields() val = dDP.cache return } if val, err = dDP.cache.FieldAsInterface(fldPath); err == utils.ErrNotFound { // in case not found in cache try to populate it return dDP.fieldAsInterface(fldPath) } return } func (dDP *libphonenumberDP) fieldAsInterface(fldPath []string) (val any, err error) { if len(fldPath) != 1 { return nil, fmt.Errorf("invalid field path <%+v> for libphonenumberDP", fldPath) } switch fldPath[0] { case "CountryCode": val = dDP.pNumber.GetCountryCode() case "NationalNumber": val = dDP.pNumber.GetNationalNumber() case "Region": val = phonenumbers.GetRegionCodeForNumber(dDP.pNumber) case "NumberType": val = phonenumbers.GetNumberType(dDP.pNumber) case "GeoLocation": regCode := phonenumbers.GetRegionCodeForNumber(dDP.pNumber) geoLocation, err := phonenumbers.GetGeocodingForNumber(dDP.pNumber, regCode) if err != nil { utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting GeoLocation for number %+v", err, dDP.pNumber)) } val = geoLocation case "Carrier": carrier, err := phonenumbers.GetCarrierForNumber(dDP.pNumber, phonenumbers.GetRegionCodeForNumber(dDP.pNumber)) if err != nil { utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting Carrier for number %+v", err, dDP.pNumber)) } val = carrier case "LengthOfNationalDestinationCode": val = phonenumbers.GetLengthOfNationalDestinationCode(dDP.pNumber) case "RawInput": val = dDP.pNumber.GetRawInput() case "Extension": val = dDP.pNumber.GetExtension() case "NumberOfLeadingZeros": val = dDP.pNumber.GetNumberOfLeadingZeros() case "ItalianLeadingZero": val = dDP.pNumber.GetItalianLeadingZero() case "PreferredDomesticCarrierCode": val = dDP.pNumber.GetPreferredDomesticCarrierCode() case "CountryCodeSource": val = dDP.pNumber.GetCountryCodeSource() } dDP.cache[fldPath[0]] = val return } func (dDP *libphonenumberDP) setDefaultFields() { if _, has := dDP.cache["CountryCode"]; !has { dDP.cache["CountryCode"] = dDP.pNumber.GetCountryCode() } if _, has := dDP.cache["NationalNumber"]; !has { dDP.cache["NationalNumber"] = dDP.pNumber.GetNationalNumber() } if _, has := dDP.cache["Region"]; !has { dDP.cache["Region"] = phonenumbers.GetRegionCodeForNumber(dDP.pNumber) } if _, has := dDP.cache["NumberType"]; !has { dDP.cache["NumberType"] = phonenumbers.GetNumberType(dDP.pNumber) } if _, has := dDP.cache["GeoLocation"]; !has { geoLocation, err := phonenumbers.GetGeocodingForNumber(dDP.pNumber, phonenumbers.GetRegionCodeForNumber(dDP.pNumber)) if err != nil { utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting GeoLocation for number %+v", err, dDP.pNumber)) } dDP.cache["GeoLocation"] = geoLocation } if _, has := dDP.cache["Carrier"]; !has { carrier, err := phonenumbers.GetCarrierForNumber(dDP.pNumber, phonenumbers.GetRegionCodeForNumber(dDP.pNumber)) if err != nil { utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting Carrier for number %+v", err, dDP.pNumber)) } dDP.cache["Carrier"] = carrier } if _, has := dDP.cache["LengthOfNationalDestinationCode"]; !has { dDP.cache["LengthOfNationalDestinationCode"] = phonenumbers.GetLengthOfNationalDestinationCode(dDP.pNumber) } if _, has := dDP.cache["RawInput"]; !has { dDP.cache["RawInput"] = dDP.pNumber.GetRawInput() } if _, has := dDP.cache["Extension"]; !has { dDP.cache["Extension"] = dDP.pNumber.GetExtension() } if _, has := dDP.cache["NumberOfLeadingZeros"]; !has { dDP.cache["NumberOfLeadingZeros"] = dDP.pNumber.GetNumberOfLeadingZeros() } if _, has := dDP.cache["ItalianLeadingZero"]; !has { dDP.cache["ItalianLeadingZero"] = dDP.pNumber.GetItalianLeadingZero() } if _, has := dDP.cache["PreferredDomesticCarrierCode"]; !has { dDP.cache["PreferredDomesticCarrierCode"] = dDP.pNumber.GetPreferredDomesticCarrierCode() } if _, has := dDP.cache["CountryCodeSource"]; !has { dDP.cache["CountryCodeSource"] = dDP.pNumber.GetCountryCodeSource() } }