ECS, support for notification of available information types 13/6613/2
authorPatrikBuhr <patrik.buhr@est.tech>
Wed, 18 Aug 2021 12:11:41 +0000 (14:11 +0200)
committerPatrikBuhr <patrik.buhr@est.tech>
Tue, 24 Aug 2021 08:24:22 +0000 (10:24 +0200)
This service operation can be used for a data conumer to subscribe to
notifications for changes in the availability of data types.

Change-Id: Iecc8866685ee0029463f0c5a032ee37acf3a7c55
Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
Issue-ID: NONRTRIC-570

18 files changed:
enrichment-coordinator-service/api/ecs-api.json
enrichment-coordinator-service/api/ecs-api.yaml
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/StatusController.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/a1e/A1eEiJobStatus.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerCallbacks.java [new file with mode: 0644]
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerConsts.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerController.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerInfoTypeInfo.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerJobInfo.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerJobStatus.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerTypeRegistrationInfo.java [new file with mode: 0644]
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerTypeSubscriptionInfo.java [new file with mode: 0644]
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1producer/ProducerController.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/InfoTypeSubscriptions.java [new file with mode: 0644]
enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java
enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ConsumerSimulatorController.java
enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java

index c89d613..b2d800b 100644 (file)
@@ -3,17 +3,36 @@
         "consumer_information_type": {
             "description": "Information for an Information type",
             "type": "object",
-            "required": ["job_data_schema"],
-            "properties": {"job_data_schema": {
-                "description": "Json schema for the job data",
-                "type": "object"
-            }}
+            "required": [
+                "job_data_schema",
+                "no_of_producers",
+                "type_status"
+            ],
+            "properties": {
+                "no_of_producers": {
+                    "format": "int32",
+                    "description": "The number of registered producers for the type",
+                    "type": "integer"
+                },
+                "type_status": {
+                    "description": "Allowed values: <br/>ENABLED: one or several producers for the information type are available <br/>DISABLED: no producers for the information type are available",
+                    "type": "string",
+                    "enum": [
+                        "ENABLED",
+                        "DISABLED"
+                    ]
+                },
+                "job_data_schema": {
+                    "description": "Json schema for the job data",
+                    "type": "object"
+                }
+            }
         },
         "EiTypeObject": {
             "description": "Information for an EI type",
             "type": "object"
         },
-        "status_info": {
+        "service_status_info": {
             "type": "object",
             "required": [
                 "no_of_jobs",
                 }
             }
         },
+        "consumer_type_registration_info": {
+            "description": "Information for an Information type",
+            "type": "object",
+            "required": [
+                "info_type_id",
+                "job_data_schema",
+                "status"
+            ],
+            "properties": {
+                "info_type_id": {
+                    "description": "Information type identifier",
+                    "type": "string"
+                },
+                "job_data_schema": {
+                    "description": "Json schema for the job data",
+                    "type": "object"
+                },
+                "status": {
+                    "description": "Allowed values: <br/>REGISTERED: the information type has been registered <br/>DEREGISTERED: the information type has been removed",
+                    "type": "string",
+                    "enum": [
+                        "REGISTERED",
+                        "DEREGISTERED"
+                    ]
+                }
+            }
+        },
         "ProblemDetails": {
             "description": "A problem detail to carry details in a HTTP response according to RFC 7807",
             "type": "object",
             "type": "object",
             "required": ["eiJobStatus"],
             "properties": {"eiJobStatus": {
-                "description": "Allowed values for EI job status",
+                "description": "Allowed values for EI job status: <br/>ENABLED: the A1-EI producer is able to deliver EI result for the EI job <br/>DISABLED: the A1-EI producer is unable to deliver EI result for the EI job",
                 "type": "string",
                 "enum": [
                     "ENABLED",
             ],
             "properties": {
                 "info_job_status": {
-                    "description": "Allowed values for Information Job status",
+                    "description": "Allowed values: <br/>ENABLED: the A1-Information producer is able to deliver result for the Information Job <br/>DISABLED: the A1-Information producer is unable to deliver result for the Information Job",
                     "type": "string",
                     "enum": [
                         "ENABLED",
                     ]
                 },
                 "producers": {
-                    "description": "An array of all registerred Information Producer Identifiers.",
+                    "description": "An array of all registered Information Producer Identifiers.",
                     "type": "array",
                     "items": {
-                        "description": "An array of all registerred Information Producer Identifiers.",
+                        "description": "An array of all registered Information Producer Identifiers.",
                         "type": "string"
                     }
                 }
             }
         },
         "consumer_job": {
-            "description": "Information for an Enrichment  Information Job",
+            "description": "Information for an Enrichment Information Job",
             "type": "object",
             "required": [
                 "info_type_id",
         "Void": {
             "description": "Void/empty ",
             "type": "object"
+        },
+        "consumer_type_subscription_info": {
+            "description": "Information for an information type subscription",
+            "type": "object",
+            "required": [
+                "owner",
+                "status_result_uri"
+            ],
+            "properties": {
+                "owner": {
+                    "description": "Identity of the owner of the subscription",
+                    "type": "string"
+                },
+                "status_result_uri": {
+                    "description": "The target URI of the subscribed information",
+                    "type": "string"
+                }
+            }
         }
     }},
     "openapi": "3.0.1",
     "paths": {
+        "/example_dataproducer/info_job/{infoJobId}": {"delete": {
+            "summary": "Callback for Information Job deletion",
+            "description": "The call is invoked to terminate a data subscription. The endpoint is provided by the Information Producer.",
+            "operationId": "jobDeletedCallback",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+            }},
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "infoJobId",
+                "required": true
+            }],
+            "tags": ["Data producer (callbacks)"]
+        }},
         "/data-producer/v1/info-types": {"get": {
             "summary": "Info Type identifiers",
             "operationId": "getInfoTypdentifiers",
             }],
             "tags": ["A1-EI (registration)"]
         }},
+        "/example_dataproducer/info_job": {"post": {
+            "summary": "Callback for Information Job creation/modification",
+            "requestBody": {
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_info_job_request"}}},
+                "required": true
+            },
+            "description": "The call is invoked to activate or to modify a data subscription. The endpoint is provided by the Information Producer.",
+            "operationId": "jobCreatedCallback",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+            }},
+            "tags": ["Data producer (callbacks)"]
+        }},
         "/data-producer/v1/info-types/{infoTypeId}": {
             "get": {
                 "summary": "Individual Information Type",
                 "tags": ["Data producer (registration)"]
             }
         },
