Merge "Integrated PMS 2.0 to test env and test cases"
authorHenrik Andersson <henrik.b.andersson@est.tech>
Wed, 4 Nov 2020 08:48:58 +0000 (08:48 +0000)
committerGerrit Code Review <gerrit@o-ran-sc.org>
Wed, 4 Nov 2020 08:48:58 +0000 (08:48 +0000)
37 files changed:
.gitignore
docs/.project [deleted file]
docs/api-docs.rst
docs/conf.py
docs/images/swagger.png [new file with mode: 0644]
docs/images/yaml_logo.png [new file with mode: 0644]
docs/requirements-docs.txt
enrichment-coordinator-service/docs/api.json
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerCallbacks.java [new file with mode: 0644]
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerCallbacks.java [moved from enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java with 68% similarity]
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerJobInfo.java [moved from enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerJobInfo.java with 94% similarity]
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java
enrichment-coordinator-service/src/main/java/org/oransc/enrichment/tasks/ProducerSupervision.java
enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java
enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ConsumerSimulatorController.java [new file with mode: 0644]
enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java
pom.xml
r-app-catalogue/.gitignore [new file with mode: 0644]
r-app-catalogue/Dockerfile [new file with mode: 0644]
r-app-catalogue/README.md [new file with mode: 0644]
r-app-catalogue/api/rac-api.json [new file with mode: 0644]
r-app-catalogue/api/rac-api.yaml [new file with mode: 0644]
r-app-catalogue/config/application.yaml [new file with mode: 0644]
r-app-catalogue/pom.xml [new file with mode: 0644]
r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java [new file with mode: 0644]
r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java [new file with mode: 0644]
tox.ini

index e5a2f72..26c6731 100644 (file)
@@ -6,10 +6,12 @@ docs/_build/
 
 # Eclipse
 .checkstyle
+.classpath
+target/
 .sts4-cache
 .project
 .settings
 .pydevproject
 infer-out/
 
-.vscode
\ No newline at end of file
+.vscode
diff --git a/docs/.project b/docs/.project
deleted file mode 100644 (file)
index e248a4f..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
-       <name>docs</name>
-       <comment></comment>
-       <projects>
-       </projects>
-       <buildSpec>
-       </buildSpec>
-       <natures>
-       </natures>
-</projectDescription>
index 12ff516..9b0608a 100644 (file)
@@ -15,9 +15,10 @@ This is the API-docs of Non-RT RIC.
    :depth: 3
    :local:
 
-The Non-RT RIC consists of two parts, described in the sections below:
+The Non-RT RIC consists of three parts, described in the sections below:
  * The Policy Agent
  * The SDNC A1 Controller
+ * The rAPP Catalogue
 
 
 Policy Agent
@@ -44,6 +45,26 @@ See :ref:`sdnc-a1-controller-api` for how to use the API.
 
 See the README.md file in the nonrtric/sdnc-a1-controller repo for info about how to use it.
 
+rAPP Catalogue
+==============
+
+The Non RT-RIC Service Catalogue provides a way for services to register themselves for other services to discover.
+
+See `RAC API <./rac-api.html>`_ for how to use the API.
+
+.. |swagger-icon| image:: ./images/swagger.png
+                  :width: 40px
+
+.. |yaml-icon| image:: ./images/yaml_logo.png
+                  :width: 40px
+
+
+.. csv-table::
+   :header: "API name", "|swagger-icon|", "|yaml-icon|"
+   :widths: 10,5, 5
+
+   "RAC API", ":download:`link <../r-app-catalogue/api/rac-api.json>`", ":download:`link <../r-app-catalogue/api/rac-api.yaml>`"
+
 Complementary tools
 ===================
 
index d620289..a6ae7f9 100644 (file)
@@ -7,9 +7,23 @@ branch = 'latest'
 linkcheck_ignore = [
     'http://localhost.*',
     'http://127.0.0.1.*',
-    'https://gerrit.o-ran-sc.org.*'
+    'https://gerrit.o-ran-sc.org.*',
+    './rac-api.html' #Generated file that doesn't exist at link check.
 ]
 
+extensions = ['sphinxcontrib.redoc', 'sphinx.ext.intersphinx',]
+
+redoc = [
+            {
+                'name': 'RAC API',
+                'page': 'rac-api',
+                'spec': '../r-app-catalogue/api/rac-api.json',
+                'embed': True,
+            }
+        ]
+
+redoc_uri = 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js'
+
 #intershpinx mapping with other projects
 intersphinx_mapping = {}
 
diff --git a/docs/images/swagger.png b/docs/images/swagger.png
new file mode 100644 (file)
index 0000000..f5a9e0c
Binary files /dev/null and b/docs/images/swagger.png differ
diff --git a/docs/images/yaml_logo.png b/docs/images/yaml_logo.png
new file mode 100644 (file)
index 0000000..0492eb4
Binary files /dev/null and b/docs/images/yaml_logo.png differ
index 09a0c1c..78db685 100644 (file)
@@ -1,5 +1,12 @@
-sphinx
-sphinx-rtd-theme
-sphinxcontrib-httpdomain
-recommonmark
-lfdocs-conf
+tox
+Sphinx>=2,<4
+doc8
+docutils
+setuptools
+six
+sphinx_rtd_theme>=0.4.3
+sphinxcontrib-needs>=0.2.3
+sphinxcontrib-swaggerdoc
+sphinx_bootstrap_theme
+sphinxcontrib-redoc
+lfdocs-conf
\ No newline at end of file
index 71f64db..a8df3c7 100644 (file)
@@ -1,21 +1,27 @@
 {
     "basePath": "/",
     "paths": {
-        "/producer_simulator/supervision": {"get": {
-            "summary": "Producer supervision",
+        "/producer_simulator/ei_job": {"post": {
+            "summary": "Callback for EI job creation",
             "deprecated": false,
             "produces": ["application/json"],
-            "operationId": "producerSupervisionUsingGET",
+            "operationId": "jobCreatedCallbackUsingPOST",
             "responses": {
-                "200": {
-                    "schema": {"type": "string"},
-                    "description": "OK"
-                },
+                "200": {"description": "OK"},
+                "201": {"description": "Created"},
                 "401": {"description": "Unauthorized"},
                 "403": {"description": "Forbidden"},
                 "404": {"description": "Not Found"}
             },
-            "tags": ["Producer Callbacks"]
+            "parameters": [{
+                "schema": {"$ref": "#/definitions/producer_ei_job_request"},
+                "in": "body",
+                "name": "request",
+                "description": "request",
+                "required": true
+            }],
+            "tags": ["Producer Callbacks"],
+            "consumes": ["application/json"]
         }},
         "/A1-EI/v1/eitypes/{eiTypeId}": {"get": {
             "summary": "Individual EI type",
             }],
             "tags": ["A1-EI (enrichment information)"]
         }},
+        "/consumer_simulator/eijobs/{eiJobId}/status": {"post": {
+            "summary": "Callback for EI job status",
+            "deprecated": false,
+            "produces": ["application/json"],
+            "operationId": "jobStatusCallbackUsingPOST",
+            "responses": {
+                "200": {"description": "OK"},
+                "201": {"description": "Created"},
+                "401": {"description": "Unauthorized"},
+                "403": {"description": "Forbidden"},
+                "404": {"description": "Not Found"}
+            },
+            "parameters": [
+                {
+                    "in": "path",
+                    "name": "eiJobId",
+                    "description": "eiJobId",
+                    "type": "string",
+                    "required": true
+                },
+                {
+                    "schema": {"$ref": "#/definitions/EiJobStatusObject"},
+                    "in": "body",
+                    "name": "status",
+                    "description": "status",
+                    "required": true
+                }
+            ],
+            "tags": ["Consumer Callbacks"],
+            "consumes": ["application/json"]
+        }},
         "/ei-producer/v1/eitypes": {"get": {
             "summary": "EI type identifiers",
             "deprecated": false,
             },
             "tags": ["A1-EI (enrichment information)"]
         }},
-        "/producer_simulator/job_deleted": {"post": {
-            "summary": "Callback for EI job deletion",
-            "deprecated": false,
-            "produces": ["application/json"],
-            "operationId": "jobDeletedCallbackUsingPOST",
-            "responses": {
-                "200": {"description": "OK"},
-                "201": {"description": "Created"},
-                "401": {"description": "Unauthorized"},
-                "403": {"description": "Forbidden"},
-                "404": {"description": "Not Found"}
-            },
-            "parameters": [{
-                "schema": {"$ref": "#/definitions/producer_ei_job_request"},
-                "in": "body",
-                "name": "request",
-                "description": "request",
-                "required": true
-            }],
-            "tags": ["Producer Callbacks"],
-            "consumes": ["application/json"]
-        }},
         "/ei-producer/v1/eiproducers/{eiProducerId}/status": {"get": {
             "summary": "EI producer status",
             "deprecated": false,
             }],
             "tags": ["Enrichment Data Producer API"]
         }},
+        "/producer_simulator/ei_job/{eiJobId}": {"delete": {
+            "summary": "Callback for EI job deletion",
+            "deprecated": false,
+            "produces": ["application/json"],
+            "operationId": "jobDeletedCallbackUsingDELETE",
+            "responses": {
+                "200": {"description": "OK"},
+                "401": {"description": "Unauthorized"},
+                "204": {"description": "No Content"},
+                "403": {"description": "Forbidden"}
+            },
+            "parameters": [{
+                "in": "path",
+                "name": "eiJobId",
+                "description": "eiJobId",
+                "type": "string",
+                "required": true
+            }],
+            "tags": ["Producer Callbacks"]
+        }},
         "/ei-producer/v1/eiproducers": {"get": {
             "summary": "EI producer identifiers",
             "deprecated": false,
                 "consumes": ["application/json"]
             }
         },
