xapp-frame v0.8.2 integration to submgr
[ric-plt/submgr.git] / pkg / control / control.go
index 02110b6..325427b 100755 (executable)
@@ -34,6 +34,7 @@ import (
        httptransport "github.com/go-openapi/runtime/client"
        "github.com/go-openapi/strfmt"
        "github.com/gorilla/mux"
+       "github.com/segmentio/ksuid"
        "github.com/spf13/viper"
 )
 
@@ -62,20 +63,21 @@ func idstring(err error, entries ...fmt.Stringer) string {
 var e2tSubReqTimeout time.Duration
 var e2tSubDelReqTime time.Duration
 var e2tRecvMsgTimeout time.Duration
+var waitRouteCleanup_ms time.Duration
 var e2tMaxSubReqTryCount uint64    // Initial try + retry
 var e2tMaxSubDelReqTryCount uint64 // Initial try + retry
 var readSubsFromDb string
 
 type Control struct {
        *xapp.RMRClient
-       e2ap     *E2ap
-       registry *Registry
-       tracker  *Tracker
-       db       Sdlnterface
-       //subscriber *xapp.Subscriber
+       e2ap          *E2ap
+       registry      *Registry
+       tracker       *Tracker
+       db            Sdlnterface
        CntRecvMsg    uint64
        ResetTestFlag bool
        Counters      map[string]xapp.Counter
+       LoggerLevel   uint32
 }
 
 type RMRMeid struct {
@@ -107,11 +109,11 @@ func NewControl() *Control {
        tracker.Init()
 
        c := &Control{e2ap: new(E2ap),
-               registry: registry,
-               tracker:  tracker,
-               db:       CreateSdl(),
-               //subscriber: subscriber,
-               Counters: xapp.Metric.RegisterCounterGroup(GetMetricsOpts(), "SUBMGR"),
+               registry:    registry,
+               tracker:     tracker,
+               db:          CreateSdl(),
+               Counters:    xapp.Metric.RegisterCounterGroup(GetMetricsOpts(), "SUBMGR"),
+               LoggerLevel: 3,
        }
        c.ReadConfigParameters("")
 
@@ -119,7 +121,7 @@ func NewControl() *Control {
        xapp.Resource.InjectRoute("/ric/v1/test/{testId}", c.TestRestHandler, "POST")
        xapp.Resource.InjectRoute("/ric/v1/symptomdata", c.SymptomDataHandler, "GET")
 
-       go xapp.Subscription.Listen(c.SubscriptionHandler, c.QueryHandler, c.SubscriptionDeleteHandler)
+       go xapp.Subscription.Listen(c.SubscriptionHandler, c.QueryHandler, c.SubscriptionDeleteHandlerCB)
 
        if readSubsFromDb == "false" {
                return c
@@ -164,11 +166,21 @@ func (c *Control) ReadConfigParameters(f string) {
                e2tRecvMsgTimeout = 2000 * 1000000
        }
        xapp.Logger.Info("e2tRecvMsgTimeout %v", e2tRecvMsgTimeout)
+
+       // Internal cfg parameter, used to define a wait time for RMR route clean-up. None default
+       // value 100ms used currently only in unittests.
+       waitRouteCleanup_ms = viper.GetDuration("controls.waitRouteCleanup_ms") * 1000000
+       if waitRouteCleanup_ms == 0 {
+               waitRouteCleanup_ms = 5000 * 1000000
+       }
+       xapp.Logger.Info("waitRouteCleanup %v", waitRouteCleanup_ms)
+
        e2tMaxSubReqTryCount = viper.GetUint64("controls.e2tMaxSubReqTryCount")
        if e2tMaxSubReqTryCount == 0 {
                e2tMaxSubReqTryCount = 1
        }
        xapp.Logger.Info("e2tMaxSubReqTryCount %v", e2tMaxSubReqTryCount)
+
        e2tMaxSubDelReqTryCount = viper.GetUint64("controls.e2tMaxSubDelReqTryCount")
        if e2tMaxSubDelReqTryCount == 0 {
                e2tMaxSubDelReqTryCount = 1
@@ -180,6 +192,10 @@ func (c *Control) ReadConfigParameters(f string) {
                readSubsFromDb = "true"
        }
        xapp.Logger.Info("readSubsFromDb %v", readSubsFromDb)
+       c.LoggerLevel = viper.GetUint32("logger.level")
+       if c.LoggerLevel == 0 {
+               c.LoggerLevel = 3
+       }
 }
 
 //-------------------------------------------------------------------
@@ -213,29 +229,287 @@ func (c *Control) Run() {
 //
 //-------------------------------------------------------------------
 func (c *Control) SubscriptionHandler(params interface{}) (*models.SubscriptionResponse, error) {
-       /*
-          switch p := params.(type) {
-          case *models.ReportParams:
-              trans := c.tracker.NewXappTransaction(NewRmrEndpoint(p.ClientEndpoint),"" , 0, &xapp.RMRMeid{RanName: p.Meid})
-              if trans == nil {
-                    xapp.Logger.Error("XAPP-SubReq: %s", idstring(fmt.Errorf("transaction not created"), params))
-                    return
-              }
-              defer trans.Release()
-          case *models.ControlParams:
-          case *models.PolicyParams:
-          }
-       */
-       return &models.SubscriptionResponse{}, fmt.Errorf("Subscription rest interface not implemented")
-}
-
-func (c *Control) SubscriptionDeleteHandler(s string) error {
+
+       c.CntRecvMsg++
+       c.UpdateCounter(cRestSubReqFromXapp)
+
+       subResp := models.SubscriptionResponse{}
+       p := params.(*models.SubscriptionParams)
+
+       if c.LoggerLevel > 2 {
+               c.PrintRESTSubscriptionRequest(p)
+       }
+
+       if p.ClientEndpoint == nil {
+               xapp.Logger.Error("ClientEndpoint == nil")
+               c.UpdateCounter(cRestSubFailToXapp)
+               return nil, fmt.Errorf("")
+       }
+
+       _, xAppRmrEndpoint, err := ConstructEndpointAddresses(*p.ClientEndpoint)
+       if err != nil {
+               xapp.Logger.Error("%s", err.Error())
+               c.UpdateCounter(cRestSubFailToXapp)
+               return nil, err
+       }
+       var restSubId string
+       var restSubscription *RESTSubscription
+       if p.SubscriptionID == "" {
+               restSubId = ksuid.New().String()
+               restSubscription, err = c.registry.CreateRESTSubscription(&restSubId, &xAppRmrEndpoint, p.Meid)
+               if err != nil {
+                       xapp.Logger.Error("%s", err.Error())
+                       c.UpdateCounter(cRestSubFailToXapp)
+                       return nil, err
+               }
+
+       } else {
+               restSubId = p.SubscriptionID
+               restSubscription, err = c.registry.GetRESTSubscription(restSubId, false)
+               if err != nil {
+                       xapp.Logger.Error("%s", err.Error())
+                       c.UpdateCounter(cRestSubFailToXapp)
+                       return nil, err
+               }
+       }
+
+       subResp.SubscriptionID = &restSubId
+       subReqList := e2ap.SubscriptionRequestList{}
+       err = c.e2ap.FillSubscriptionReqMsgs(params, &subReqList, restSubscription)
+       if err != nil {
+               xapp.Logger.Error("%s", err.Error())
+               c.registry.DeleteRESTSubscription(&restSubId)
+               c.UpdateCounter(cRestSubFailToXapp)
+               return nil, err
+       }
+
+       go c.processSubscriptionRequests(restSubscription, &subReqList, p.ClientEndpoint, p.Meid, &restSubId)
+
+       c.UpdateCounter(cRestSubRespToXapp)
+       return &subResp, nil
+}
+
+//-------------------------------------------------------------------
+//
+//-------------------------------------------------------------------
+
+func (c *Control) processSubscriptionRequests(restSubscription *RESTSubscription, subReqList *e2ap.SubscriptionRequestList,
+       clientEndpoint *models.SubscriptionParamsClientEndpoint, meid *string, restSubId *string) {
+
+       xapp.Logger.Info("Subscription Request count=%v ", len(subReqList.E2APSubscriptionRequests))
+
+       _, xAppRmrEndpoint, err := ConstructEndpointAddresses(*clientEndpoint)
+       if err != nil {
+               xapp.Logger.Error("%s", err.Error())
+               return
+       }
+
+       var xAppEventInstanceID int64
+       var e2EventInstanceID int64
+       for index := 0; index < len(subReqList.E2APSubscriptionRequests); index++ {
+               subReqMsg := subReqList.E2APSubscriptionRequests[index]
+
+               trans := c.tracker.NewXappTransaction(xapp.NewRmrEndpoint(xAppRmrEndpoint), *restSubId, subReqMsg.RequestId, &xapp.RMRMeid{RanName: *meid})
+               if trans == nil {
+                       c.registry.DeleteRESTSubscription(restSubId)
+                       xapp.Logger.Error("XAPP-SubReq transaction not created. RESTSubId=%s, EndPoint=%s, Meid=%s", *restSubId, xAppRmrEndpoint, *meid)
+                       return
+               }
+
+               defer trans.Release()
+               xapp.Logger.Info("Handle SubscriptionRequest index=%v, %s", index, idstring(nil, trans))
+               subRespMsg, err := c.handleSubscriptionRequest(trans, &subReqMsg, meid, restSubId)
+               if err != nil {
+                       // Send notification to xApp that prosessing of a Subscription Request has failed.
+                       xAppEventInstanceID = (int64)(subReqMsg.RequestId.Id)
+                       e2EventInstanceID = (int64)(0)
+                       resp := &models.SubscriptionResponse{
+                               SubscriptionID: restSubId,
+                               SubscriptionInstances: []*models.SubscriptionInstance{
+                                       &models.SubscriptionInstance{E2EventInstanceID: &e2EventInstanceID,
+                                               ErrorCause:          nil, //TODO: Suitable Error cause.
+                                               XappEventInstanceID: &xAppEventInstanceID},
+                               },
+                       }
+                       // Mark REST subscription request processed.
+                       restSubscription.SetProcessed()
+                       xapp.Logger.Info("Sending unsuccessful REST notification to endpoint=%v:%v, XappEventInstanceID=%v, E2EventInstanceID=%v, %s",
+                               clientEndpoint.Host, *clientEndpoint.HTTPPort, xAppEventInstanceID, e2EventInstanceID, idstring(nil, trans))
+                       xapp.Subscription.Notify(resp, *clientEndpoint)
+                       c.UpdateCounter(cRestSubFailNotifToXapp)
+               } else {
+                       xAppEventInstanceID = (int64)(subRespMsg.RequestId.Id)
+                       e2EventInstanceID = (int64)(subRespMsg.RequestId.InstanceId)
+
+                       xapp.Logger.Info("SubscriptionRequest index=%v processed successfully. endpoint=%v:%v, XappEventInstanceID=%v, E2EventInstanceID=%v, %s",
+                               index, clientEndpoint.Host, *clientEndpoint.HTTPPort, xAppEventInstanceID, e2EventInstanceID, idstring(nil, trans))
+
+                       // Store successfully processed InstanceId for deletion
+                       restSubscription.AddE2InstanceId(subRespMsg.RequestId.InstanceId)
+                       restSubscription.AddXappIdToE2Id(xAppEventInstanceID, e2EventInstanceID)
+
+                       // Send notification to xApp that a Subscription Request has been processed.
+                       resp := &models.SubscriptionResponse{
+                               SubscriptionID: restSubId,
+                               SubscriptionInstances: []*models.SubscriptionInstance{
+                                       &models.SubscriptionInstance{E2EventInstanceID: &e2EventInstanceID,
+                                               ErrorCause:          nil,
+                                               XappEventInstanceID: &xAppEventInstanceID},
+                               },
+                       }
+                       // Mark REST subscription request processesd.
+                       restSubscription.SetProcessed()
+                       xapp.Logger.Info("Sending successful REST notification to endpoint=%v:%v, XappEventInstanceID=%v, E2EventInstanceID=%v, %s",
+                               clientEndpoint.Host, *clientEndpoint.HTTPPort, xAppEventInstanceID, e2EventInstanceID, idstring(nil, trans))
+                       xapp.Subscription.Notify(resp, *clientEndpoint)
+                       c.UpdateCounter(cRestSubNotifToXapp)
+
+               }
+       }
+}
+
+//-------------------------------------------------------------------
+//
+//------------------------------------------------------------------
+func (c *Control) handleSubscriptionRequest(trans *TransactionXapp, subReqMsg *e2ap.E2APSubscriptionRequest, meid *string,
+       restSubId *string) (*e2ap.E2APSubscriptionResponse, error) {
+
+       err := c.tracker.Track(trans)
+       if err != nil {
+               err = fmt.Errorf("XAPP-SubReq: %s", idstring(err, trans))
+               xapp.Logger.Error("%s", err.Error())
+               return nil, err
+       }
+
+       subs, err := c.registry.AssignToSubscription(trans, subReqMsg, c.ResetTestFlag, c)
+       if err != nil {
+               err = fmt.Errorf("XAPP-SubReq: %s", idstring(err, trans))
+               xapp.Logger.Error("%s", err.Error())
+               return nil, err
+       }
+
+       //
+       // Wake subs request
+       //
+       go c.handleSubscriptionCreate(subs, trans)
+       event, _ := trans.WaitEvent(0) //blocked wait as timeout is handled in subs side
+
+       err = nil
+       if event != nil {
+               switch themsg := event.(type) {
+               case *e2ap.E2APSubscriptionResponse:
+                       trans.Release()
+                       return themsg, nil
+               case *e2ap.E2APSubscriptionFailure:
+                       err = fmt.Errorf("SubscriptionFailure received")
+                       return nil, err
+               default:
+                       break
+               }
+       }
+       err = fmt.Errorf("XAPP-SubReq: failed %s", idstring(err, trans, subs))
+       xapp.Logger.Error("%s", err.Error())
+       c.registry.RemoveFromSubscription(subs, trans, waitRouteCleanup_ms, c)
+       return nil, err
+}
+
+//-------------------------------------------------------------------
+//
+//-------------------------------------------------------------------
+func (c *Control) SubscriptionDeleteHandlerCB(restSubId string) error {
+
+       c.CntRecvMsg++
+       c.UpdateCounter(cRestSubDelReqFromXapp)
+
+       xapp.Logger.Info("SubscriptionDeleteRequest from XAPP")
+
+       restSubscription, err := c.registry.GetRESTSubscription(restSubId, true)
+       if err != nil {
+               xapp.Logger.Error("%s", err.Error())
+               if restSubscription == nil {
+                       // Subscription was not found
+                       return nil
+               } else {
+                       if restSubscription.SubReqOngoing == true {
+                               err := fmt.Errorf("Handling of the REST Subscription Request still ongoing %s", restSubId)
+                               xapp.Logger.Error("%s", err.Error())
+                               return err
+                       } else if restSubscription.SubDelReqOngoing == true {
+                               // Previous request for same restSubId still ongoing
+                               return nil
+                       }
+               }
+       }
+
+       xAppRmrEndPoint := restSubscription.xAppRmrEndPoint
+       go func() {
+               for _, instanceId := range restSubscription.InstanceIds {
+                       xAppEventInstanceID, err := c.SubscriptionDeleteHandler(&restSubId, &xAppRmrEndPoint, &restSubscription.Meid, instanceId)
+
+                       if err != nil {
+                               xapp.Logger.Error("%s", err.Error())
+                               //return err
+                       }
+                       xapp.Logger.Info("Deleteting instanceId = %v", instanceId)
+                       restSubscription.DeleteXappIdToE2Id(xAppEventInstanceID)
+                       restSubscription.DeleteE2InstanceId(instanceId)
+               }
+               c.registry.DeleteRESTSubscription(&restSubId)
+       }()
+
+       c.UpdateCounter(cRestSubDelRespToXapp)
+
        return nil
 }
 
+//-------------------------------------------------------------------
+//
+//-------------------------------------------------------------------
+func (c *Control) SubscriptionDeleteHandler(restSubId *string, endPoint *string, meid *string, instanceId uint32) (int64, error) {
+
+       var xAppEventInstanceID int64
+       subs, err := c.registry.GetSubscriptionFirstMatch([]uint32{instanceId})
+       if err != nil {
+               xapp.Logger.Info("Subscription Delete Handler subscription for restSubId=%v, E2EventInstanceID=%v not found %s",
+                       restSubId, instanceId, idstring(err, nil))
+               return xAppEventInstanceID, nil
+       }
+
+       xAppEventInstanceID = int64(subs.ReqId.Id)
+       trans := c.tracker.NewXappTransaction(xapp.NewRmrEndpoint(*endPoint), *restSubId, e2ap.RequestId{subs.ReqId.Id, 0}, &xapp.RMRMeid{RanName: *meid})
+       if trans == nil {
+               err := fmt.Errorf("XAPP-SubDelReq transaction not created. restSubId %s, endPoint %s, meid %s, instanceId %v", *restSubId, *endPoint, *meid, instanceId)
+               xapp.Logger.Error("%s", err.Error())
+       }
+       defer trans.Release()
+
+       err = c.tracker.Track(trans)
+       if err != nil {
+               err := fmt.Errorf("XAPP-SubDelReq %s:", idstring(err, trans))
+               xapp.Logger.Error("%s", err.Error())
+               return xAppEventInstanceID, &time.ParseError{}
+       }
+       //
+       // Wake subs delete
+       //
+       go c.handleSubscriptionDelete(subs, trans)
+       trans.WaitEvent(0) //blocked wait as timeout is handled in subs side
+
+       xapp.Logger.Debug("XAPP-SubDelReq: Handling event %s ", idstring(nil, trans, subs))
+
+       c.registry.RemoveFromSubscription(subs, trans, waitRouteCleanup_ms, c)
+
+       return xAppEventInstanceID, nil
+}
+
+//-------------------------------------------------------------------
+//
+//-------------------------------------------------------------------
 func (c *Control) QueryHandler() (models.SubscriptionList, error) {
        xapp.Logger.Info("QueryHandler() called")
 
+       c.CntRecvMsg++
+
        return c.registry.QueryHandler()
 }
 
@@ -504,6 +778,7 @@ func (c *Control) handleSubscriptionCreate(subs *Subscription, parentTrans *Tran
                        removeSubscriptionFromDb = true
                        subRfMsg, valid = subs.SetCachedResponse(event, false)
                        xapp.Logger.Info("SUBS-SubReq: internal delete due event(%s) %s", typeofSubsMessage(event), idstring(nil, trans, subs, parentTrans))
+                       c.sendE2TSubscriptionDeleteRequest(subs, trans, parentTrans)
                case *SubmgrRestartTestEvent:
                        // This simulates that no response has been received and after restart subscriptions are restored from db
                        xapp.Logger.Debug("Test restart flag is active. Dropping this transaction to test restart case")
@@ -521,7 +796,7 @@ func (c *Control) handleSubscriptionCreate(subs *Subscription, parentTrans *Tran
 
        //Now RemoveFromSubscription in here to avoid race conditions (mostly concerns delete)
        if valid == false {
-               c.registry.RemoveFromSubscription(subs, parentTrans, 5*time.Second, c)
+               c.registry.RemoveFromSubscription(subs, parentTrans, waitRouteCleanup_ms, c)
        }
 
        c.UpdateSubscriptionInDB(subs, removeSubscriptionFromDb)
@@ -553,7 +828,7 @@ func (c *Control) handleSubscriptionDelete(subs *Subscription, parentTrans *Tran
        //Now RemoveFromSubscription in here to avoid race conditions (mostly concerns delete)
        //  If parallel deletes ongoing both might pass earlier sendE2TSubscriptionDeleteRequest(...) if
        //  RemoveFromSubscription locates in caller side (now in handleXAPPSubscriptionDeleteRequest(...))
-       c.registry.RemoveFromSubscription(subs, parentTrans, 5*time.Second, c)
+       c.registry.RemoveFromSubscription(subs, parentTrans, waitRouteCleanup_ms, c)
        c.registry.UpdateSubscriptionToDb(subs, c)
        parentTrans.SendEvent(nil, 0)
 }
@@ -576,6 +851,7 @@ func (c *Control) sendE2TSubscriptionRequest(subs *Subscription, trans *Transact
 
        // Write uncompleted subscrition in db. If no response for subscrition it need to be re-processed (deleted) after restart
        c.WriteSubscriptionToDb(subs)
+
        for retries := uint64(0); retries < e2tMaxSubReqTryCount; retries++ {
                desc := fmt.Sprintf("(retry %d)", retries)
                if retries == 0 {
@@ -643,6 +919,7 @@ func (c *Control) sendE2TSubscriptionDeleteRequest(subs *Subscription, trans *Tr
 func (c *Control) handleE2TSubscriptionResponse(params *xapp.RMRParams) {
        xapp.Logger.Info("MSG from E2T: %s", params.String())
        c.UpdateCounter(cSubRespFromE2)
+
        subRespMsg, err := c.e2ap.UnpackSubscriptionResponse(params.Payload)
        if err != nil {
                xapp.Logger.Error("MSG-SubResp %s", idstring(err, params))
@@ -765,14 +1042,14 @@ func typeofSubsMessage(v interface{}) string {
                return "NIL"
        }
        switch v.(type) {
-       case *e2ap.E2APSubscriptionRequest:
-               return "SubReq"
+       //case *e2ap.E2APSubscriptionRequest:
+       //      return "SubReq"
        case *e2ap.E2APSubscriptionResponse:
                return "SubResp"
        case *e2ap.E2APSubscriptionFailure:
                return "SubFail"
-       case *e2ap.E2APSubscriptionDeleteRequest:
-               return "SubDelReq"
+       //case *e2ap.E2APSubscriptionDeleteRequest:
+       //      return "SubDelReq"
        case *e2ap.E2APSubscriptionDeleteResponse:
                return "SubDelResp"
        case *e2ap.E2APSubscriptionDeleteFailure:
@@ -848,3 +1125,54 @@ func (c *Control) SendSubscriptionDeleteReq(subs *Subscription) {
                c.handleXAPPSubscriptionDeleteRequest(params)
        }
 }
+
+func (c *Control) PrintRESTSubscriptionRequest(p *models.SubscriptionParams) {
+
+       fmt.Println("CRESTSubscriptionRequest")
+       fmt.Printf("  ClientEndpoint.Host = %s\n", p.ClientEndpoint.Host)
+
+       if p.ClientEndpoint.HTTPPort != nil {
+               fmt.Printf("  ClientEndpoint.HTTPPort = %v\n", *p.ClientEndpoint.HTTPPort)
+       } else {
+               fmt.Println("  ClientEndpoint.HTTPPort = nil")
+       }
+
+       if p.ClientEndpoint.RMRPort != nil {
+               fmt.Printf("  ClientEndpoint.RMRPort = %v\n", *p.ClientEndpoint.RMRPort)
+       } else {
+               fmt.Println("  ClientEndpoint.RMRPort = nil")
+       }
+
+       if p.Meid != nil {
+               fmt.Printf("  Meid = %s\n", *p.Meid)
+       } else {
+               fmt.Println("  Meid = nil")
+       }
+
+       for _, subscriptionDetail := range p.SubscriptionDetails {
+               if p.RANFunctionID != nil {
+                       fmt.Printf("  RANFunctionID = %v\n", *p.RANFunctionID)
+               } else {
+                       fmt.Println("  RANFunctionID = nil")
+               }
+               fmt.Printf("  SubscriptionDetail.XappEventInstanceID = %v\n", *subscriptionDetail.XappEventInstanceID)
+               fmt.Printf("  SubscriptionDetail.EventTriggers.OctetString = %X\n", subscriptionDetail.EventTriggers.OctetString)
+
+               for _, actionToBeSetup := range subscriptionDetail.ActionToBeSetupList {
+                       fmt.Printf("  SubscriptionDetail.ActionToBeSetup.ActionID = %v\n", *actionToBeSetup.ActionID)
+                       fmt.Printf("  SubscriptionDetail.ActionToBeSetup.ActionType = %s\n", *actionToBeSetup.ActionType)
+                       if actionToBeSetup.ActionDefinition != nil {
+                               fmt.Printf("  SubscriptionDetail.ActionToBeSetup.ActionDefinition.OctetString = %X\n", actionToBeSetup.ActionDefinition.OctetString)
+                       } else {
+                               fmt.Println("  SubscriptionDetail.ActionToBeSetup.ActionDefinition = nil")
+
+                       }
+                       if actionToBeSetup.SubsequentAction != nil {
+                               fmt.Printf("  SubscriptionDetail.ActionToBeSetup.SubsequentAction.SubsequentActionType = %s\n", *actionToBeSetup.SubsequentAction.SubsequentActionType)
+                               fmt.Printf("  SubscriptionDetail.ActionToBeSetup..SubsequentAction.TimeToWait = %s\n", *actionToBeSetup.SubsequentAction.TimeToWait)
+                       } else {
+                               fmt.Println("  SubscriptionDetail.ActionToBeSetup.SubsequentAction = nil")
+                       }
+               }
+       }
+}