+        "/data-consumer/v1/info-type-subscription/{subscriptionId}": {
+            "get": {
+                "summary": "Individual subscription for information types (registration/deregistration)",
+                "operationId": "getIndividualTypeSubscription",
+                "responses": {
+                    "200": {
+                        "description": "Type subscription",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_type_subscription_info"}}}
+                    },
+                    "404": {
+                        "description": "Subscription is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "subscriptionId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            },
+            "delete": {
+                "summary": "Individual subscription for information types (registration/deregistration)",
+                "operationId": "deleteIndividualTypeSubscription",
+                "responses": {
+                    "200": {
+                        "description": "Not used",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "204": {
+                        "description": "Subscription deleted",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "404": {
+                        "description": "Subscription is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "subscriptionId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            },
+            "put": {
+                "summary": "Individual subscription for information types (registration/deregistration)",
+                "requestBody": {
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_type_subscription_info"}}},
+                    "required": true
+                },
+                "description": "This service operation is used to subscribe to notifications for changes in the availability of data types.",
+                "operationId": "putIndividualTypeSubscription",
+                "responses": {
+                    "200": {
+                        "description": "Subscription updated",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "201": {
+                        "description": "Subscription created",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "subscriptionId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            }
+        },
+        "/example_dataproducer/health_check": {"get": {
+            "summary": "Producer supervision",
+            "description": "The endpoint is provided by the Information Producer and is used for supervision of the producer.",
+            "operationId": "producerSupervision",
+            "responses": {"200": {
+                "description": "The producer is OK",
+                "content": {"application/json": {"schema": {"type": "string"}}}
+            }},
+            "tags": ["Data producer (callbacks)"]
+        }},
         "/A1-EI/v1/eitypes": {"get": {
             "summary": "EI type identifiers",
             "operationId": "getEiTypeIdentifiers",
                 "tags": ["Data producer (registration)"]
             }
         },
-        "/producer_simulator/info_job/{infoJobId}": {"delete": {
-            "summary": "Callback for Information Job deletion",
-            "description": "The call is invoked to terminate a data subscription. The endpoint is provided by the Information Producer.",
-            "operationId": "jobDeletedCallback",
-            "responses": {"200": {
-                "description": "OK",
-                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
-            }},
-            "parameters": [{
-                "schema": {"type": "string"},
-                "in": "path",
-                "name": "infoJobId",
-                "required": true
-            }],
-            "tags": ["Data producer (callbacks)"]
-        }},
         "/status": {"get": {
             "summary": "Returns status and statistics of this service",
             "operationId": "getStatus",
             "responses": {"200": {
                 "description": "Service is living",
-                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/status_info"}}}
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/service_status_info"}}}
             }},
             "tags": ["Service status"]
         }},
+        "/data-consumer/v1/info-type-subscription": {"get": {
+            "summary": "Information type subscription identifiers",
+            "description": "query for information type subscription identifiers",
+            "operationId": "getInfoTypeSubscriptions",
+            "responses": {"200": {
+                "description": "Information type subscription identifiers",
+                "content": {"application/json": {"schema": {
+                    "type": "array",
+                    "items": {"type": "string"}
+                }}}
+            }},
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "query",
+                "name": "owner",
+                "description": "selects result for one owner",
+                "required": false
+            }],
+            "tags": ["Data consumer"]
+        }},
         "/A1-EI/v1/eijobs/{eiJobId}": {
             "get": {
                 "summary": "Individual EI job",
                 "tags": ["A1-EI (registration)"]
             }
         },
-        "/producer_simulator/health_check": {"get": {
-            "summary": "Producer supervision",
-            "description": "The endpoint is provided by the Information Producer and is used for supervision of the producer.",
-            "operationId": "producerSupervision",
-            "responses": {"200": {
-                "description": "The producer is OK",
-                "content": {"application/json": {"schema": {"type": "string"}}}
-            }},
-            "tags": ["Data producer (callbacks)"]
-        }},
         "/data-consumer/v1/info-jobs": {"get": {
             "summary": "Information Job identifiers",
             "description": "query for information job identifiers",
                     "schema": {"type": "string"},
                     "in": "query",
                     "name": "owner",
-                    "description": "selects subscription jobs for one job owner",
+                    "description": "selects result for one owner",
                     "required": false
                 }
             ],
             }],
             "tags": ["Data consumer"]
         }},
-        "/producer_simulator/info_job": {"post": {
-            "summary": "Callback for Information Job creation/modification",
-            "requestBody": {
-                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_info_job_request"}}},
-                "required": true
-            },
-            "description": "The call is invoked to activate or to modify a data subscription. The endpoint is provided by the Information Producer.",
-            "operationId": "jobCreatedCallback",
-            "responses": {"200": {
-                "description": "OK",
-                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
-            }},
-            "tags": ["Data producer (callbacks)"]
-        }},
         "/example_dataconsumer/info_jobs/{infoJobId}/status": {"post": {
             "summary": "Callback for changed Information Job status",
             "requestBody": {
                 "required": true
             }],
             "tags": ["Data consumer"]
+        }},
+        "/example_dataconsumer/info_type_status": {"post": {
+            "summary": "Callback for changed Information type registration status",
+            "requestBody": {
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_type_registration_info"}}},
+                "required": true
+            },
+            "description": "The primitive is implemented by the data consumer and is invoked when a Information type status has been changed. <br/>Subscription are managed by primitives in 'Data consumer'",
+            "operationId": "typeStatusCallback",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+            }},
+            "tags": ["Data consumer (callbacks)"]
         }}
     },
     "info": {
             "name": "Copyright (C) 2020 Nordix Foundation. Licensed under the Apache License.",
             "url": "http://www.apache.org/licenses/LICENSE-2.0"
         },
-        "description": "<h1>API documentation<\/h1><h2>General<\/h2><p>  The service is mainly a broker between data producers and data consumers. A data producer has the ability to produce one or several types of data (Information Type). One type of data can be produced by zero to many producers. <br /><br />A data consumer can have several active data subscriptions (Information Job). One Information Job consists of the type of data to produce and additional parameters for filtering of the data. These parameters are different for different data types.<\/p><h2>APIs provided by the service<\/h2><h4>A1-EI<\/h4><p>  This API is between Near-RT RIC and the Non-RT RIC.  The Near-RT RIC is a data consumer, which creates Information Jobs to subscribe for data.  In this context, the information is referred to as 'Enrichment Information', EI.<\/p><h4>Data producer API<\/h4><p>  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The API is for use by different kinds of data producers and provides support for:<ul><li>Registry of supported information types and which parameters needed to setup a subscription.<\/li><li>Registry of existing data producers.<\/li><li>Callback API provided by producers to setup subscriptions.<\/li><\/ul><\/p><h4>Data consumer API<\/h4><p>  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The API is for use by different kinds of data consumers and provides support for:<ul><li>Querying of available types of data to consume.<\/li><li>Management of data subscription jobs<\/li><\/ul><\/p><h4>Service status<\/h4><p>  This API provides a means to monitor the health of this service.<\/p>",
+        "description": "<h1>API documentation<\/h1><h2>General<\/h2><p>  The service is mainly a broker between data producers and data consumers. A data producer has the ability to produce one or several types of data (Information Type). One type of data can be produced by zero to many producers. <br /><br />A data consumer can have several active data subscriptions (Information Job). One Information Job consists of the type of data to produce and additional parameters for filtering of the data. These parameters are different for different data types.<\/p><h2>APIs provided by the service<\/h2><h4>A1-EI<\/h4><p>  This API is between Near-RT RIC and the Non-RT RIC.  The Near-RT RIC is a data consumer, which creates Information Jobs to subscribe for data.  In this context, the information is referred to as 'Enrichment Information', EI.<\/p><h4>Data producer API<\/h4><p>  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The API is for use by different kinds of data producers and provides support for:<ul><li>Registry of supported information types and which parameters needed to setup a subscription.<\/li><li>Registry of existing data producers.<\/li><li>Callback API provided by producers to setup subscriptions.<\/li><\/ul><\/p><h4>Data consumer API<\/h4><p>  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The API is for use by different kinds of data consumers and provides support for:<ul><li>Querying of available types of data to consume.<\/li><li>Management of data subscription jobs<\/li><li>Optional callback API provided by consumers to get notification on added and removed information types.<\/li><\/ul><\/p><h4>Service status<\/h4><p>  This API provides a means to monitor the health of this service.<\/p>",
         "title": "Data management and exposure",
         "version": "1.0"
     },
index 6318ee1..3f2a1cd 100644 (file)
@@ -20,8 +20,9 @@ info:
     by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The
     API is for use by different kinds of data consumers and provides support for:<ul><li>Querying
     of available types of data to consume.</li><li>Management of data subscription
-    jobs</li></ul></p><h4>Service status</h4><p>  This API provides a means to monitor
-    the health of this service.</p>
+    jobs</li><li>Optional callback API provided by consumers to get notification on
+    added and removed information types.</li></ul></p><h4>Service status</h4><p>  This
+    API provides a means to monitor the health of this service.</p>
   license:
     name: Copyright (C) 2020 Nordix Foundation. Licensed under the Apache License.
     url: http://www.apache.org/licenses/LICENSE-2.0
@@ -42,6 +43,29 @@ tags:
 - name: Data consumer
   description: API for data consumers
 paths:
+  /example_dataproducer/info_job/{infoJobId}:
+    delete:
+      tags:
+      - Data producer (callbacks)
+      summary: Callback for Information Job deletion
+      description: The call is invoked to terminate a data subscription. The endpoint
+        is provided by the Information Producer.
+      operationId: jobDeletedCallback
+      parameters:
+      - name: infoJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
   /data-producer/v1/info-types:
     get:
       tags:
@@ -84,6 +108,27 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ProblemDetails'
+  /example_dataproducer/info_job:
+    post:
+      tags:
+      - Data producer (callbacks)
+      summary: Callback for Information Job creation/modification
+      description: The call is invoked to activate or to modify a data subscription.
+        The endpoint is provided by the Information Producer.
+      operationId: jobCreatedCallback
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/producer_info_job_request'
+        required: true
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
   /data-producer/v1/info-types/{infoTypeId}:
     get:
       tags:
@@ -187,6 +232,114 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ProblemDetails'
+  /data-consumer/v1/info-type-subscription/{subscriptionId}:
+    get:
+      tags:
+      - Data consumer
+      summary: Individual subscription for information types (registration/deregistration)
+      operationId: getIndividualTypeSubscription
+      parameters:
+      - name: subscriptionId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Type subscription
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/consumer_type_subscription_info'
+        404:
+          description: Subscription is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    put:
+      tags:
+      - Data consumer
+      summary: Individual subscription for information types (registration/deregistration)
+      description: This service operation is used to subscribe to notifications for
+        changes in the availability of data types.
+      operationId: putIndividualTypeSubscription
+      parameters:
+      - name: subscriptionId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/consumer_type_subscription_info'
+        required: true
+      responses:
+        200:
+          description: Subscription updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        201:
+          description: Subscription created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+    delete:
+      tags:
+      - Data consumer
+      summary: Individual subscription for information types (registration/deregistration)
+      operationId: deleteIndividualTypeSubscription
+      parameters:
+      - name: subscriptionId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Not used
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        204:
+          description: Subscription deleted
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        404:
+          description: Subscription is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /example_dataproducer/health_check:
+    get:
+      tags:
+      - Data producer (callbacks)
+      summary: Producer supervision
+      description: The endpoint is provided by the Information Producer and is used
+        for supervision of the producer.
+      operationId: producerSupervision
+      responses:
+        200:
+          description: The producer is OK
+          content:
+            application/json:
+              schema:
+                type: string
   /A1-EI/v1/eitypes:
     get:
       tags:
@@ -314,42 +467,44 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ProblemDetails'
-  /producer_simulator/info_job/{infoJobId}:
-    delete:
+  /status:
+    get:
       tags:
-      - Data producer (callbacks)
-      summary: Callback for Information Job deletion
-      description: The call is invoked to terminate a data subscription. The endpoint
-        is provided by the Information Producer.
-      operationId: jobDeletedCallback
-      parameters:
-      - name: infoJobId
-        in: path
-        required: true
-        style: simple
-        explode: false
-        schema:
-          type: string
+      - Service status
+      summary: Returns status and statistics of this service
+      operationId: getStatus
       responses:
         200:
-          description: OK
+          description: Service is living
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/Void'
-  /status:
+                $ref: '#/components/schemas/service_status_info'
+  /data-consumer/v1/info-type-subscription:
     get:
       tags:
-      - Service status
-      summary: Returns status and statistics of this service
-      operationId: getStatus
+      - Data consumer
+      summary: Information type subscription identifiers
+      description: query for information type subscription identifiers
+      operationId: getInfoTypeSubscriptions
+      parameters:
+      - name: owner
+        in: query
+        description: selects result for one owner
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: string
       responses:
         200:
-          description: Service is living
+          description: Information type subscription identifiers
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/status_info'
+                type: array
+                items:
+                  type: string
   /A1-EI/v1/eijobs/{eiJobId}:
     get:
       tags:
@@ -447,21 +602,6 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ProblemDetails'
-  /producer_simulator/health_check:
-    get:
-      tags:
-      - Data producer (callbacks)
-      summary: Producer supervision
-      description: The endpoint is provided by the Information Producer and is used
-        for supervision of the producer.
-      operationId: producerSupervision
-      responses:
-        200:
-          description: The producer is OK
-          content:
-            application/json:
-              schema:
-                type: string
   /data-consumer/v1/info-jobs:
     get:
       tags:
@@ -480,7 +620,7 @@ paths:
           type: string
       - name: owner
         in: query
-        description: selects subscription jobs for one job owner
+        description: selects result for one owner
         required: false
         style: form
         explode: true
@@ -660,27 +800,6 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ProblemDetails'
-  /producer_simulator/info_job:
-    post:
-      tags:
-      - Data producer (callbacks)
-      summary: Callback for Information Job creation/modification
-      description: The call is invoked to activate or to modify a data subscription.
-        The endpoint is provided by the Information Producer.
-      operationId: jobCreatedCallback
-      requestBody:
-        content:
-          application/json:
-            schema:
-              $ref: '#/components/schemas/producer_info_job_request'
-        required: true
-      responses:
-        200:
-          description: OK
-          content:
-            application/json:
-              schema:
-                $ref: '#/components/schemas/Void'
   /example_dataconsumer/info_jobs/{infoJobId}/status:
     post:
       tags:
@@ -860,13 +979,49 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/ProblemDetails'
+  /example_dataconsumer/info_type_status:
+    post:
+      tags:
+      - Data consumer (callbacks)
+      summary: Callback for changed Information type registration status
+      description: The primitive is implemented by the data consumer and is invoked
+        when a Information type status has been changed. <br/>Subscription are managed
+        by primitives in 'Data consumer'
+      operationId: typeStatusCallback
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/consumer_type_registration_info'
+        required: true
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
 components:
   schemas:
     consumer_information_type:
       required:
       - job_data_schema
+      - no_of_producers
+      - type_status
       type: object
       properties:
+        no_of_producers:
+          type: integer
+          description: The number of registered producers for the type
+          format: int32
+        type_status:
+          type: string
+          description: 'Allowed values: <br/>ENABLED: one or several producers for
+            the information type are available <br/>DISABLED: no producers for the
+            information type are available'
+          enum:
+          - ENABLED
+          - DISABLED
         job_data_schema:
           type: object
           description: Json schema for the job data
@@ -874,7 +1029,7 @@ components:
     EiTypeObject:
       type: object
       description: Information for an EI type
-    status_info:
+    service_status_info:
       required:
       - no_of_jobs
       - no_of_producers
@@ -917,6 +1072,27 @@ components:
           type: string
           description: callback for Information Job
       description: Information for an Information Producer
+    consumer_type_registration_info:
+      required:
+      - info_type_id
+      - job_data_schema
+      - status
+      type: object
+      properties:
+        info_type_id:
+          type: string
+          description: Information type identifier
+        job_data_schema:
+          type: object
+          description: Json schema for the job data
+        status:
+          type: string
+          description: 'Allowed values: <br/>REGISTERED: the information type has
+            been registered <br/>DEREGISTERED: the information type has been removed'
+          enum:
+          - REGISTERED
+          - DEREGISTERED
+      description: Information for an Information type
     ProblemDetails:
       type: object
       properties:
@@ -940,7 +1116,9 @@ components:
       properties:
         eiJobStatus:
           type: string
-          description: Allowed values for EI job status
+          description: 'Allowed values for EI job status: <br/>ENABLED: the A1-EI
+            producer is able to deliver EI result for the EI job <br/>DISABLED: the
+            A1-EI producer is unable to deliver EI result for the EI job'
           enum:
           - ENABLED
           - DISABLED
@@ -953,16 +1131,18 @@ components:
       properties:
         info_job_status:
           type: string
-          description: Allowed values for Information Job status
+          description: 'Allowed values: <br/>ENABLED: the A1-Information producer
+            is able to deliver result for the Information Job <br/>DISABLED: the A1-Information
+            producer is unable to deliver result for the Information Job'
           enum:
           - ENABLED
           - DISABLED
         producers:
           type: array
-          description: An array of all registerred Information Producer Identifiers.
+          description: An array of all registered Information Producer Identifiers.
           items:
             type: string
-            description: An array of all registerred Information Producer Identifiers.
+            description: An array of all registered Information Producer Identifiers.
       description: Status for an Information Job
     EiJobObject:
       required:
@@ -1045,7 +1225,7 @@ components:
         status_notification_uri:
           type: string
           description: The target of Information subscription job status notifications
-      description: Information for an Enrichment  Information Job
+      description: Information for an Enrichment Information Job
     producer_status:
       required:
       - operational_state
@@ -1061,3 +1241,16 @@ components:
     Void:
       type: object
       description: 'Void/empty '
+    consumer_type_subscription_info:
+      required:
+      - owner
+      - status_result_uri
+      type: object
+      properties:
+        owner:
+          type: string
+          description: Identity of the owner of the subscription
+        status_result_uri:
+          type: string
+          description: The target URI of the subscribed information
+      description: Information for an information type subscription
index 6b5edc0..5f61e31 100644 (file)
@@ -89,6 +89,7 @@ public class SwaggerConfig {
         + "<ul>" //
         + "<li>Querying of available types of data to consume.</li>" //
         + "<li>Management of data subscription jobs</li>" //
+        + "<li>Optional callback API provided by consumers to get notification on added and removed information types.</li>" //
         + "</ul>" //
         + "</p>" //
         + "<h4>Service status</h4>" //
index 68b654e..9e21548 100644 (file)
@@ -59,7 +59,7 @@ public class StatusController {
     private InfoProducers infoProducers;
 
     @Gson.TypeAdapters
-    @Schema(name = "status_info")
+    @Schema(name = "service_status_info")
     public static class StatusInfo {
         @Schema(name = "status", description = "status text")
         @SerializedName("status")
index cc55865..dff0bf3 100644 (file)
@@ -32,13 +32,13 @@ import org.immutables.gson.Gson;
 public class A1eEiJobStatus {
 
     @Gson.TypeAdapters
-    @Schema(name = "EiJobStatusValues", description = "Allowed values for EI job status")
+    @Schema(name = "EiJobStatusValues", description = OPERATIONAL_STATE_DESCRIPTION)
     public enum EiJobStatusValues {
         ENABLED, DISABLED
     }
 
-    private static final String OPERATIONAL_STATE_DESCRIPTION = "values:\n" //
-        + "ENABLED: the A1-EI producer is able to deliver EI result for the EI job\n" //
+    private static final String OPERATIONAL_STATE_DESCRIPTION = "Allowed values for EI job status: <br/>" //
+        + "ENABLED: the A1-EI producer is able to deliver EI result for the EI job <br/>" //
         + "DISABLED: the A1-EI producer is unable to deliver EI result for the EI job";
 
     @Schema(name = "eiJobStatus", description = OPERATIONAL_STATE_DESCRIPTION, required = true)
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerCallbacks.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerCallbacks.java
new file mode 100644 (file)
index 0000000..97a829c
--- /dev/null
@@ -0,0 +1,81 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2021 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controllers.r1consumer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.lang.invoke.MethodHandles;
+
+import org.oransc.enrichment.clients.AsyncRestClient;
+import org.oransc.enrichment.clients.AsyncRestClientFactory;
+import org.oransc.enrichment.configuration.ApplicationConfig;
+import org.oransc.enrichment.repository.InfoType;
+import org.oransc.enrichment.repository.InfoTypeSubscriptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * Callbacks to the Consumer. Notifies consumer according to the API (which this
+ * class adapts to)
+ */
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
+@Component
+public class ConsumerCallbacks implements InfoTypeSubscriptions.Callbacks {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private static Gson gson = new GsonBuilder().create();
+
+    private final AsyncRestClient restClient;
+
+    public ConsumerCallbacks(@Autowired ApplicationConfig config) {
+        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config.getWebClientConfig());
+        this.restClient = restClientFactory.createRestClientNoHttpProxy("");
+    }
+
+    @Override
+    public void notifyTypeRegistered(InfoType type, InfoTypeSubscriptions.SubscriptionInfo subscriptionInfo) {
+        ConsumerTypeRegistrationInfo info = new ConsumerTypeRegistrationInfo(type.getJobDataSchema(),
+            ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.REGISTERED, type.getId());
+        String body = gson.toJson(info);
+
+        post(subscriptionInfo.getCallbackUrl(), body);
+
+    }
+
+    @Override
+    public void notifyTypeRemoved(InfoType type, InfoTypeSubscriptions.SubscriptionInfo subscriptionInfo) {
+        ConsumerTypeRegistrationInfo info = new ConsumerTypeRegistrationInfo(type.getJobDataSchema(),
+            ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.DEREGISTERED, type.getId());
+        String body = gson.toJson(info);
+        post(subscriptionInfo.getCallbackUrl(), body);
+
+    }
+
+    private void post(String url, String body) {
+        restClient.post(url, body) //
+            .subscribe(response -> logger.debug("Post OK {}", url), //
+                throwable -> logger.warn("Post failed for consumer callback {} {}", url, body), null);
+    }
+
+}
index 6ddd583..2fa00e1 100644 (file)
@@ -25,10 +25,11 @@ public class ConsumerConsts {
     public static final String API_ROOT = "/data-consumer/v1";
 
     public static final String CONSUMER_API_NAME = "Data consumer";
+    public static final String CONSUMER_API_CALLBACKS_NAME = "Data consumer (callbacks)";
     public static final String CONSUMER_API_DESCRIPTION = "API for data consumers";
 
     public static final String OWNER_PARAM = "owner";
-    public static final String OWNER_PARAM_DESCRIPTION = "selects subscription jobs for one job owner";
+    public static final String OWNER_PARAM_DESCRIPTION = "selects result for one owner";
 
     public static final String INDIVIDUAL_JOB = "Individual data subscription job";
 
@@ -42,6 +43,12 @@ public class ConsumerConsts {
     public static final String PERFORM_TYPE_CHECK_PARAM_DESCRIPTION =
         "when true, a validation of that the type exists and that the job matches the type schema.";
 
+    public static final String INDIVIDUAL_TYPE_SUBSCRIPTION =
+        "Individual subscription for information types (registration/deregistration)";
+
+    public static final String TYPE_SUBSCRIPTION_DESCRIPTION =
+        "This service operation is used to subscribe to notifications for changes in the availability of data types.";
+
     private ConsumerConsts() {
     }
 }
index 9f16728..fd1901e 100644 (file)
@@ -48,8 +48,10 @@ import org.oransc.enrichment.controllers.r1producer.ProducerCallbacks;
 import org.oransc.enrichment.exceptions.ServiceException;
 import org.oransc.enrichment.repository.InfoJob;
 import org.oransc.enrichment.repository.InfoJobs;
+import org.oransc.enrichment.repository.InfoProducer;
 import org.oransc.enrichment.repository.InfoProducers;
 import org.oransc.enrichment.repository.InfoType;
+import org.oransc.enrichment.repository.InfoTypeSubscriptions;
 import org.oransc.enrichment.repository.InfoTypes;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -88,7 +90,13 @@ public class ConsumerController {
     private InfoProducers infoProducers;
 
     @Autowired
-    ProducerCallbacks producerCallbacks;
+    private ConsumerCallbacks consumerCallbacks;
+
+    @Autowired
+    private ProducerCallbacks producerCallbacks;
+
+    @Autowired
+    private InfoTypeSubscriptions infoTypeSubscriptions;
 
     private static Gson gson = new GsonBuilder().create();
 
@@ -305,6 +313,132 @@ public class ConsumerController {
             .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.NOT_FOUND)));
     }
 
+    @GetMapping(path = "/info-type-subscription", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Information type subscription identifiers",
+        description = "query for information type subscription identifiers")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information type subscription identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))),})
+    public ResponseEntity<Object> getInfoTypeSubscriptions( //
+
+        @Parameter(
+            name = ConsumerConsts.OWNER_PARAM,
+            required = false, //
+            description = ConsumerConsts.OWNER_PARAM_DESCRIPTION) //
+        @RequestParam(name = ConsumerConsts.OWNER_PARAM, required = false) String owner) {
+        try {
+            List<String> result = new ArrayList<>();
+            if (owner != null) {
+                this.infoTypeSubscriptions.getSubscriptionsForOwner(owner)
+                    .forEach(subscription -> result.add(subscription.getId()));
+            } else {
+                this.infoTypeSubscriptions.getAllSubscriptions()
+                    .forEach(subscription -> result.add(subscription.getId()));
+            }
+            return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/info-type-subscription/{subscriptionId}", produces = MediaType.APPLICATION_JSON_VALUE) //
+    @Operation(summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION, description = "") //
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Type subscription", //
+                content = @Content(schema = @Schema(implementation = ConsumerTypeSubscriptionInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Subscription is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getIndividualTypeSubscription( //
+        @PathVariable("subscriptionId") String subscriptionId) {
+        try {
+            InfoTypeSubscriptions.SubscriptionInfo subscription =
+                this.infoTypeSubscriptions.getSubscription(subscriptionId);
+            return new ResponseEntity<>(gson.toJson(toTypeSuscriptionInfo(subscription)), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PutMapping(
+        path = "/info-type-subscription/{subscriptionId}", //
+        produces = MediaType.APPLICATION_JSON_VALUE, //
+        consumes = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION,
+        description = ConsumerConsts.TYPE_SUBSCRIPTION_DESCRIPTION)
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "201",
+                description = "Subscription created", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Subscription updated", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public Mono<ResponseEntity<Object>> putIndividualTypeSubscription( //
+        @PathVariable("subscriptionId") String subscriptionId, //
+        @RequestBody ConsumerTypeSubscriptionInfo subscription) {
+
+        final boolean isNewSubscription = this.infoTypeSubscriptions.get(subscriptionId) == null;
+        this.infoTypeSubscriptions.put(toTypeSuscriptionInfo(subscription, subscriptionId));
+        return Mono.just(new ResponseEntity<>(isNewSubscription ? HttpStatus.CREATED : HttpStatus.OK));
+    }
+
+    @DeleteMapping(path = "/info-type-subscription/{subscriptionId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION, description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Not used", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "204",
+                description = "Subscription deleted", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "404",
+                description = "Subscription is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> deleteIndividualTypeSubscription( //
+        @PathVariable("subscriptionId") String subscriptionId) {
+        try {
+            InfoTypeSubscriptions.SubscriptionInfo subscription =
+                this.infoTypeSubscriptions.getSubscription(subscriptionId);
+            this.infoTypeSubscriptions.remove(subscription);
+            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    private ConsumerTypeSubscriptionInfo toTypeSuscriptionInfo(InfoTypeSubscriptions.SubscriptionInfo s) {
+        return new ConsumerTypeSubscriptionInfo(s.getCallbackUrl(), s.getOwner());
+    }
+
+    private InfoTypeSubscriptions.SubscriptionInfo toTypeSuscriptionInfo(ConsumerTypeSubscriptionInfo s,
+        String subscriptionId) {
+        return InfoTypeSubscriptions.SubscriptionInfo.builder() //
+            .callback(this.consumerCallbacks) //
+            .owner(s.owner) //
+            .id(subscriptionId) //
+            .callbackUrl(s.statusResultUri).build();
+
+    }
+
     private Mono<InfoJob> startInfoSubscriptionJob(InfoJob newInfoJob) {
         return this.producerCallbacks.startInfoSubscriptionJob(newInfoJob, infoProducers) //
             .doOnNext(noOfAcceptingProducers -> this.logger.debug("Started job {}, number of activated producers: {}",
@@ -370,7 +504,17 @@ public class ConsumerController {
     }
 
     private ConsumerInfoTypeInfo toInfoTypeInfo(InfoType type) {
-        return new ConsumerInfoTypeInfo(type.getJobDataSchema());
+        return new ConsumerInfoTypeInfo(type.getJobDataSchema(), typeStatus(type),
+            this.infoProducers.getProducerIdsForType(type.getId()).size());
+    }
+
+    private ConsumerInfoTypeInfo.ConsumerTypeStatusValues typeStatus(InfoType type) {
+        for (InfoProducer producer : this.infoProducers.getProducersForType(type)) {
+            if (producer.isAvailable()) {
+                return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.ENABLED;
+            }
+        }
+        return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.DISABLED;
     }
 
     private ConsumerJobInfo toInfoJobInfo(InfoJob s) {
index c227a20..92ee19b 100644 (file)
@@ -2,7 +2,7 @@
  * ========================LICENSE_START=================================
  * O-RAN-SC
  * %%
- * Copyright (C) 2020 Nordix Foundation
+ * Copyright (C) 2021 Nordix Foundation
  * %%
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -36,8 +36,30 @@ public class ConsumerInfoTypeInfo {
     @JsonProperty(value = "job_data_schema", required = true)
     public Object jobDataSchema;
 
-    public ConsumerInfoTypeInfo(Object jobDataSchema) {
+    @Gson.TypeAdapters
+    @Schema(name = "consumer_type_status_values", description = STATUS_DESCRIPTION)
+    public enum ConsumerTypeStatusValues {
+        ENABLED, DISABLED
+    }
+
+    private static final String STATUS_DESCRIPTION = "Allowed values: <br/>" //
+        + "ENABLED: one or several producers for the information type are available <br/>" //
+        + "DISABLED: no producers for the information type are available";
+
+    @Schema(name = "type_status", description = STATUS_DESCRIPTION, required = true)
+    @SerializedName("type_status")
+    @JsonProperty(value = "type_status", required = true)
+    public ConsumerTypeStatusValues state;
+
+    @Schema(name = "no_of_producers", description = "The number of registered producers for the type", required = true)
+    @SerializedName("no_of_producers")
+    @JsonProperty(value = "no_of_producers", required = true)
+    public int noOfProducers;
+
+    public ConsumerInfoTypeInfo(Object jobDataSchema, ConsumerTypeStatusValues state, int noOfProducers) {
         this.jobDataSchema = jobDataSchema;
+        this.state = state;
+        this.noOfProducers = noOfProducers;
     }
 
     public ConsumerInfoTypeInfo() {
index bc37628..4f91d64 100644 (file)
@@ -28,7 +28,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import org.immutables.gson.Gson;
 
 @Gson.TypeAdapters
-@Schema(name = "consumer_job", description = "Information for an Enrichment  Information Job")
+@Schema(name = "consumer_job", description = "Information for an Enrichment Information Job")
 public class ConsumerJobInfo {
 
     @Schema(
index 84f605b..3e8bb88 100644 (file)
@@ -34,16 +34,16 @@ import org.immutables.gson.Gson;
 public class ConsumerJobStatus {
 
     @Gson.TypeAdapters
-    @Schema(name = "info_job_status_values", description = "Allowed values for Information Job status")
+    @Schema(name = "info_job_status_values", description = OPERATIONAL_STATE_DESCRIPTION)
     public enum InfoJobStatusValues {
         ENABLED, DISABLED
     }
 
-    private static final String OPERATIONAL_STATE_DESCRIPTION = "values:\n" //
-        + "ENABLED: the A1-Information producer is able to deliver result for the Information Job\n" //
+    private static final String OPERATIONAL_STATE_DESCRIPTION = "Allowed values: <br/>" //
+        + "ENABLED: the A1-Information producer is able to deliver result for the Information Job <br/>" //
         + "DISABLED: the A1-Information producer is unable to deliver result for the Information Job";
 
-    private static final String PRODUCERS_DESCRIPTION = "An array of all registerred Information Producer Identifiers.";
+    private static final String PRODUCERS_DESCRIPTION = "An array of all registered Information Producer Identifiers.";
 
     @Schema(name = "info_job_status", description = OPERATIONAL_STATE_DESCRIPTION, required = true)
     @SerializedName("info_job_status")
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerTypeRegistrationInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerTypeRegistrationInfo.java
new file mode 100644 (file)
index 0000000..3d1533f
--- /dev/null
@@ -0,0 +1,68 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2021 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controllers.r1consumer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "consumer_type_registration_info", description = "Information for an Information type")
+public class ConsumerTypeRegistrationInfo {
+
+    @Schema(name = "info_type_id", description = "Information type identifier", required = true)
+    @SerializedName("info_type_id")
+    @JsonProperty(value = "info_type_id", required = true)
+    public String infoTypeId;
+
+    @Schema(name = "job_data_schema", description = "Json schema for the job data", required = true)
+    @SerializedName("job_data_schema")
+    @JsonProperty(value = "job_data_schema", required = true)
+    public Object jobDataSchema;
+
+    @Gson.TypeAdapters
+    @Schema(name = "consumer_type_registration_values", description = REGISTRATION_DESCRIPTION)
+    public enum ConsumerTypeStatusValues {
+        REGISTERED, DEREGISTERED
+    }
+
+    private static final String REGISTRATION_DESCRIPTION = "Allowed values: <br/>" //
+        + "REGISTERED: the information type has been registered <br/>" //
+        + "DEREGISTERED: the information type has been removed";
+
+    @Schema(name = "status", description = REGISTRATION_DESCRIPTION, required = true)
+    @SerializedName("status")
+    @JsonProperty(value = "status", required = true)
+    public ConsumerTypeStatusValues state;
+
+    public ConsumerTypeRegistrationInfo(Object jobDataSchema, ConsumerTypeStatusValues state, String infoTypeId) {
+        this.jobDataSchema = jobDataSchema;
+        this.state = state;
+        this.infoTypeId = infoTypeId;
+    }
+
+    public ConsumerTypeRegistrationInfo() {
+    }
+
+}
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerTypeSubscriptionInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/r1consumer/ConsumerTypeSubscriptionInfo.java
new file mode 100644 (file)
index 0000000..a0c4722
--- /dev/null
@@ -0,0 +1,53 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2021 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.controllers.r1consumer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.EqualsAndHashCode;
+
+import org.immutables.gson.Gson;
+
+@EqualsAndHashCode
+@Gson.TypeAdapters
+@Schema(name = "consumer_type_subscription_info", description = "Information for an information type subscription")
+public class ConsumerTypeSubscriptionInfo {
+
+    @Schema(name = "status_result_uri", description = "The target URI of the subscribed information", required = true)
+    @SerializedName("status_result_uri")
+    @JsonProperty(value = "status_result_uri", required = true)
+    public String statusResultUri = "";
+
+    @Schema(name = "owner", description = "Identity of the owner of the subscription", required = true)
+    @SerializedName("owner")
+    @JsonProperty(value = "owner", required = true)
+    public String owner = "";
+
+    public ConsumerTypeSubscriptionInfo() {
+    }
+
+    public ConsumerTypeSubscriptionInfo(String statusResultUri, String owner) {
+        this.statusResultUri = statusResultUri;
+        this.owner = owner;
+    }
+}
index 15bd56d..29426ab 100644 (file)
@@ -46,6 +46,7 @@ import org.oransc.enrichment.repository.InfoJobs;
 import org.oransc.enrichment.repository.InfoProducer;
 import org.oransc.enrichment.repository.InfoProducers;
 import org.oransc.enrichment.repository.InfoType;
+import org.oransc.enrichment.repository.InfoTypeSubscriptions;
 import org.oransc.enrichment.repository.InfoTypes;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
@@ -75,6 +76,9 @@ public class ProducerController {
     @Autowired
     private InfoProducers infoProducers;
 
+    @Autowired
+    private InfoTypeSubscriptions typeSubscriptions;
+
     @GetMapping(path = ProducerConsts.API_ROOT + "/info-types", produces = MediaType.APPLICATION_JSON_VALUE) //
     @Operation(summary = "Info Type identifiers", description = "") //
     @ApiResponses(
@@ -145,7 +149,9 @@ public class ProducerController {
         if (registrationInfo.jobDataSchema == null) {
             return ErrorResponse.create("No schema provided", HttpStatus.BAD_REQUEST);
         }
-        this.infoTypes.put(new InfoType(infoTypeId, registrationInfo.jobDataSchema));
+        InfoType newDefinition = new InfoType(infoTypeId, registrationInfo.jobDataSchema);
+        this.infoTypes.put(newDefinition);
+        this.typeSubscriptions.notifyTypeRegistered(newDefinition);
         return new ResponseEntity<>(previousDefinition == null ? HttpStatus.CREATED : HttpStatus.OK);
     }
 
@@ -184,6 +190,7 @@ public class ProducerController {
             return ErrorResponse.create("The type has active producers: " + firstProducerId, HttpStatus.NOT_ACCEPTABLE);
         }
         this.infoTypes.remove(type);
+        this.typeSubscriptions.notifyTypeRemoved(type);
         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
     }
 
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/InfoTypeSubscriptions.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/InfoTypeSubscriptions.java
new file mode 100644 (file)
index 0000000..f54a849
--- /dev/null
@@ -0,0 +1,137 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.enrichment.repository;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import org.oransc.enrichment.exceptions.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Subscriptions of callbacks for type registrations
+ */
+@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
+@Component
+public class InfoTypeSubscriptions {
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private final Map<String, SubscriptionInfo> allSubscriptions = new HashMap<>();
+    private final MultiMap<SubscriptionInfo> subscriptionsByOwner = new MultiMap<>();
+
+    public interface Callbacks {
+        void notifyTypeRegistered(InfoType type, SubscriptionInfo subscriptionInfo);
+
+        void notifyTypeRemoved(InfoType type, SubscriptionInfo subscriptionInfo);
+    }
+
+    @Builder
+    @Getter
+    public static class SubscriptionInfo {
+        private String id;
+
+        private String callbackUrl;
+
+        private String owner;
+
+        private Callbacks callback;
+    }
+
+    public synchronized void put(SubscriptionInfo subscription) {
+        allSubscriptions.put(subscription.getId(), subscription);
+        subscriptionsByOwner.put(subscription.owner, subscription.id, subscription);
+        logger.debug("Added type status subscription {}", subscription.id);
+    }
+
+    public synchronized Collection<SubscriptionInfo> getAllSubscriptions() {
+        return new Vector<>(allSubscriptions.values());
+    }
+
+    /**
+     * Get a subscription and throw if not fond.
+     * 
+     * @param id the ID of the subscription to get.
+     * @return SubscriptionInfo
+     * @throws ServiceException if not found
+     */
+    public synchronized SubscriptionInfo getSubscription(String id) throws ServiceException {
+        SubscriptionInfo p = allSubscriptions.get(id);
+        if (p == null) {
+            throw new ServiceException("Could not find Information subscription: " + id);
+        }
+        return p;
+    }
+
+    /**
+     * Get a subscription or return null if not found. Equivalent to get in all java
+     * collections.
+     * 
+     * @param id the ID of the subscription to get.
+     * @return SubscriptionInfo
+     */
+    public synchronized SubscriptionInfo get(String id) {
+        return allSubscriptions.get(id);
+    }
+
+    public synchronized int size() {
+        return allSubscriptions.size();
+    }
+
+    public synchronized void clear() {
+        this.allSubscriptions.clear();
+        this.subscriptionsByOwner.clear();
+    }
+
+    public void remove(SubscriptionInfo subscription) {
+        allSubscriptions.remove(subscription.getId());
+        this.subscriptionsByOwner.remove(subscription.owner, subscription.id);
+        logger.debug("Removed type status subscription {}", subscription.id);
+    }
+
+    /**
+     * returns all subscriptions for an owner. The colllection can contain 0..n
+     * subscriptions.
+     * 
+     * @param owner
+     * @return
+     */
+    public synchronized Collection<SubscriptionInfo> getSubscriptionsForOwner(String owner) {
+        return this.subscriptionsByOwner.get(owner);
+    }
+
+    public synchronized void notifyTypeRegistered(InfoType type) {
+        this.allSubscriptions
+            .forEach((id, subscription) -> subscription.callback.notifyTypeRegistered(type, subscription));
+    }
+
+    public synchronized void notifyTypeRemoved(InfoType type) {
+        this.allSubscriptions
+            .forEach((id, subscription) -> subscription.callback.notifyTypeRemoved(type, subscription));
+    }
+
+}
index dceed3c..df938ad 100644 (file)
@@ -58,6 +58,8 @@ import org.oransc.enrichment.controllers.r1consumer.ConsumerConsts;
 import org.oransc.enrichment.controllers.r1consumer.ConsumerInfoTypeInfo;
 import org.oransc.enrichment.controllers.r1consumer.ConsumerJobInfo;
 import org.oransc.enrichment.controllers.r1consumer.ConsumerJobStatus;
+import org.oransc.enrichment.controllers.r1consumer.ConsumerTypeRegistrationInfo;
+import org.oransc.enrichment.controllers.r1consumer.ConsumerTypeSubscriptionInfo;
 import org.oransc.enrichment.controllers.r1producer.ProducerCallbacks;
 import org.oransc.enrichment.controllers.r1producer.ProducerConsts;
 import org.oransc.enrichment.controllers.r1producer.ProducerInfoTypeInfo;
@@ -70,6 +72,7 @@ import org.oransc.enrichment.repository.InfoJobs;
 import org.oransc.enrichment.repository.InfoProducer;
 import org.oransc.enrichment.repository.InfoProducers;
 import org.oransc.enrichment.repository.InfoType;
+import org.oransc.enrichment.repository.InfoTypeSubscriptions;
 import org.oransc.enrichment.repository.InfoTypes;
 import org.oransc.enrichment.tasks.ProducerSupervision;
 import org.slf4j.Logger;
@@ -135,6 +138,9 @@ class ApplicationTest {
     @Autowired
     ProducerCallbacks producerCallbacks;
 
+    @Autowired
+    InfoTypeSubscriptions infoTypeSubscriptions;
+
     private static Gson gson = new GsonBuilder().create();
 
     /**
@@ -227,6 +233,8 @@ class ApplicationTest {
         ConsumerInfoTypeInfo info = gson.fromJson(rsp, ConsumerInfoTypeInfo.class);
         assertThat(info).isNotNull();
         assertThat(info.jobDataSchema).isNotNull();
+        assertThat(info.state).isEqualTo(ConsumerInfoTypeInfo.ConsumerTypeStatusValues.ENABLED);
+        assertThat(info.noOfProducers).isEqualTo(1);
     }
 
     @Test
@@ -352,8 +360,9 @@ class ApplicationTest {
 
         String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId/status";
         String rsp = restClient().get(url).block();
-        assertThat(rsp).contains("ENABLED");
-        assertThat(rsp).contains(PRODUCER_ID);
+        assertThat(rsp) //
+            .contains("ENABLED") //
+            .contains(PRODUCER_ID);
 
         ConsumerJobStatus status = gson.fromJson(rsp, ConsumerJobStatus.class);
         assertThat(status.producers).contains(PRODUCER_ID);
@@ -546,11 +555,12 @@ class ApplicationTest {
     @Test
     void producerDeleteEiType() throws Exception {
         putInfoType(TYPE_ID);
-        String url = ProducerConsts.API_ROOT + "/info-types/" + TYPE_ID;
-        restClient().delete(url).block();
+        deleteInfoType(TYPE_ID);
+
         assertThat(this.infoTypes.size()).isZero();
 
-        testErrorCode(restClient().delete(url), HttpStatus.NOT_FOUND, "Information type not found");
+        testErrorCode(restClient().delete(deleteInfoTypeUrl(TYPE_ID)), HttpStatus.NOT_FOUND,
+            "Information type not found");
     }
 
     @Test
@@ -714,12 +724,13 @@ class ApplicationTest {
         deleteEiProducer("infoProducerId");
         assertThat(this.infoTypes.size()).isEqualTo(1); // The type remains
         assertThat(this.infoJobs.size()).isEqualTo(1); // The job remains
-        await().untilAsserted(() -> assertThat(consumerCalls.status.size()).isEqualTo(1));
-        assertThat(consumerCalls.status.get(0).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks.size()).isEqualTo(1));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(0).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
 
         putInfoProducerWithOneType("infoProducerId", TYPE_ID);
-        await().untilAsserted(() -> assertThat(consumerCalls.status.size()).isEqualTo(2));
-        assertThat(consumerCalls.status.get(1).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks.size()).isEqualTo(2));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(1).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
     }
 
     @Test
@@ -734,13 +745,14 @@ class ApplicationTest {
         putInfoProducerWithOneType(PRODUCER_ID, "junk");
         verifyJobStatus(EI_JOB_ID, "DISABLED");
         ConsumerSimulatorController.TestResults consumerCalls = this.consumerSimulator.getTestResults();
-        await().untilAsserted(() -> assertThat(consumerCalls.status.size()).isEqualTo(1));
-        assertThat(consumerCalls.status.get(0).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks.size()).isEqualTo(1));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(0).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
 
         putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
         verifyJobStatus(EI_JOB_ID, "ENABLED");
-        await().untilAsserted(() -> assertThat(consumerCalls.status.size()).isEqualTo(2));
-        assertThat(consumerCalls.status.get(1).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks.size()).isEqualTo(2));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(1).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
     }
 
     @Test
@@ -781,8 +793,9 @@ class ApplicationTest {
             verifyJobStatus(EI_JOB_ID, "ENABLED");
             deleteEiProducer(PRODUCER_ID);
             // A Job disabled status notification shall now be received
-            await().untilAsserted(() -> assertThat(consumerResults.status.size()).isEqualTo(1));
-            assertThat(consumerResults.status.get(0).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+            await().untilAsserted(() -> assertThat(consumerResults.eiJobStatusCallbacks.size()).isEqualTo(1));
+            assertThat(consumerResults.eiJobStatusCallbacks.get(0).state)
+                .isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
             verifyJobStatus(EI_JOB_ID, "DISABLED");
         }
 
@@ -797,7 +810,7 @@ class ApplicationTest {
         assertThat(this.infoProducers.size()).isEqualTo(1);
         assertProducerOpState("simulateProducerError", ProducerStatusInfo.OperationalState.DISABLED);
 
-        // After 3 failed checks, the producer shall be deregisterred
+        // After 3 failed checks, the producer shall be deregistered
         this.producerSupervision.createTask().blockLast();
         assertThat(this.infoProducers.size()).isZero(); // The producer is removed
         assertThat(this.infoTypes.size()).isEqualTo(1); // The type remains
@@ -805,8 +818,9 @@ class ApplicationTest {
         // Now we have one disabled job, and no producer.
         // PUT a producer, then a Job ENABLED status notification shall be received
         putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
-        await().untilAsserted(() -> assertThat(consumerResults.status.size()).isEqualTo(2));
-        assertThat(consumerResults.status.get(1).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+        await().untilAsserted(() -> assertThat(consumerResults.eiJobStatusCallbacks.size()).isEqualTo(2));
+        assertThat(consumerResults.eiJobStatusCallbacks.get(1).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
         verifyJobStatus(EI_JOB_ID, "ENABLED");
     }
 
@@ -829,8 +843,9 @@ class ApplicationTest {
         // Run the supervision and wait for the job to get started in the producer
         this.producerSupervision.createTask().blockLast();
         ConsumerSimulatorController.TestResults consumerResults = this.consumerSimulator.getTestResults();
-        await().untilAsserted(() -> assertThat(consumerResults.status.size()).isEqualTo(1));
-        assertThat(consumerResults.status.get(0).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+        await().untilAsserted(() -> assertThat(consumerResults.eiJobStatusCallbacks.size()).isEqualTo(1));
+        assertThat(consumerResults.eiJobStatusCallbacks.get(0).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
         verifyJobStatus(EI_JOB_ID, "ENABLED");
     }
 
@@ -904,6 +919,81 @@ class ApplicationTest {
         assertThat(this.infoJobs.size()).isZero();
     }
 
+    @Test
+    void testConsumerTypeSubscription() throws Exception {
+
+        final String callbackUrl = baseUrl() + ConsumerSimulatorController.getTypeStatusCallbackUrl();
+        final ConsumerTypeSubscriptionInfo info = new ConsumerTypeSubscriptionInfo(callbackUrl, "owner");
+
+        {
+            // PUT a subscription
+            String body = gson.toJson(info);
+            ResponseEntity<String> resp =
+                restClient().putForEntity(typeSubscriptionUrl() + "/subscriptionId", body).block();
+            assertThat(this.infoTypeSubscriptions.size()).isEqualTo(1);
+            assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+            resp = restClient().putForEntity(typeSubscriptionUrl() + "/subscriptionId", body).block();
+            assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+        }
+        {
+            // GET IDs
+            ResponseEntity<String> resp = restClient().getForEntity(typeSubscriptionUrl()).block();
+            assertThat(resp.getBody()).isEqualTo("[\"subscriptionId\"]");
+            resp = restClient().getForEntity(typeSubscriptionUrl() + "?owner=owner").block();
+            assertThat(resp.getBody()).isEqualTo("[\"subscriptionId\"]");
+            resp = restClient().getForEntity(typeSubscriptionUrl() + "?owner=junk").block();
+            assertThat(resp.getBody()).isEqualTo("[]");
+        }
+
+        {
+            // GET the individual subscription
+            ResponseEntity<String> resp = restClient().getForEntity(typeSubscriptionUrl() + "/subscriptionId").block();
+            ConsumerTypeSubscriptionInfo respInfo = gson.fromJson(resp.getBody(), ConsumerTypeSubscriptionInfo.class);
+            assertThat(respInfo).isEqualTo(info);
+        }
+
+        {
+            // Test the callbacks
+            final ConsumerSimulatorController.TestResults consumerCalls = this.consumerSimulator.getTestResults();
+
+            // Test callback for PUT type
+            this.putInfoType(TYPE_ID);
+            await().untilAsserted(() -> assertThat(consumerCalls.typeRegistrationInfoCallbacks.size()).isEqualTo(1));
+            assertThat(consumerCalls.typeRegistrationInfoCallbacks.get(0).state)
+                .isEqualTo(ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.REGISTERED);
+
+            // Test callback for DELETE type
+            this.deleteInfoType(TYPE_ID);
+            await().untilAsserted(() -> assertThat(consumerCalls.typeRegistrationInfoCallbacks.size()).isEqualTo(2));
+            assertThat(consumerCalls.typeRegistrationInfoCallbacks.get(1).state)
+                .isEqualTo(ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.DEREGISTERED);
+        }
+
+        {
+            // DELETE the subscription
+            ResponseEntity<String> resp =
+                restClient().deleteForEntity(typeSubscriptionUrl() + "/subscriptionId").block();
+            assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
+            assertThat(this.infoTypeSubscriptions.size()).isZero();
+            resp = restClient().getForEntity(typeSubscriptionUrl()).block();
+            assertThat(resp.getBody()).isEqualTo("[]");
+        }
+    }
+
+    @Test
+    void testTypeSubscriptionErrorCodes() throws Exception {
+
+        testErrorCode(restClient().get(typeSubscriptionUrl() + "/junk"), HttpStatus.NOT_FOUND,
+            "Could not find Information subscription: junk");
+
+        testErrorCode(restClient().delete(typeSubscriptionUrl() + "/junk"), HttpStatus.NOT_FOUND,
+            "Could not find Information subscription: junk");
+    }
+
+    private String typeSubscriptionUrl() {
+        return ConsumerConsts.API_ROOT + "/info-type-subscription";
+    }
+
     private void deleteEiProducer(String infoProducerId) {
         String url = ProducerConsts.API_ROOT + "/info-producers/" + infoProducerId;
         restClient().deleteForEntity(url).block();
@@ -1007,7 +1097,14 @@ class ApplicationTest {
         ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
         this.infoTypes.getType(infoTypeId);
         return resp.getStatusCode();
+    }
+
+    private String deleteInfoTypeUrl(String typeId) {
+        return ProducerConsts.API_ROOT + "/info-types/" + typeId;
+    }
 
+    private void deleteInfoType(String typeId) {
+        restClient().delete(deleteInfoTypeUrl(typeId)).block();
     }
 
     private InfoType putEiProducerWithOneTypeRejecting(String producerId, String infoTypeId)
index a9702b2..27160f1 100644 (file)
@@ -37,6 +37,8 @@ import lombok.Getter;
 import org.oransc.enrichment.controllers.VoidResponse;
 import org.oransc.enrichment.controllers.a1e.A1eConsts;
 import org.oransc.enrichment.controllers.a1e.A1eEiJobStatus;
+import org.oransc.enrichment.controllers.r1consumer.ConsumerConsts;
+import org.oransc.enrichment.controllers.r1consumer.ConsumerTypeRegistrationInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
@@ -48,17 +50,20 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController("ConsumerSimulatorController")
-@Tag(name = A1eConsts.CONSUMER_API_CALLBACKS_NAME)
 public class ConsumerSimulatorController {
 
     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
     public static class TestResults {
 
-        public List<A1eEiJobStatus> status = Collections.synchronizedList(new ArrayList<A1eEiJobStatus>());
+        public List<A1eEiJobStatus> eiJobStatusCallbacks =
+            Collections.synchronizedList(new ArrayList<A1eEiJobStatus>());
+        public List<ConsumerTypeRegistrationInfo> typeRegistrationInfoCallbacks =
+            Collections.synchronizedList(new ArrayList<ConsumerTypeRegistrationInfo>());
 
         public void reset() {
-            status.clear();
+            eiJobStatusCallbacks.clear();
+            typeRegistrationInfoCallbacks.clear();
         }
     }
 
@@ -69,6 +74,7 @@ public class ConsumerSimulatorController {
         return "/example_dataconsumer/info_jobs/" + infoJobId + "/status";
     }
 
+    @Tag(name = A1eConsts.CONSUMER_API_CALLBACKS_NAME)
     @PostMapping(
         path = "/example_dataconsumer/info_jobs/{infoJobId}/status",
         produces = MediaType.APPLICATION_JSON_VALUE)
@@ -86,7 +92,33 @@ public class ConsumerSimulatorController {
         @PathVariable("infoJobId") String infoJobId, //
         @RequestBody A1eEiJobStatus status) {
         logger.info("Job status callback status: {} infoJobId: {}", status.state, infoJobId);
-        this.testResults.status.add(status);
+        this.testResults.eiJobStatusCallbacks.add(status);
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+
+    private static final String TYPE_STATUS_CALLBACK_URL = "/example_dataconsumer/info_type_status";
+
+    public static String getTypeStatusCallbackUrl() {
+        return TYPE_STATUS_CALLBACK_URL;
+    }
+
+    @Tag(name = ConsumerConsts.CONSUMER_API_CALLBACKS_NAME)
+    @PostMapping(path = TYPE_STATUS_CALLBACK_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Callback for changed Information type registration status",
+        description = "The primitive is implemented by the data consumer and is invoked when a Information type status has been changed. <br/>"
+            + "Subscription are managed by primitives in '" + ConsumerConsts.CONSUMER_API_NAME + "'")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public ResponseEntity<Object> typeStatusCallback( //
+        @RequestBody ConsumerTypeRegistrationInfo status) {
+        logger.info("Job type registration status callback status: {}", status);
+        this.testResults.typeRegistrationInfoCallbacks.add(status);
         return new ResponseEntity<>(HttpStatus.OK);
     }
 
index 1c0767b..66af67c 100644 (file)
@@ -56,11 +56,11 @@ public class ProducerSimulatorController {
 
     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-    public static final String JOB_URL = "/producer_simulator/info_job";
-    public static final String JOB_ERROR_URL = "/producer_simulator/info_job_error";
+    public static final String JOB_URL = "/example_dataproducer/info_job";
+    public static final String JOB_ERROR_URL = "/example_dataproducer/info_job_error";
 
-    public static final String SUPERVISION_URL = "/producer_simulator/health_check";
-    public static final String SUPERVISION_ERROR_URL = "/producer_simulator/health_check_error";
+    public static final String SUPERVISION_URL = "/example_dataproducer/health_check";
+    public static final String SUPERVISION_ERROR_URL = "/example_dataproducer/health_check_error";
 
     public static class TestResults {
 
@@ -111,7 +111,7 @@ public class ProducerSimulatorController {
         }
     }
 
-    @DeleteMapping(path = "/producer_simulator/info_job/{infoJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping(path = JOB_URL + "/{infoJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
     @Operation(
         summary = "Callback for Information Job deletion",
         description = "The call is invoked to terminate a data subscription. The endpoint is provided by the Information Producer.")