+        "/producer_simulator/health_check": {"get": {
+            "summary": "Producer supervision",
+            "deprecated": false,
+            "produces": ["application/json"],
+            "operationId": "producerSupervisionUsingGET",
+            "responses": {
+                "200": {
+                    "schema": {"type": "string"},
+                    "description": "OK"
+                },
+                "401": {"description": "Unauthorized"},
+                "403": {"description": "Forbidden"},
+                "404": {"description": "Not Found"}
+            },
+            "tags": ["Producer Callbacks"]
+        }},
         "/ei-producer/v1/eiproducers/{eiProducerId}/eijobs": {"get": {
             "summary": "EI job definitions",
             "deprecated": false,
                 "required": true
             }],
             "tags": ["A1-EI (enrichment information)"]
-        }},
-        "/producer_simulator/job_created": {"post": {
-            "summary": "Callback for EI job creation",
-            "deprecated": false,
-            "produces": ["application/json"],
-            "operationId": "jobCreatedCallbackUsingPOST",
-            "responses": {
-                "200": {"description": "OK"},
-                "201": {"description": "Created"},
-                "401": {"description": "Unauthorized"},
-                "403": {"description": "Forbidden"},
-                "404": {"description": "Not Found"}
-            },
-            "parameters": [{
-                "schema": {"$ref": "#/definitions/producer_ei_job_request"},
-                "in": "body",
-                "name": "request",
-                "description": "request",
-                "required": true
-            }],
-            "tags": ["Producer Callbacks"],
-            "consumes": ["application/json"]
         }}
     },
-    "host": "localhost:42127",
+    "host": "localhost:41549",
     "definitions": {
         "producer_ei_job_request": {
             "description": "The body of the EI producer callbacks for EI job creation and deletion",
             "type": "object",
             "title": "producer_registration_info",
             "required": [
-                "ei_job_creation_callback_url",
-                "ei_job_deletion_callback_url",
+                "ei_job_callback_url",
                 "ei_producer_supervision_callback_url",
                 "supported_ei_types"
             ],
                     "type": "array",
                     "items": {"$ref": "#/definitions/producer_ei_type_registration_info"}
                 },
-                "ei_job_creation_callback_url": {
-                    "description": "callback for job creation",
-                    "type": "string"
-                },
-                "ei_job_deletion_callback_url": {
-                    "description": "callback for job deletion",
-                    "type": "string"
-                },
                 "ei_producer_supervision_callback_url": {
                     "description": "callback for producer supervision",
                     "type": "string"
+                },
+                "ei_job_callback_url": {
+                    "description": "callback for EI job",
+                    "type": "string"
                 }
             }
         },
             "name": "A1-EI (enrichment information)",
             "description": "Consumer Controller"
         },
+        {
+            "name": "Consumer Callbacks",
+            "description": "Consumer Simulator Controller"
+        },
         {
             "name": "Enrichment Data Producer API",
             "description": "Producer Controller"
index ce41956..f4cf9dc 100644 (file)
@@ -23,7 +23,6 @@ package org.oransc.enrichment;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import org.apache.catalina.connector.Connector;
-import org.oransc.enrichment.clients.ProducerCallbacks;
 import org.oransc.enrichment.configuration.ApplicationConfig;
 import org.oransc.enrichment.repository.EiJobs;
 import org.oransc.enrichment.repository.EiProducers;
@@ -76,11 +75,6 @@ class BeanFactory {
         return this.applicationConfig;
     }
 
-    @Bean
-    public ProducerCallbacks getProducerCallbacks() {
-        return new ProducerCallbacks(this.applicationConfig);
-    }
-
     private static Connector getHttpConnector(int httpPort) {
         Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
         connector.setScheme("http");
diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerCallbacks.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerCallbacks.java
new file mode 100644 (file)
index 0000000..cded953
--- /dev/null
@@ -0,0 +1,89 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 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.consumer;
+
+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.EiJob;
+import org.oransc.enrichment.repository.EiJobs;
+import org.oransc.enrichment.repository.EiProducer;
+import org.oransc.enrichment.repository.EiType;
+import org.oransc.enrichment.repository.EiTypes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * Callbacks to the EiProducer
+ */
+@Component
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
+public class ConsumerCallbacks {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private static Gson gson = new GsonBuilder().create();
+
+    private final AsyncRestClient restClient;
+    private final EiTypes eiTypes;
+    private final EiJobs eiJobs;
+
+    @Autowired
+    public ConsumerCallbacks(ApplicationConfig config, EiTypes eiTypes, EiJobs eiJobs) {
+        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config.getWebClientConfig());
+        this.restClient = restClientFactory.createRestClient("");
+        this.eiTypes = eiTypes;
+        this.eiJobs = eiJobs;
+    }
+
+    public void notifyConsumersProducerDeleted(EiProducer eiProducer) {
+        for (EiType type : eiProducer.getEiTypes()) {
+            if (this.eiTypes.get(type.getId()) == null) {
+                for (EiJob job : this.eiJobs.getJobsForType(type)) {
+                    noifyJobOwner(job, new ConsumerEiJobStatus(ConsumerEiJobStatus.EiJobStatusValues.DISABLED));
+                }
+            }
+        }
+    }
+
+    public void notifyConsumersTypeAdded(EiType eiType) {
+        for (EiJob job : this.eiJobs.getJobsForType(eiType)) {
+            noifyJobOwner(job, new ConsumerEiJobStatus(ConsumerEiJobStatus.EiJobStatusValues.ENABLED));
+        }
+    }
+
+    private void noifyJobOwner(EiJob job, ConsumerEiJobStatus status) {
+        if (!job.jobStatusUrl().isEmpty()) {
+            String body = gson.toJson(status);
+            this.restClient.post(job.jobStatusUrl(), body) //
+                .subscribe(notUsed -> logger.debug("Consumer notified OK {}", job.id()), //
+                    throwable -> logger.warn("Consumer notify failed {} {}", job.jobStatusUrl(), throwable.toString()), //
+                    null);
+        }
+    }
+
+}
index bead826..b194dc1 100644 (file)
@@ -31,18 +31,21 @@ import io.swagger.annotations.ApiResponse;
 import io.swagger.annotations.ApiResponses;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
+import java.util.Vector;
 
 import org.everit.json.schema.Schema;
 import org.everit.json.schema.loader.SchemaLoader;
 import org.json.JSONObject;
-import org.oransc.enrichment.clients.ProducerCallbacks;
 import org.oransc.enrichment.configuration.ApplicationConfig;
 import org.oransc.enrichment.controllers.ErrorResponse;
 import org.oransc.enrichment.controllers.VoidResponse;
+import org.oransc.enrichment.controllers.producer.ProducerCallbacks;
 import org.oransc.enrichment.exceptions.ServiceException;
 import org.oransc.enrichment.repository.EiJob;
 import org.oransc.enrichment.repository.EiJobs;
+import org.oransc.enrichment.repository.EiProducer;
 import org.oransc.enrichment.repository.EiType;
 import org.oransc.enrichment.repository.EiTypes;
 import org.oransc.enrichment.repository.ImmutableEiJob;
@@ -78,8 +81,7 @@ public class ConsumerController {
     @Autowired
     ProducerCallbacks producerCallbacks;
 
-    private static Gson gson = new GsonBuilder() //
-        .create(); //
+    private static Gson gson = new GsonBuilder().create();
 
     @GetMapping(path = "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "EI type identifiers", notes = "")
@@ -149,7 +151,7 @@ public class ConsumerController {
             List<String> result = new ArrayList<>();
             if (owner != null) {
                 for (EiJob job : this.eiJobs.getJobsForOwner(owner)) {
-                    if (eiTypeId == null || job.type().getId().equals(eiTypeId)) {
+                    if (eiTypeId == null || job.typeId().equals(eiTypeId)) {
                         result.add(job.id());
                     }
                 }
@@ -204,9 +206,21 @@ public class ConsumerController {
         }
     }
 
+    private Collection<EiProducer> getProducers(EiJob eiJob) {
+        try {
+            return this.eiTypes.getType(eiJob.typeId()).getProducers();
+        } catch (Exception e) {
+            return new Vector<>();
+        }
+    }
+
     private ConsumerEiJobStatus toEiJobStatus(EiJob job) {
-        // TODO
-        return new ConsumerEiJobStatus(ConsumerEiJobStatus.EiJobStatusValues.ENABLED);
+        for (EiProducer producer : getProducers(job)) {
+            if (producer.isAvailable()) {
+                return new ConsumerEiJobStatus(ConsumerEiJobStatus.EiJobStatusValues.ENABLED);
+            }
+        }
+        return new ConsumerEiJobStatus(ConsumerEiJobStatus.EiJobStatusValues.DISABLED);
     }
 
     @DeleteMapping(path = "/eijobs/{eiJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
@@ -274,7 +288,7 @@ public class ConsumerController {
             validateJsonObjectAgainstSchema(eiType.getJobDataSchema(), eiJobInfo.jobData);
             EiJob existingEiJob = this.eiJobs.get(eiJobId);
 
-            if (existingEiJob != null && !existingEiJob.type().getId().equals(eiJobInfo.eiTypeId)) {
+            if (existingEiJob != null && !existingEiJob.typeId().equals(eiJobInfo.eiTypeId)) {
                 throw new ServiceException("Not allowed to change type for existing EI job", HttpStatus.CONFLICT);
             }
             return Mono.just(toEiJob(eiJobInfo, eiJobId, eiType));
@@ -301,15 +315,14 @@ public class ConsumerController {
         }
     }
 
-    // Status TBD
-
     private EiJob toEiJob(ConsumerEiJobInfo info, String id, EiType type) {
         return ImmutableEiJob.builder() //
             .id(id) //
-            .type(type) //
+            .typeId(type.getId()) //
             .owner(info.owner) //
             .jobData(info.jobData) //
-            .targetUri(info.targetUri) //
+            .targetUrl(info.targetUri) //
+            .jobStatusUrl(info.statusNotificationUri == null ? "" : info.statusNotificationUri) //
             .build();
     }
 
@@ -318,6 +331,6 @@ public class ConsumerController {
     }
 
     private ConsumerEiJobInfo toEiJobInfo(EiJob s) {
-        return new ConsumerEiJobInfo(s.type().getId(), s.jobData(), s.owner(), s.targetUri());
+        return new ConsumerEiJobInfo(s.typeId(), s.jobData(), s.owner(), s.targetUrl(), s.jobStatusUrl());
     }
 }
index d6996ce..d88091f 100644 (file)
@@ -60,10 +60,12 @@ public class ConsumerEiJobInfo {
     public ConsumerEiJobInfo() {
     }
 
-    public ConsumerEiJobInfo(String eiTypeId, Object jobData, String owner, String targetUri) {
+    public ConsumerEiJobInfo(String eiTypeId, Object jobData, String owner, String targetUri,
+        String statusNotificationUri) {
         this.eiTypeId = eiTypeId;
         this.jobData = jobData;
         this.owner = owner;
         this.targetUri = targetUri;
+        this.statusNotificationUri = statusNotificationUri;
     }
 }
index b2f657f..60752ec 100644 (file)
@@ -45,7 +45,10 @@ public class ConsumerEiJobStatus {
     @ApiModelProperty(value = OPERATIONAL_STATE_DESCRIPTION, name = "eiJobStatus", required = true)
     @SerializedName("eiJobStatus")
     @JsonProperty(value = "eiJobStatus", required = true)
-    public final EiJobStatusValues state;
+    public EiJobStatusValues state;
+
+    public ConsumerEiJobStatus() {
+    }
 
     public ConsumerEiJobStatus(EiJobStatusValues state) {
         this.state = state;
  * ========================LICENSE_END===================================
  */
 
-package org.oransc.enrichment.clients;
+package org.oransc.enrichment.controllers.producer;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 
 import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.Vector;
 
+import org.oransc.enrichment.clients.AsyncRestClient;
+import org.oransc.enrichment.clients.AsyncRestClientFactory;
 import org.oransc.enrichment.configuration.ApplicationConfig;
 import org.oransc.enrichment.repository.EiJob;
 import org.oransc.enrichment.repository.EiProducer;
+import org.oransc.enrichment.repository.EiTypes;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
 
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
@@ -37,6 +44,7 @@ import reactor.core.publisher.Mono;
 /**
  * Callbacks to the EiProducer
  */
+@Component
 @SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
 public class ProducerCallbacks {
 
@@ -44,19 +52,23 @@ public class ProducerCallbacks {
     private static Gson gson = new GsonBuilder().create();
 
     private final AsyncRestClient restClient;
+    private final EiTypes eiTypes;
 
-    public ProducerCallbacks(ApplicationConfig config) {
+    @Autowired
+    public ProducerCallbacks(ApplicationConfig config, EiTypes eiTypes) {
         AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config.getWebClientConfig());
         this.restClient = restClientFactory.createRestClient("");
+        this.eiTypes = eiTypes;
     }
 
     public void notifyProducersJobDeleted(EiJob eiJob) {
-        ProducerJobInfo request = new ProducerJobInfo(eiJob);
-        String body = gson.toJson(request);
-        for (EiProducer producer : eiJob.type().getProducers()) {
-            restClient.post(producer.getJobDeletionCallbackUrl(), body) //
-                .subscribe(notUsed -> logger.debug("Job deleted OK {}", producer.getId()), //
-                    throwable -> logger.warn("Job delete failed {}", producer.getId(), throwable.toString()), null);
+        for (EiProducer producer : getProducers(eiJob)) {
+            String url = producer.getJobCallbackUrl() + "/" + eiJob.id();
+            restClient.delete(url) //
+                .subscribe(notUsed -> logger.debug("Producer job deleted OK {}", producer.getId()), //
+                    throwable -> logger.warn("Producer job delete failed {} {}", producer.getId(),
+                        throwable.getMessage()),
+                    null);
         }
     }
 
@@ -67,7 +79,7 @@ public class ProducerCallbacks {
      * @return the number of producers that returned OK
      */
     public Mono<Integer> notifyProducersJobStarted(EiJob eiJob) {
-        return Flux.fromIterable(eiJob.type().getProducers()) //
+        return Flux.fromIterable(getProducers(eiJob)) //
             .flatMap(eiProducer -> notifyProducerJobStarted(eiProducer, eiJob)) //
             .collectList() //
             .flatMap(okResponses -> Mono.just(Integer.valueOf(okResponses.size()))); //
@@ -84,7 +96,7 @@ public class ProducerCallbacks {
         ProducerJobInfo request = new ProducerJobInfo(eiJob);
         String body = gson.toJson(request);
 
-        return restClient.post(producer.getJobCreationCallbackUrl(), body)
+        return restClient.post(producer.getJobCallbackUrl(), body)
             .doOnNext(resp -> logger.debug("Job subscription started OK {}", producer.getId()))
             .onErrorResume(throwable -> {
                 logger.warn("Job subscription failed {}", producer.getId(), throwable.toString());
@@ -92,4 +104,12 @@ public class ProducerCallbacks {
             });
     }
 
+    private Collection<EiProducer> getProducers(EiJob eiJob) {
+        try {
+            return this.eiTypes.getType(eiJob.typeId()).getProducers();
+        } catch (Exception e) {
+            return new Vector<>();
+        }
+    }
+
 }
index 306e3a9..c670ea4 100644 (file)
@@ -33,10 +33,9 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 
-import org.oransc.enrichment.clients.ProducerCallbacks;
-import org.oransc.enrichment.clients.ProducerJobInfo;
 import org.oransc.enrichment.controllers.ErrorResponse;
 import org.oransc.enrichment.controllers.VoidResponse;
+import org.oransc.enrichment.controllers.consumer.ConsumerCallbacks;
 import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo.ProducerEiTypeRegistrationInfo;
 import org.oransc.enrichment.repository.EiJob;
 import org.oransc.enrichment.repository.EiJobs;
@@ -78,6 +77,9 @@ public class ProducerController {
     @Autowired
     ProducerCallbacks producerCallbacks;
 
+    @Autowired
+    ConsumerCallbacks consumerCallbacks;
+
     @GetMapping(path = ProducerConsts.API_ROOT + "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "EI type identifiers", notes = "")
     @ApiResponses(
@@ -214,6 +216,7 @@ public class ProducerController {
         ProducerStatusInfo.OperationalState opState =
             producer.isAvailable() ? ProducerStatusInfo.OperationalState.ENABLED
                 : ProducerStatusInfo.OperationalState.DISABLED;
+        this.logger.debug("opState {}", opState);
         return new ProducerStatusInfo(opState);
     }
 
@@ -251,7 +254,7 @@ public class ProducerController {
     private void purgeTypes(Collection<EiType> types) {
         for (EiType type : types) {
             if (type.getProducerIds().isEmpty()) {
-                this.eiTypes.deregisterType(type, this.eiJobs);
+                this.eiTypes.remove(type);
             }
         }
     }
@@ -269,6 +272,7 @@ public class ProducerController {
         try {
             final EiProducer producer = this.eiProducers.getProducer(eiProducerId);
             this.eiProducers.deregisterProducer(producer, this.eiTypes, this.eiJobs);
+            this.consumerCallbacks.notifyConsumersProducerDeleted(producer);
             return new ResponseEntity<>(HttpStatus.NO_CONTENT);
         } catch (Exception e) {
             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
@@ -280,13 +284,14 @@ public class ProducerController {
         if (type == null) {
             type = new EiType(typeInfo.eiTypeId, typeInfo.jobDataSchema);
             this.eiTypes.put(type);
+            this.consumerCallbacks.notifyConsumersTypeAdded(type);
         }
         return type;
     }
 
     EiProducer createProducer(Collection<EiType> types, String producerId, ProducerRegistrationInfo registrationInfo) {
-        return new EiProducer(producerId, types, registrationInfo.jobCreationCallbackUrl,
-            registrationInfo.jobDeletionCallbackUrl, registrationInfo.producerSupervisionCallbackUrl);
+        return new EiProducer(producerId, types, registrationInfo.jobCallbackUrl,
+            registrationInfo.producerSupervisionCallbackUrl);
     }
 
     private EiProducer registerProducer(String producerId, ProducerRegistrationInfo registrationInfo) {
@@ -312,8 +317,7 @@ public class ProducerController {
         for (EiType type : p.getEiTypes()) {
             types.add(toEiTypeRegistrationInfo(type));
         }
-        return new ProducerRegistrationInfo(types, p.getJobCreationCallbackUrl(), p.getJobDeletionCallbackUrl(),
-            p.getProducerSupervisionCallbackUrl());
+        return new ProducerRegistrationInfo(types, p.getJobCallbackUrl(), p.getProducerSupervisionCallbackUrl());
     }
 
     private ProducerEiTypeRegistrationInfo toEiTypeRegistrationInfo(EiType type) {
@@ -18,7 +18,7 @@
  * ========================LICENSE_END===================================
  */
 
-package org.oransc.enrichment.clients;
+package org.oransc.enrichment.controllers.producer;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.gson.annotations.SerializedName;
@@ -63,7 +63,7 @@ public class ProducerJobInfo {
     }
 
     public ProducerJobInfo(EiJob job) {
-        this(job.jobData(), job.id(), job.type().getId(), job.targetUri());
+        this(job.jobData(), job.id(), job.typeId(), job.targetUrl());
     }
 
     public ProducerJobInfo() {
index 859443d..3f78580 100644 (file)
@@ -62,26 +62,20 @@ public class ProducerRegistrationInfo {
     @JsonProperty(value = "supported_ei_types", required = true)
     public Collection<ProducerEiTypeRegistrationInfo> types;
 
-    @ApiModelProperty(value = "callback for job creation", required = true)
-    @SerializedName("ei_job_creation_callback_url")
-    @JsonProperty(value = "ei_job_creation_callback_url", required = true)
-    public String jobCreationCallbackUrl;
-
-    @ApiModelProperty(value = "callback for job deletion", required = true)
-    @SerializedName("ei_job_deletion_callback_url")
-    @JsonProperty(value = "ei_job_deletion_callback_url", required = true)
-    public String jobDeletionCallbackUrl;
+    @ApiModelProperty(value = "callback for EI job", required = true)
+    @SerializedName("ei_job_callback_url")
+    @JsonProperty(value = "ei_job_callback_url", required = true)
+    public String jobCallbackUrl;
 
     @ApiModelProperty(value = "callback for producer supervision", required = true)
     @SerializedName("ei_producer_supervision_callback_url")
     @JsonProperty(value = "ei_producer_supervision_callback_url", required = true)
     public String producerSupervisionCallbackUrl;
 
-    public ProducerRegistrationInfo(Collection<ProducerEiTypeRegistrationInfo> types, String jobCreationCallbackUrl,
-        String jobDeletionCallbackUrl, String producerSupervisionCallbackUrl) {
+    public ProducerRegistrationInfo(Collection<ProducerEiTypeRegistrationInfo> types, String jobCallbackUrl,
+        String producerSupervisionCallbackUrl) {
         this.types = types;
-        this.jobCreationCallbackUrl = jobCreationCallbackUrl;
-        this.jobDeletionCallbackUrl = jobDeletionCallbackUrl;
+        this.jobCallbackUrl = jobCallbackUrl;
         this.producerSupervisionCallbackUrl = producerSupervisionCallbackUrl;
     }
 
index 8c1206f..95bbc03 100644 (file)
@@ -32,11 +32,13 @@ public interface EiJob {
 
     String id();
 
-    EiType type();
+    String typeId();
 
     String owner();
 
     Object jobData();
 
-    String targetUri();
+    String targetUrl();
+
+    String jobStatusUrl();
 }
index f0e4051..706c8dd 100644 (file)
@@ -38,7 +38,7 @@ public class EiJobs {
 
     public synchronized void put(EiJob job) {
         allEiJobs.put(job.id(), job);
-        jobsByType.put(job.type().getId(), job.id(), job);
+        jobsByType.put(job.typeId(), job.id(), job);
         jobsByOwner.put(job.owner(), job.id(), job);
     }
 
@@ -80,7 +80,7 @@ public class EiJobs {
 
     public synchronized void remove(EiJob job) {
         this.allEiJobs.remove(job.id());
-        jobsByType.remove(job.type().getId(), job.id());
+        jobsByType.remove(job.typeId(), job.id());
         jobsByOwner.remove(job.owner(), job.id());
     }
 
index 99932a7..63e5d4c 100644 (file)
@@ -32,22 +32,18 @@ public class EiProducer {
     private final Collection<EiType> eiTypes;
 
     @Getter
-    private final String jobCreationCallbackUrl;
-
-    @Getter
-    private final String jobDeletionCallbackUrl;
+    private final String jobCallbackUrl;
 
     @Getter
     private final String producerSupervisionCallbackUrl;
 
     private int unresponsiveCounter = 0;
 
-    public EiProducer(String id, Collection<EiType> eiTypes, String jobCreationCallbackUrl,
-        String jobDeletionCallbackUrl, String producerSupervisionCallbackUrl) {
+    public EiProducer(String id, Collection<EiType> eiTypes, String jobCallbackUrl,
+        String producerSupervisionCallbackUrl) {
         this.id = id;
         this.eiTypes = eiTypes;
-        this.jobCreationCallbackUrl = jobCreationCallbackUrl;
-        this.jobDeletionCallbackUrl = jobDeletionCallbackUrl;
+        this.jobCallbackUrl = jobCallbackUrl;
         this.producerSupervisionCallbackUrl = producerSupervisionCallbackUrl;
     }
 
index b3cd895..801e7fc 100644 (file)
@@ -36,11 +36,10 @@ import org.slf4j.LoggerFactory;
 @SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
 public class EiProducers {
     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-    private Map<String, EiProducer> allEiProducers = new HashMap<>();
+    private final Map<String, EiProducer> allEiProducers = new HashMap<>();
 
     public synchronized void put(EiProducer producer) {
         allEiProducers.put(producer.getId(), producer);
-
     }
 
     public synchronized Collection<EiProducer> getAllProducers() {
@@ -79,7 +78,7 @@ public class EiProducers {
                 this.logger.error("Bug, no producer found");
             }
             if (type.getProducerIds().isEmpty()) {
-                eiTypes.deregisterType(type, eiJobs);
+                eiTypes.remove(type);
             }
         }
     }
index 6397c2f..d0bf53a 100644 (file)
@@ -73,13 +73,4 @@ public class EiTypes {
     public synchronized void clear() {
         this.allEiTypes.clear();
     }
-
-    public void deregisterType(EiType type, EiJobs eiJobs) {
-        this.remove(type);
-        for (EiJob job : eiJobs.getJobsForType(type.getId())) {
-            eiJobs.remove(job);
-            this.logger.warn("Deleted job {} because no producers left", job.id());
-        }
-    }
-
 }
index f239a48..e242166 100644 (file)
@@ -23,6 +23,7 @@ package org.oransc.enrichment.tasks;
 import org.oransc.enrichment.clients.AsyncRestClient;
 import org.oransc.enrichment.clients.AsyncRestClientFactory;
 import org.oransc.enrichment.configuration.ApplicationConfig;
+import org.oransc.enrichment.controllers.consumer.ConsumerCallbacks;
 import org.oransc.enrichment.repository.EiJobs;
 import org.oransc.enrichment.repository.EiProducer;
 import org.oransc.enrichment.repository.EiProducers;
@@ -50,15 +51,17 @@ public class ProducerSupervision {
     private final EiJobs eiJobs;
     private final EiTypes eiTypes;
     private final AsyncRestClient restClient;
+    private final ConsumerCallbacks consumerCallbacks;
 
     @Autowired
     public ProducerSupervision(ApplicationConfig applicationConfig, EiProducers eiProducers, EiJobs eiJobs,
-        EiTypes eiTypes) {
+        EiTypes eiTypes, ConsumerCallbacks consumerCallbacks) {
         AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(applicationConfig.getWebClientConfig());
         this.restClient = restClientFactory.createRestClient("");
         this.eiJobs = eiJobs;
         this.eiProducers = eiProducers;
         this.eiTypes = eiTypes;
+        this.consumerCallbacks = consumerCallbacks;
     }
 
     @Scheduled(fixedRate = 1000 * 60 * 5)
@@ -87,6 +90,7 @@ public class ProducerSupervision {
         producer.setAliveStatus(false);
         if (producer.isDead()) {
             this.eiProducers.deregisterProducer(producer, this.eiTypes, this.eiJobs);
+            this.consumerCallbacks.notifyConsumersProducerDeleted(producer);
         }
     }
 
index e272faa..3dab460 100644 (file)
@@ -43,15 +43,17 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.oransc.enrichment.clients.AsyncRestClient;
 import org.oransc.enrichment.clients.AsyncRestClientFactory;
-import org.oransc.enrichment.clients.ProducerJobInfo;
 import org.oransc.enrichment.configuration.ApplicationConfig;
 import org.oransc.enrichment.configuration.ImmutableWebClientConfig;
 import org.oransc.enrichment.configuration.WebClientConfig;
+import org.oransc.enrichment.controller.ConsumerSimulatorController;
 import org.oransc.enrichment.controller.ProducerSimulatorController;
 import org.oransc.enrichment.controllers.consumer.ConsumerConsts;
 import org.oransc.enrichment.controllers.consumer.ConsumerEiJobInfo;
+import org.oransc.enrichment.controllers.consumer.ConsumerEiJobStatus;
 import org.oransc.enrichment.controllers.consumer.ConsumerEiTypeInfo;
 import org.oransc.enrichment.controllers.producer.ProducerConsts;
+import org.oransc.enrichment.controllers.producer.ProducerJobInfo;
 import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo;
 import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo.ProducerEiTypeRegistrationInfo;
 import org.oransc.enrichment.controllers.producer.ProducerStatusInfo;
@@ -91,6 +93,7 @@ class ApplicationTest {
     private final String EI_TYPE_ID = "typeId";
     private final String EI_PRODUCER_ID = "producerId";
     private final String EI_JOB_PROPERTY = "\"property1\"";
+    private final String EI_JOB_ID = "jobId";
 
     @Autowired
     ApplicationContext context;
@@ -110,6 +113,9 @@ class ApplicationTest {
     @Autowired
     ProducerSimulatorController producerSimulator;
 
+    @Autowired
+    ConsumerSimulatorController consumerSimulator;
+
     @Autowired
     ProducerSupervision producerSupervision;
 
@@ -135,6 +141,7 @@ class ApplicationTest {
         this.eiTypes.clear();
         this.eiProducers.clear();
         this.producerSimulator.getTestResults().reset();
+        this.consumerSimulator.getTestResults().reset();
     }
 
     @AfterEach
@@ -236,12 +243,9 @@ class ApplicationTest {
     void testGetEiJobStatus() throws Exception {
         putEiProducerWithOneType(EI_PRODUCER_ID, EI_TYPE_ID);
         putEiJob(EI_TYPE_ID, "jobId");
-        String url = ConsumerConsts.API_ROOT + "/eijobs/jobId/status";
-        String rsp = restClient().get(url).block();
-        assertThat(rsp).contains("ENABLED");
-    }
 
-    // Status TBD
+        verifyJobStatus("jobId", "ENABLED");
+    }
 
     @Test
     void testDeleteEiJob() throws Exception {
@@ -254,7 +258,7 @@ class ApplicationTest {
 
         ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
         await().untilAsserted(() -> assertThat(simulatorResults.jobsStopped.size()).isEqualTo(1));
-        assertThat(simulatorResults.jobsStopped.get(0).id).isEqualTo("jobId");
+        assertThat(simulatorResults.jobsStopped.get(0)).isEqualTo("jobId");
     }
 
     @Test
@@ -306,8 +310,8 @@ class ApplicationTest {
 
         String url = ConsumerConsts.API_ROOT + "/eijobs/jobId";
         // The element with name "property1" is mandatory in the schema
-        ConsumerEiJobInfo jobInfo =
-            new ConsumerEiJobInfo("typeId", jsonObject("{ \"XXstring\" : \"value\" }"), "owner", "targetUri");
+        ConsumerEiJobInfo jobInfo = new ConsumerEiJobInfo("typeId", jsonObject("{ \"XXstring\" : \"value\" }"), "owner",
+            "targetUri", "jobStatusUrl");
         String body = gson.toJson(jobInfo);
 
         testErrorCode(restClient().put(url, body), HttpStatus.CONFLICT, "Json validation failure");
@@ -349,7 +353,7 @@ class ApplicationTest {
         putEiJob("typeId1", "jobId");
 
         String url = ConsumerConsts.API_ROOT + "/eijobs/jobId";
-        String body = gson.toJson(eiJobInfo("typeId2"));
+        String body = gson.toJson(eiJobInfo("typeId2", "jobId"));
         testErrorCode(restClient().put(url, body), HttpStatus.CONFLICT,
             "Not allowed to change type for existing EI job");
     }
@@ -440,17 +444,32 @@ class ApplicationTest {
         putEiJob(EI_TYPE_ID, "jobId");
         assertThat(this.eiJobs.size()).isEqualTo(1);
 
-        String url = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId";
-        restClient().deleteForEntity(url).block();
+        deleteEiProducer("eiProducerId");
         assertThat(this.eiProducers.size()).isEqualTo(1);
         assertThat(this.eiTypes.getType(EI_TYPE_ID).getProducerIds()).doesNotContain("eiProducerId");
-        assertThat(this.eiJobs.size()).isEqualTo(1);
+        verifyJobStatus("jobId", "ENABLED");
 
-        String url2 = ProducerConsts.API_ROOT + "/eiproducers/eiProducerId2";
-        restClient().deleteForEntity(url2).block();
+        deleteEiProducer("eiProducerId2");
         assertThat(this.eiProducers.size()).isZero();
         assertThat(this.eiTypes.size()).isZero();
-        assertThat(this.eiJobs.size()).isZero();
+        verifyJobStatus("jobId", "DISABLED");
+    }
+
+    @Test
+    void testJobStatusNotifications() throws JsonMappingException, JsonProcessingException, ServiceException {
+        putEiProducerWithOneType("eiProducerId", EI_TYPE_ID);
+        putEiJob(EI_TYPE_ID, "jobId");
+
+        deleteEiProducer("eiProducerId");
+        assertThat(this.eiTypes.size()).isZero(); // The type is gone
+        assertThat(this.eiJobs.size()).isEqualTo(1); // The job remains
+        ConsumerSimulatorController.TestResults consumerResults = this.consumerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(consumerResults.status.size()).isEqualTo(1));
+        assertThat(consumerResults.status.get(0).state).isEqualTo(ConsumerEiJobStatus.EiJobStatusValues.DISABLED);
+
+        putEiProducerWithOneType("eiProducerId", EI_TYPE_ID);
+        await().untilAsserted(() -> assertThat(consumerResults.status.size()).isEqualTo(2));
+        assertThat(consumerResults.status.get(1).state).isEqualTo(ConsumerEiJobStatus.EiJobStatusValues.ENABLED);
     }
 
     @Test
@@ -469,18 +488,17 @@ class ApplicationTest {
         assertThat(resp.getBody()).contains(EI_PRODUCER_ID);
     }
 
-    private void assertProducerOpState(String producerId,
-        ProducerStatusInfo.OperationalState expectedOperationalState) {
-        String statusUrl = ProducerConsts.API_ROOT + "/eiproducers/" + producerId + "/status";
-        ResponseEntity<String> resp = restClient().getForEntity(statusUrl).block();
-        ProducerStatusInfo statusInfo = gson.fromJson(resp.getBody(), ProducerStatusInfo.class);
-        assertThat(statusInfo.opState).isEqualTo(expectedOperationalState);
-    }
-
     @Test
     void testProducerSupervision() throws JsonMappingException, JsonProcessingException, ServiceException {
         putEiProducerWithOneTypeRejecting("simulateProducerError", EI_TYPE_ID);
 
+        {
+            // Create a job
+            putEiProducerWithOneType(EI_PRODUCER_ID, EI_TYPE_ID);
+            putEiJob(EI_TYPE_ID, "jobId");
+            deleteEiProducer(EI_PRODUCER_ID);
+        }
+
         assertThat(this.eiProducers.size()).isEqualTo(1);
         assertThat(this.eiTypes.size()).isEqualTo(1);
         assertProducerOpState("simulateProducerError", ProducerStatusInfo.OperationalState.ENABLED);
@@ -490,10 +508,15 @@ class ApplicationTest {
         assertThat(this.eiProducers.size()).isEqualTo(1);
         assertProducerOpState("simulateProducerError", ProducerStatusInfo.OperationalState.DISABLED);
 
-        // After 3 failed checks, the producer shall be deregisterred
+        // After 3 failed checks, the producer and the type shall be deregisterred
         this.producerSupervision.createTask().blockLast();
         assertThat(this.eiProducers.size()).isEqualTo(0);
         assertThat(this.eiTypes.size()).isEqualTo(0);
+
+        // Job disabled status notification shall be received
+        ConsumerSimulatorController.TestResults consumerResults = this.consumerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(consumerResults.status.size()).isEqualTo(1));
+        assertThat(consumerResults.status.get(0).state).isEqualTo(ConsumerEiJobStatus.EiJobStatusValues.DISABLED);
     }
 
     @Test
@@ -506,6 +529,25 @@ class ApplicationTest {
         assertThat(resp.getBody()).contains("hunky dory");
     }
 
+    private void deleteEiProducer(String eiProducerId) {
+        String url = ProducerConsts.API_ROOT + "/eiproducers/" + eiProducerId;
+        restClient().deleteForEntity(url).block();
+    }
+
+    private void verifyJobStatus(String jobId, String expStatus) {
+        String url = ConsumerConsts.API_ROOT + "/eijobs/" + jobId + "/status";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp).contains(expStatus);
+    }
+
+    private void assertProducerOpState(String producerId,
+        ProducerStatusInfo.OperationalState expectedOperationalState) {
+        String statusUrl = ProducerConsts.API_ROOT + "/eiproducers/" + producerId + "/status";
+        ResponseEntity<String> resp = restClient().getForEntity(statusUrl).block();
+        ProducerStatusInfo statusInfo = gson.fromJson(resp.getBody(), ProducerStatusInfo.class);
+        assertThat(statusInfo.opState).isEqualTo(expectedOperationalState);
+    }
+
     ProducerEiTypeRegistrationInfo producerEiTypeRegistrationInfo(String typeId)
         throws JsonMappingException, JsonProcessingException {
         return new ProducerEiTypeRegistrationInfo(jsonSchemaObject(), typeId);
@@ -516,8 +558,7 @@ class ApplicationTest {
         Collection<ProducerEiTypeRegistrationInfo> types = new ArrayList<>();
         types.add(producerEiTypeRegistrationInfo(typeId));
         return new ProducerRegistrationInfo(types, //
-            baseUrl() + ProducerSimulatorController.JOB_CREATED_ERROR_URL,
-            baseUrl() + ProducerSimulatorController.JOB_DELETED_ERROR_URL,
+            baseUrl() + ProducerSimulatorController.JOB_ERROR_URL,
             baseUrl() + ProducerSimulatorController.SUPERVISION_ERROR_URL);
     }
 
@@ -526,20 +567,19 @@ class ApplicationTest {
         Collection<ProducerEiTypeRegistrationInfo> types = new ArrayList<>();
         types.add(producerEiTypeRegistrationInfo(typeId));
         return new ProducerRegistrationInfo(types, //
-            baseUrl() + ProducerSimulatorController.JOB_CREATED_URL,
-            baseUrl() + ProducerSimulatorController.JOB_DELETED_URL,
-            baseUrl() + ProducerSimulatorController.SUPERVISION_URL);
+            baseUrl() + ProducerSimulatorController.JOB_URL, baseUrl() + ProducerSimulatorController.SUPERVISION_URL);
     }
 
-    ConsumerEiJobInfo eiJobInfo() throws JsonMappingException, JsonProcessingException {
-        return eiJobInfo(EI_TYPE_ID);
+    private ConsumerEiJobInfo eiJobInfo() throws JsonMappingException, JsonProcessingException {
+        return eiJobInfo(EI_TYPE_ID, EI_JOB_ID);
     }
 
-    ConsumerEiJobInfo eiJobInfo(String typeId) throws JsonMappingException, JsonProcessingException {
-        return new ConsumerEiJobInfo(typeId, jsonObject(), "owner", "targetUri");
+    ConsumerEiJobInfo eiJobInfo(String typeId, String eiJobId) throws JsonMappingException, JsonProcessingException {
+        return new ConsumerEiJobInfo(typeId, jsonObject(), "owner", "targetUri",
+            baseUrl() + ConsumerSimulatorController.getJobStatusUrl(eiJobId));
     }
 
-    Object jsonObject(String json) {
+    private Object jsonObject(String json) {
         try {
             return JsonParser.parseString(json).getAsJsonObject();
         } catch (Exception e) {
@@ -547,7 +587,7 @@ class ApplicationTest {
         }
     }
 
-    Object jsonSchemaObject() {
+    private Object jsonSchemaObject() {
         // a json schema with one mandatory property named "string"
         String schemaStr = "{" //
             + "\"$schema\": \"http://json-schema.org/draft-04/schema#\"," //
@@ -564,7 +604,7 @@ class ApplicationTest {
         return jsonObject(schemaStr);
     }
 
-    Object jsonObject() {
+    private Object jsonObject() {
         return jsonObject("{ " + EI_JOB_PROPERTY + " : \"value\" }");
     }
 
@@ -572,7 +612,7 @@ class ApplicationTest {
         throws JsonMappingException, JsonProcessingException, ServiceException {
 
         String url = ConsumerConsts.API_ROOT + "/eijobs/" + jobId;
-        String body = gson.toJson(eiJobInfo(eiTypeId));
+        String body = gson.toJson(eiJobInfo(eiTypeId, jobId));
         restClient().putForEntity(url, body).block();
 
         return this.eiJobs.getJob(jobId);
diff --git a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ConsumerSimulatorController.java b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ConsumerSimulatorController.java
new file mode 100644 (file)
index 0000000..562f286
--- /dev/null
@@ -0,0 +1,83 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 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.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import lombok.Getter;
+
+import org.oransc.enrichment.controllers.VoidResponse;
+import org.oransc.enrichment.controllers.consumer.ConsumerEiJobStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController("ConsumerSimulatorController")
+@Api(tags = {"Consumer Callbacks"})
+public class ConsumerSimulatorController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    public static class TestResults {
+
+        public List<ConsumerEiJobStatus> status = Collections.synchronizedList(new ArrayList<ConsumerEiJobStatus>());
+
+        public void reset() {
+            status.clear();
+        }
+    }
+
+    @Getter
+    private TestResults testResults = new TestResults();
+
+    public static String getJobStatusUrl(String eiJobId) {
+        return "/consumer_simulator/eijobs/" + eiJobId + "/status";
+    }
+
+    @PostMapping(path = "/consumer_simulator/eijobs/{eiJobId}/status", produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiOperation(value = "Callback for EI job status", notes = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(code = 200, message = "OK", response = VoidResponse.class)} //
+    )
+    public ResponseEntity<Object> jobStatusCallback( //
+        @PathVariable("eiJobId") String eiJobId, //
+        @RequestBody ConsumerEiJobStatus status) {
+        logger.info("Job status callback status: {} eiJobId: {}", status.state, eiJobId);
+        this.testResults.status.add(status);
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+
+}
index 4c57abd..2a3688c 100644 (file)
@@ -32,15 +32,17 @@ import java.util.List;
 
 import lombok.Getter;
 
-import org.oransc.enrichment.clients.ProducerJobInfo;
 import org.oransc.enrichment.controllers.ErrorResponse;
 import org.oransc.enrichment.controllers.VoidResponse;
+import org.oransc.enrichment.controllers.producer.ProducerJobInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
@@ -51,18 +53,16 @@ public class ProducerSimulatorController {
 
     private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-    public static final String JOB_CREATED_URL = "/producer_simulator/job_created";
-    public static final String JOB_DELETED_URL = "/producer_simulator/job_deleted";
-    public static final String JOB_CREATED_ERROR_URL = "/producer_simulator/job_created_error";
-    public static final String JOB_DELETED_ERROR_URL = "/producer_simulator/job_deleted_error";
+    public static final String JOB_URL = "/producer_simulator/ei_job";
+    public static final String JOB_ERROR_URL = "/producer_simulator/ei_job_error";
 
-    public static final String SUPERVISION_URL = "/producer_simulator/supervision";
-    public static final String SUPERVISION_ERROR_URL = "/producer_simulator/supervision_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 class TestResults {
 
         public List<ProducerJobInfo> jobsStarted = Collections.synchronizedList(new ArrayList<ProducerJobInfo>());
-        public List<ProducerJobInfo> jobsStopped = Collections.synchronizedList(new ArrayList<ProducerJobInfo>());
+        public List<String> jobsStopped = Collections.synchronizedList(new ArrayList<String>());
         public int noOfRejectedCreate = 0;
         public int noOfRejectedDelete = 0;
         public boolean errorFound = false;
@@ -82,7 +82,7 @@ public class ProducerSimulatorController {
     @Getter
     private TestResults testResults = new TestResults();
 
-    @PostMapping(path = JOB_CREATED_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(path = JOB_URL, produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Callback for EI job creation", notes = "")
     @ApiResponses(
         value = { //
@@ -103,24 +103,24 @@ public class ProducerSimulatorController {
         }
     }
 
-    @PostMapping(path = JOB_DELETED_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping(path = "/producer_simulator/ei_job/{eiJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Callback for EI job deletion", notes = "")
     @ApiResponses(
         value = { //
             @ApiResponse(code = 200, message = "OK", response = VoidResponse.class)}//
     )
     public ResponseEntity<Object> jobDeletedCallback( //
-        @RequestBody ProducerJobInfo request) {
+        @PathVariable("eiJobId") String eiJobId) {
         try {
-            logger.info("Job deleted callback {}", request.id);
-            this.testResults.jobsStopped.add(request);
+            logger.info("Job deleted callback {}", eiJobId);
+            this.testResults.jobsStopped.add(eiJobId);
             return new ResponseEntity<>(HttpStatus.OK);
         } catch (Exception e) {
             return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
         }
     }
 
-    @PostMapping(path = JOB_CREATED_ERROR_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(path = JOB_ERROR_URL, produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Callback for EI job creation, returns error", notes = "", hidden = true)
     @ApiResponses(
         value = { //
@@ -133,7 +133,7 @@ public class ProducerSimulatorController {
         return ErrorResponse.create("Producer returns error on create job", HttpStatus.NOT_FOUND);
     }
 
-    @PostMapping(path = JOB_DELETED_ERROR_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping(path = JOB_ERROR_URL + "/{eiJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
     @ApiOperation(value = "Callback for EI job creation, returns error", notes = "", hidden = true)
     @ApiResponses(
         value = { //
diff --git a/pom.xml b/pom.xml
index cb8302e..f21502f 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
         <module>policy-agent</module>
         <module>sdnc-a1-controller</module>
         <module>enrichment-coordinator-service</module>
+        <module>r-app-catalogue</module>
     </modules>
     <build>
         <plugins>
diff --git a/r-app-catalogue/.gitignore b/r-app-catalogue/.gitignore
new file mode 100644 (file)
index 0000000..ad56f2d
--- /dev/null
@@ -0,0 +1,3 @@
+.swagger-codegen-ignore
+.swagger-codegen/
+api/README.md
diff --git a/r-app-catalogue/Dockerfile b/r-app-catalogue/Dockerfile
new file mode 100644 (file)
index 0000000..a85f57d
--- /dev/null
@@ -0,0 +1,39 @@
+#
+# ============LICENSE_START=======================================================
+#  Copyright (C) 2020 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.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=========================================================
+#
+FROM openjdk:11-jre-slim
+
+ARG JAR
+
+WORKDIR /opt/app/r-app-catalogue
+RUN mkdir -p /var/log/r-app-catalogue
+
+EXPOSE 8081 8433
+
+ADD /config/application.yaml /opt/app/r-app-catalogue/config/application.yaml
+ADD target/${JAR} /opt/app/r-app-catalogue/r-app-catalogue.jar
+
+
+RUN chmod -R 777 /opt/app/r-app-catalogue/config/
+
+CMD ["java", "-jar", "/opt/app/r-app-catalogue/r-app-catalogue.jar"]
+
+
+
+
diff --git a/r-app-catalogue/README.md b/r-app-catalogue/README.md
new file mode 100644 (file)
index 0000000..863713d
--- /dev/null
@@ -0,0 +1,27 @@
+# O-RAN-SC Non-RT RIC rAPP Catalogue
+
+The O-RAN Non-RT RIC rApp Catalogue provides an OpenApi 3.0 REST API for services to register themselves and discover
+other services.
+
+**NOTE!** The definition of the REST API is done in the `api/rac-api.json` file. The yaml version of the file is
+generated during compilation.
+
+The application is a SpringBoot application generated using the openapitools openapi-generator-maven-plugin.
+
+To start the application run:
+`mvn spring-boot:run`
+
+## License
+
+Copyright (C) 2020 Nordix Foundation. All rights reserved.
+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.
diff --git a/r-app-catalogue/api/rac-api.json b/r-app-catalogue/api/rac-api.json
new file mode 100644 (file)
index 0000000..3741bdd
--- /dev/null
@@ -0,0 +1,246 @@
+{
+  "openapi": "3.0.0",
+  "info": {
+    "title": "rAPP Catalogue API",
+    "description": "The Non RT-RIC Service Catalogue provides a way for services to register themselves for other services to discover.",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/services": {
+      "get": {
+        "summary": "Service names",
+        "deprecated": false,
+        "operationId": "getServiceNamesUsingGET",
+        "responses": {
+          "200": {
+            "description": "Service names",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "type": "string"
+                  }
+                },
+                "example": [
+                  "DroneIdentifier",
+                  "Collector"
+                ]
+              }
+            }
+          },
+          "401": {
+            "description": "Unauthorized"
+          },
+          "403": {
+            "description": "Forbidden"
+          },
+          "404": {
+            "description": "Not used"
+          }
+        },
+        "tags": [
+          "rAPP Catalogue API"
+        ]
+      }
+    },
+    "/services/{serviceName}": {
+      "get": {
+        "summary": "Individual Service",
+        "deprecated": false,
+        "operationId": "getIndividualServiceUsingGET",
+        "responses": {
+          "200": {
+            "description": "EI Job",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/service"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Unauthorized"
+          },
+          "403": {
+            "description": "Forbidden"
+          },
+          "404": {
+            "description": "Service is not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/error_information"
+                }
+              }
+            }
+          }
+        },
+        "parameters": [
+          {
+            "in": "path",
+            "name": "serviceName",
+            "description": "serviceName",
+            "schema": {
+              "type": "string"
+            },
+            "required": true,
+            "example": "DroneIdentifier"
+          }
+        ],
+        "tags": [
+          "rAPP Catalogue API"
+        ]
+      },
+      "put": {
+        "summary": "Create or update a Service",
+        "deprecated": false,
+        "operationId": "putIndividualServiceUsingPUT",
+        "responses": {
+          "200": {
+            "description": "Service updated"
+          },
+          "201": {
+            "description": "Service created"
+          },
+          "401": {
+            "description": "Unauthorized"
+          },
+          "403": {
+            "description": "Forbidden"
+          },
+          "404": {
+            "description": "Provided service is not correct",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/error_information"
+                },
+                "example": {
+                  "detail": "Service is missing required property version",
+                  "status": 404
+                }
+              }
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "serviceName",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            },
+            "example": "DroneIdentifier"
+          }
+        ],
+        "requestBody": {
+          "description": "Service to create/update",
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/service"
+              }
+            }
+          }
+        },
+        "tags": [
+          "rAPP Catalogue API"
+        ]
+      },
+      "delete": {
+        "summary": "Remove a Service from the catalogue",
+        "deprecated": false,
+        "operationId": "deleteIndividualServiceUsingDELETE",
+        "responses": {
+          "200": {
+            "description": "Not used"
+          },
+          "204": {
+            "description": "Job deleted"
+          },
+          "401": {
+            "description": "Unauthorized"
+          },
+          "403": {
+            "description": "Forbidden"
+          },
+          "404": {
+            "description": "Service is not found",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/error_information"
+                }
+              }
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "serviceName",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            },
+            "example": "DroneIdentifier"
+          }
+        ],
+        "tags": [
+          "rAPP Catalogue API"
+        ]
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "service": {
+        "description": "A Service",
+        "type": "object",
+        "title": "service",
+        "required": [
+          "version"
+        ],
+        "properties": {
+          "version": {
+            "description": "Version of the Service",
+            "type": "string",
+            "example": "1.0.0"
+          },
+          "display_name": {
+            "description": "Display name for the Service",
+            "type": "string",
+            "example": "Drone Identifier"
+          },
+          "description": {
+            "description": "Description of the Service",
+            "type": "string",
+            "example": "Detects if a UE is a drone"
+          }
+        }
+      },
+      "error_information": {
+        "description": "Problem as defined in https://tools.ietf.org/html/rfc7807",
+        "type": "object",
+        "title": "error_information",
+        "properties": {
+          "detail": {
+            "description": "A human-readable explanation specific to this occurrence of the problem.",
+            "type": "string",
+            "example": "Service not found"
+          },
+          "status": {
+            "format": "int32",
+            "description": "The HTTP status code generated by the origin server for this occurrence of the problem.",
+            "type": "integer",
+            "example": 404
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/r-app-catalogue/api/rac-api.yaml b/r-app-catalogue/api/rac-api.yaml
new file mode 100644 (file)
index 0000000..87e2eb9
--- /dev/null
@@ -0,0 +1,175 @@
+openapi: 3.0.0
+info:
+  title: rAPP Catalogue API
+  description: The Non RT-RIC Service Catalogue provides a way for services to register
+    themselves for other services to discover.
+  version: 1.0.0
+servers:
+- url: /
+paths:
+  /services:
+    get:
+      tags:
+      - rAPP Catalogue API
+      summary: Service names
+      operationId: getServiceNamesUsingGET
+      responses:
+        200:
+          description: Service names
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+              example:
+              - DroneIdentifier
+              - Collector
+        401:
+          description: Unauthorized
+        403:
+          description: Forbidden
+        404:
+          description: Not used
+      deprecated: false
+  /services/{serviceName}:
+    get:
+      tags:
+      - rAPP Catalogue API
+      summary: Individual Service
+      operationId: getIndividualServiceUsingGET
+      parameters:
+      - name: serviceName
+        in: path
+        description: serviceName
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+        example: DroneIdentifier
+      responses:
+        200:
+          description: EI Job
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/service'
+        401:
+          description: Unauthorized
+        403:
+          description: Forbidden
+        404:
+          description: Service is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/error_information'
+      deprecated: false
+    put:
+      tags:
+      - rAPP Catalogue API
+      summary: Create or update a Service
+      operationId: putIndividualServiceUsingPUT
+      parameters:
+      - name: serviceName
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+        example: DroneIdentifier
+      requestBody:
+        description: Service to create/update
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/service'
+        required: true
+      responses:
+        200:
+          description: Service updated
+        201:
+          description: Service created
+        401:
+          description: Unauthorized
+        403:
+          description: Forbidden
+        404:
+          description: Provided service is not correct
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/error_information'
+              example:
+                detail: Service is missing required property version
+                status: 404
+      deprecated: false
+    delete:
+      tags:
+      - rAPP Catalogue API
+      summary: Remove a Service from the catalogue
+      operationId: deleteIndividualServiceUsingDELETE
+      parameters:
+      - name: serviceName
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+        example: DroneIdentifier
+      responses:
+        200:
+          description: Not used
+        204:
+          description: Job deleted
+        401:
+          description: Unauthorized
+        403:
+          description: Forbidden
+        404:
+          description: Service is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/error_information'
+      deprecated: false
+components:
+  schemas:
+    service:
+      title: service
+      required:
+      - version
+      type: object
+      properties:
+        version:
+          type: string
+          description: Version of the Service
+          example: 1.0.0
+        display_name:
+          type: string
+          description: Display name for the Service
+          example: Drone Identifier
+        description:
+          type: string
+          description: Description of the Service
+          example: Detects if a UE is a drone
+      description: A Service
+    error_information:
+      title: error_information
+      type: object
+      properties:
+        detail:
+          type: string
+          description: A human-readable explanation specific to this occurrence of
+            the problem.
+          example: Service not found
+        status:
+          type: integer
+          description: The HTTP status code generated by the origin server for this
+            occurrence of the problem.
+          format: int32
+          example: 404
+      description: Problem as defined in https://tools.ietf.org/html/rfc7807
diff --git a/r-app-catalogue/config/application.yaml b/r-app-catalogue/config/application.yaml
new file mode 100644 (file)
index 0000000..fadf7d2
--- /dev/null
@@ -0,0 +1,4 @@
+spring:
+  profiles:
+    active: prod
+
diff --git a/r-app-catalogue/pom.xml b/r-app-catalogue/pom.xml
new file mode 100644 (file)
index 0000000..3ac8562
--- /dev/null
@@ -0,0 +1,250 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<!--\r
+* ========================LICENSE_START=================================\r
+* O-RAN-SC\r
+* %%\r
+* Copyright (C) 2020 Nordix Foundation\r
+* %%\r
+* Licensed under the Apache License, Version 2.0 (the "License");\r
+* you may not use this file except in compliance with the License.\r
+* You may obtain a copy of the License at\r
+*\r
+* http://www.apache.org/licenses/LICENSE-2.0\r
+*\r
+* Unless required by applicable law or agreed to in writing, software\r
+* distributed under the License is distributed on an "AS IS" BASIS,\r
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+* See the License for the specific language governing permissions and\r
+* limitations under the License.\r
+* ========================LICENSE_END===================================\r
+-->\r
+<project xmlns="http://maven.apache.org/POM/4.0.0"\r
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\r
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">\r
+    <modelVersion>4.0.0</modelVersion>\r
+\r
+    <parent>\r
+        <groupId>org.springframework.boot</groupId>\r
+        <artifactId>spring-boot-starter-parent</artifactId>\r
+        <version>2.3.4.RELEASE</version>\r
+        <relativePath />\r
+    </parent>\r
+    <groupId>org.o-ran-sc.nonrtric</groupId>\r
+    <artifactId>r-app-catalogue</artifactId>\r
+    <version>1.0.0-SNAPSHOT</version>\r
+    <licenses>\r
+        <license>\r
+            <name>The Apache Software License, Version 2.0</name>\r
+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\r
+        </license>\r
+    </licenses>\r
+    <properties>\r
+        <java.version>11</java.version>\r
+        <swagger-annotations.version>1.5.22</swagger-annotations.version>\r
+        <springfox.version>2.9.2</springfox.version>\r
+        <jackson-databind-nullable.version>0.2.1</jackson-databind-nullable.version>\r
+        <openapi-generator-maven-plugin.version>4.3.1</openapi-generator-maven-plugin.version>\r
+        <swagger-codegen-maven-plugin.version>3.0.11</swagger-codegen-maven-plugin.version>\r
+        <docker-maven-plugin.version>0.30.0</docker-maven-plugin.version>\r
+    </properties>\r
+\r
+    <dependencies>\r
+        <dependency>\r
+            <groupId>io.swagger</groupId>\r
+            <artifactId>swagger-annotations</artifactId>\r
+            <version>${swagger-annotations.version}</version>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>com.fasterxml.jackson.core</groupId>\r
+            <artifactId>jackson-annotations</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.springframework</groupId>\r
+            <artifactId>spring-beans</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.springframework.boot</groupId>\r
+            <artifactId>spring-boot-autoconfigure</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.springframework</groupId>\r
+            <artifactId>spring-web</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.springframework.boot</groupId>\r
+            <artifactId>spring-boot</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.springframework</groupId>\r
+            <artifactId>spring-webmvc</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.springframework</groupId>\r
+            <artifactId>spring-context</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>io.springfox</groupId>\r
+            <artifactId>springfox-swagger2</artifactId>\r
+            <version>${springfox.version}</version>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>io.springfox</groupId>\r
+            <artifactId>springfox-core</artifactId>\r
+            <version>${springfox.version}</version>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>io.springfox</groupId>\r
+            <artifactId>springfox-spring-web</artifactId>\r
+            <version>${springfox.version}</version>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>io.springfox</groupId>\r
+            <artifactId>springfox-spi</artifactId>\r
+            <version>${springfox.version}</version>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.assertj</groupId>\r
+            <artifactId>assertj-core</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.apache.tomcat.embed</groupId>\r
+            <artifactId>tomcat-embed-core</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.openapitools</groupId>\r
+            <artifactId>jackson-databind-nullable</artifactId>\r
+            <version>${jackson-databind-nullable.version}</version>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>javax.validation</groupId>\r
+            <artifactId>validation-api</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>com.fasterxml.jackson.core</groupId>\r
+            <artifactId>jackson-databind</artifactId>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.yaml</groupId>\r
+            <artifactId>snakeyaml</artifactId>\r
+            <scope>runtime</scope>\r
+        </dependency>\r
+        <!-- TEST -->\r
+        <dependency>\r
+            <groupId>org.springframework</groupId>\r
+            <artifactId>spring-test</artifactId>\r
+            <scope>test</scope>\r
+        </dependency>\r
+        <dependency>\r
+            <groupId>org.junit.jupiter</groupId>\r
+            <artifactId>junit-jupiter-api</artifactId>\r
+            <scope>test</scope>\r
+        </dependency>\r
+    </dependencies>\r
+\r
+    <build>\r
+        <plugins>\r
+            <plugin>\r
+                <groupId>org.openapitools</groupId>\r
+                <artifactId>openapi-generator-maven-plugin</artifactId>\r
+                <version>${openapi-generator-maven-plugin.version}</version>\r
+                <executions>\r
+                    <execution>\r
+                        <goals>\r
+                            <goal>generate</goal>\r
+                        </goals>\r
+                        <configuration>\r
+                            <inputSpec>${project.basedir}/api/rac-api.json</inputSpec>\r
+                            <generatorName>spring</generatorName>\r
+                            <apiPackage>org.oransc.rappcatalogue.api</apiPackage>\r
+                            <modelPackage>org.oransc.rappcatalogue.model</modelPackage>\r
+                            <configOptions>\r
+                                <delegatePattern>true</delegatePattern>\r
+                            </configOptions>\r
+                        </configuration>\r
+                    </execution>\r
+                </executions>\r
+            </plugin>\r
+            <plugin>\r
+                <groupId>io.swagger.codegen.v3</groupId>\r
+                <artifactId>swagger-codegen-maven-plugin</artifactId>\r
+                <version>${swagger-codegen-maven-plugin.version}</version>\r
+                <executions>\r
+                    <execution>\r
+                        <goals>\r
+                            <goal>generate</goal>\r
+                        </goals>\r
+                        <configuration>\r
+                            <inputSpec>${project.basedir}/api/rac-api.json</inputSpec>\r
+                            <language>openapi-yaml</language>\r
+                            <output>${project.basedir}/api/</output>\r
+                            <configOptions>\r
+                                <outputFile>rac-api.yaml</outputFile>\r
+                            </configOptions>\r
+                        </configuration>\r
+                    </execution>\r
+                </executions>\r
+            </plugin>\r
+            <plugin>\r
+                <groupId>io.fabric8</groupId>\r
+                <artifactId>docker-maven-plugin</artifactId>\r
+                <version>${docker-maven-plugin.version}</version>\r
+                <inherited>false</inherited>\r
+                <executions>\r
+                    <execution>\r
+                        <id>generate-r-app-catalogue-image</id>\r
+                        <phase>package</phase>\r
+                        <goals>\r
+                            <goal>build</goal>\r
+                        </goals>\r
+                        <configuration>\r
+                            <pullRegistry>${env.CONTAINER_PULL_REGISTRY}</pullRegistry>\r
+                            <images>\r
+                                <image>\r
+                                    <name>o-ran-sc/nonrtric-r-app-catalogue:${project.version}</name>\r
+                                    <build>\r
+                                        <cleanup>try</cleanup>\r
+                                        <contextDir>${basedir}</contextDir>\r
+                                        <dockerFile>Dockerfile</dockerFile>\r
+                                        <args>\r
+                                            <JAR>${project.build.finalName}.jar</JAR>\r
+                                        </args>\r
+                                        <tags>\r
+                                            <tag>${project.version}</tag>\r
+                                        </tags>\r
+                                    </build>\r
+                                </image>\r
+                            </images>\r
+                        </configuration>\r
+                    </execution>\r
+                    <execution>\r
+                        <id>push-r-app-catalogue-image</id>\r
+                        <goals>\r
+                            <goal>build</goal>\r
+                            <goal>push</goal>\r
+                        </goals>\r
+                        <configuration>\r
+                            <pullRegistry>${env.CONTAINER_PULL_REGISTRY}</pullRegistry>\r
+                            <pushRegistry>${env.CONTAINER_PUSH_REGISTRY}</pushRegistry>\r
+                            <images>\r
+                                <image>\r
+                                    <name>o-ran-sc/nonrtric-r-app-catalogue:${project.version}</name>\r
+                                    <build>\r
+                                        <contextDir>${basedir}</contextDir>\r
+                                        <dockerFile>Dockerfile</dockerFile>\r
+                                        <args>\r
+                                            <JAR>${project.build.finalName}.jar</JAR>\r
+                                        </args>\r
+                                        <tags>\r
+                                            <tag>${project.version}</tag>\r
+                                            <tag>latest</tag>\r
+                                        </tags>\r
+                                    </build>\r
+                                </image>\r
+                            </images>\r
+                        </configuration>\r
+                    </execution>\r
+                </executions>\r
+            </plugin>\r
+        </plugins>\r
+    </build>\r
+</project>
\ No newline at end of file
diff --git a/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java b/r-app-catalogue/src/main/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImpl.java
new file mode 100644 (file)
index 0000000..701f1d8
--- /dev/null
@@ -0,0 +1,34 @@
+package org.oransc.rappcatalogue.api;\r
+\r
+import java.util.Arrays;\r
+import java.util.List;\r
+\r
+import org.springframework.http.HttpStatus;\r
+import org.springframework.http.ResponseEntity;\r
+\r
+@org.springframework.stereotype.Service\r
+public class ServicesApiDelegateImpl implements ServicesApiDelegate {\r
+\r
+    @Override\r
+    public ResponseEntity<Void> deleteIndividualServiceUsingDELETE(String serviceName) {\r
+        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);\r
+    }\r
+\r
+    // @Override\r
+    // public ResponseEntity<Service> getIndividualServiceUsingGET(String serviceName) {\r
+    //     return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);\r
+\r
+    // }\r
+\r
+    @Override\r
+    public ResponseEntity<List<String>> getServiceNamesUsingGET() {\r
+        List<String> services = Arrays.asList("a", "b");\r
+        return ResponseEntity.ok(services);\r
+    }\r
+\r
+    // @Override\r
+    // public ResponseEntity<Void> putIndividualServiceUsingPUT(String serviceName, Service service) {\r
+    //     return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);\r
+\r
+    // }\r
+}\r
diff --git a/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java b/r-app-catalogue/src/test/java/org/oransc/rappcatalogue/api/ServicesApiDelegateImplTest.java
new file mode 100644 (file)
index 0000000..53dfc1a
--- /dev/null
@@ -0,0 +1,33 @@
+package org.oransc.rappcatalogue.api;\r
+\r
+import static org.assertj.core.api.Assertions.assertThat;\r
+\r
+import java.util.Arrays;\r
+import java.util.List;\r
+\r
+import org.junit.jupiter.api.Test;\r
+import org.junit.jupiter.api.extension.ExtendWith;\r
+import org.springframework.http.HttpStatus;\r
+import org.springframework.http.ResponseEntity;\r
+import org.springframework.test.context.junit.jupiter.SpringExtension;\r
+\r
+@ExtendWith(SpringExtension.class)\r
+class ServicesApiDelegateImplTest {\r
+\r
+    @Test\r
+    void putValidService_shouldBeOk() {\r
+        ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl();\r
+\r
+        ResponseEntity<List<String>> response = delegateUnderTest.getServiceNamesUsingGET();\r
+    }\r
+\r
+    @Test\r
+    void getServices_shouldProvideArrayOfServices() throws Exception {\r
+        ServicesApiDelegateImpl delegateUnderTest = new ServicesApiDelegateImpl();\r
+\r
+        ResponseEntity<List<String>> response = delegateUnderTest.getServiceNamesUsingGET();\r
+\r
+        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);\r
+        assertThat(response.getBody()).isEqualTo(Arrays.asList("a", "b"));\r
+    }\r
+}\r
diff --git a/tox.ini b/tox.ini
index 4491722..2705e16 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -24,23 +24,14 @@ skipsdist = true
 
 [testenv:docs]
 basepython = python3
-deps =
-    sphinx
-    sphinx-rtd-theme
-    sphinxcontrib-httpdomain
-    recommonmark
-    lfdocs-conf
+deps = -r{toxinidir}/docs/requirements-docs.txt
 
 commands =
-    sphinx-build -W -b html -n -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/html
+    sphinx-build -W -b html -n -d {envtmpdir}/docs/doctrees ./docs/ {toxinidir}/docs/_build/html
     echo "Generated docs available in {toxinidir}/docs/_build/html"
 whitelist_externals = echo
 
 [testenv:docs-linkcheck]
 basepython = python3
-deps = sphinx
-       sphinx-rtd-theme
-       sphinxcontrib-httpdomain
-       recommonmark
-       lfdocs-conf
+deps = -r{toxinidir}/docs/requirements-docs.txt
 commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck