Seed code 33/8033/1
authorelinuxhenrik <henrik.b.andersson@est.tech>
Mon, 4 Apr 2022 13:47:19 +0000 (15:47 +0200)
committerelinuxhenrik <henrik.b.andersson@est.tech>
Mon, 4 Apr 2022 13:47:23 +0000 (15:47 +0200)
Issue-ID: NONRTRIC-716
Signed-off-by: elinuxhenrik <henrik.b.andersson@est.tech>
Change-Id: I3db047b6e46530987da171508b33e68c27e2763e

74 files changed:
.gitignore [new file with mode: 0644]
.readthedocs.yaml [new file with mode: 0644]
Dockerfile [new file with mode: 0644]
LICENSE.txt [new file with mode: 0644]
api/ics-api.json [new file with mode: 0644]
api/ics-api.yaml [new file with mode: 0644]
config/README [new file with mode: 0644]
config/application.yaml [new file with mode: 0644]
config/keystore.jks [new file with mode: 0644]
config/truststore.jks [new file with mode: 0644]
docs/_static/logo.png [new file with mode: 0644]
docs/api-docs.rst [new file with mode: 0644]
docs/conf.py [new file with mode: 0644]
docs/conf.yaml [new file with mode: 0644]
docs/developer-guide.rst [new file with mode: 0644]
docs/favicon.ico [new file with mode: 0644]
docs/images/swagger.png [new file with mode: 0644]
docs/images/yaml_logo.png [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/installation-guide.rst [new file with mode: 0644]
docs/overview.rst [new file with mode: 0644]
docs/release-notes.rst [new file with mode: 0644]
docs/requirements-docs.txt [new file with mode: 0644]
eclipse-formatter.xml [new file with mode: 0644]
pom.xml [new file with mode: 0644]
src/main/java/org/oransc/ics/Application.java [new file with mode: 0644]
src/main/java/org/oransc/ics/BeanFactory.java [new file with mode: 0644]
src/main/java/org/oransc/ics/SwaggerConfig.java [new file with mode: 0644]
src/main/java/org/oransc/ics/clients/AsyncRestClient.java [new file with mode: 0644]
src/main/java/org/oransc/ics/clients/AsyncRestClientFactory.java [new file with mode: 0644]
src/main/java/org/oransc/ics/clients/SecurityContext.java [new file with mode: 0644]
src/main/java/org/oransc/ics/configuration/ApplicationConfig.java [new file with mode: 0644]
src/main/java/org/oransc/ics/configuration/WebClientConfig.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/ErrorResponse.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/StatusController.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/VoidResponse.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/a1e/A1eController.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/a1e/A1eEiJobInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/a1e/A1eEiJobStatus.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/a1e/A1eEiTypeInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerInfoTypeInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerJobInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerJobStatus.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerTypeRegistrationInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerTypeSubscriptionInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1producer/ProducerInfoTypeInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1producer/ProducerRegistrationInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/controllers/r1producer/ProducerStatusInfo.java [new file with mode: 0644]
src/main/java/org/oransc/ics/exceptions/ServiceException.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoJob.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoJobs.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoProducer.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoProducers.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoType.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/InfoTypes.java [new file with mode: 0644]
src/main/java/org/oransc/ics/repository/MultiMap.java [new file with mode: 0644]
src/main/java/org/oransc/ics/tasks/ProducerSupervision.java [new file with mode: 0644]
src/test/java/org/oransc/ics/ApplicationTest.java [new file with mode: 0644]
src/test/java/org/oransc/ics/MockInformationService.java [new file with mode: 0644]
src/test/java/org/oransc/ics/clients/AsyncRestClientTest.java [new file with mode: 0644]
src/test/java/org/oransc/ics/controller/A1eCallbacksSimulatorController.java [new file with mode: 0644]
src/test/java/org/oransc/ics/controller/ConsumerSimulatorController.java [new file with mode: 0644]
src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java [new file with mode: 0644]
tox.ini [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..80763d8
--- /dev/null
@@ -0,0 +1,23 @@
+# Documentation
+.idea/
+.tox
+docs/_build/
+.DS_STORE
+.swagger*
+docs/offeredapis/swagger/README.md
+
+# Eclipse
+.checkstyle
+.classpath
+target/
+.sts4-cache
+.project
+.settings
+.pydevproject
+infer-out/
+/target/
+
+.vscode
+.factorypath
+
+coverage.*
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644 (file)
index 0000000..095222a
--- /dev/null
@@ -0,0 +1,16 @@
+---
+version: 2
+
+formats:
+  - htmlzip
+
+build:
+  image: latest
+
+python:
+  version: 3.7
+  install:
+    - requirements: docs/requirements-docs.txt
+
+sphinx:
+  configuration: docs/conf.py
diff --git a/Dockerfile b/Dockerfile
new file mode 100644 (file)
index 0000000..cc8813e
--- /dev/null
@@ -0,0 +1,51 @@
+#
+# ============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/information-coordinator-service
+RUN mkdir -p /var/log/information-coordinator-service
+RUN mkdir -p /opt/app/information-coordinator-service/etc/cert/
+RUN mkdir -p /var/information-coordinator-service
+
+EXPOSE 8083 8434
+
+ADD /config/application.yaml /opt/app/information-coordinator-service/config/application.yaml
+ADD target/${JAR} /opt/app/information-coordinator-service/information-coordinator-service.jar
+ADD /config/keystore.jks /opt/app/information-coordinator-service/etc/cert/keystore.jks
+ADD /config/truststore.jks /opt/app/information-coordinator-service/etc/cert/truststore.jks
+
+ARG user=nonrtric
+ARG group=nonrtric
+
+RUN groupadd $user && \
+    useradd -r -g $group $user
+RUN chown -R $user:$group /opt/app/information-coordinator-service
+RUN chown -R $user:$group /var/log/information-coordinator-service
+RUN chown -R $user:$group /var/information-coordinator-service
+
+USER ${user}
+
+CMD ["java", "-jar", "/opt/app/information-coordinator-service/information-coordinator-service.jar"]
+
+
+
+
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..96589bf
--- /dev/null
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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/api/ics-api.json b/api/ics-api.json
new file mode 100644 (file)
index 0000000..56bbcb5
--- /dev/null
@@ -0,0 +1,1289 @@
+{
+    "components": {"schemas": {
+        "consumer_information_type": {
+            "description": "Information for an Information type",
+            "type": "object",
+            "required": [
+                "job_data_schema",
+                "no_of_producers",
+                "type_status"
+            ],
+            "properties": {
+                "no_of_producers": {
+                    "format": "int32",
+                    "description": "The number of registered producers for the type",
+                    "type": "integer"
+                },
+                "type_status": {
+                    "description": "Allowed values: <br/>ENABLED: one or several producers for the information type are available <br/>DISABLED: no producers for the information type are available",
+                    "type": "string",
+                    "enum": [
+                        "ENABLED",
+                        "DISABLED"
+                    ]
+                },
+                "job_data_schema": {
+                    "description": "Json schema for the job data",
+                    "type": "object"
+                }
+            }
+        },
+        "EiTypeObject": {
+            "description": "Information for an EI type",
+            "type": "object"
+        },
+        "service_status_info": {
+            "type": "object",
+            "required": [
+                "no_of_jobs",
+                "no_of_producers",
+                "no_of_types",
+                "status"
+            ],
+            "properties": {
+                "no_of_producers": {
+                    "format": "int32",
+                    "description": "Number of Information Producers",
+                    "type": "integer"
+                },
+                "no_of_types": {
+                    "format": "int32",
+                    "description": "Number of Information Types",
+                    "type": "integer"
+                },
+                "no_of_jobs": {
+                    "format": "int32",
+                    "description": "Number of Information Jobs",
+                    "type": "integer"
+                },
+                "status": {
+                    "description": "status text",
+                    "type": "string"
+                }
+            }
+        },
+        "producer_registration_info": {
+            "description": "Information for an Information Producer",
+            "type": "object",
+            "required": [
+                "info_job_callback_url",
+                "info_producer_supervision_callback_url",
+                "supported_info_types"
+            ],
+            "properties": {
+                "info_producer_supervision_callback_url": {
+                    "description": "callback for producer supervision",
+                    "type": "string"
+                },
+                "supported_info_types": {
+                    "description": "Supported Information Type IDs",
+                    "type": "array",
+                    "items": {
+                        "description": "Supported Information Type IDs",
+                        "type": "string"
+                    }
+                },
+                "info_job_callback_url": {
+                    "description": "callback for Information Job",
+                    "type": "string"
+                }
+            }
+        },
+        "consumer_type_registration_info": {
+            "description": "Information for an Information type",
+            "type": "object",
+            "required": [
+                "info_type_id",
+                "job_data_schema",
+                "status"
+            ],
+            "properties": {
+                "info_type_id": {
+                    "description": "Information type identifier",
+                    "type": "string"
+                },
+                "job_data_schema": {
+                    "description": "Json schema for the job data",
+                    "type": "object"
+                },
+                "status": {
+                    "description": "Allowed values: <br/>REGISTERED: the information type has been registered <br/>DEREGISTERED: the information type has been removed",
+                    "type": "string",
+                    "enum": [
+                        "REGISTERED",
+                        "DEREGISTERED"
+                    ]
+                }
+            }
+        },
+        "ProblemDetails": {
+            "description": "A problem detail to carry details in a HTTP response according to RFC 7807",
+            "type": "object",
+            "properties": {
+                "detail": {
+                    "description": "A human-readable explanation specific to this occurrence of the problem.",
+                    "type": "string",
+                    "example": "Information Job type 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
+                }
+            }
+        },
+        "EiJobStatusObject": {
+            "description": "Status for an EI job",
+            "type": "object",
+            "required": ["eiJobStatus"],
+            "properties": {"eiJobStatus": {
+                "description": "Allowed values for EI job status: <br/>ENABLED: the A1-EI producer is able to deliver EI result for the EI job <br/>DISABLED: the A1-EI producer is unable to deliver EI result for the EI job",
+                "type": "string",
+                "enum": [
+                    "ENABLED",
+                    "DISABLED"
+                ]
+            }}
+        },
+        "consumer_job_status": {
+            "description": "Status for an Information Job",
+            "type": "object",
+            "required": [
+                "info_job_status",
+                "producers"
+            ],
+            "properties": {
+                "info_job_status": {
+                    "description": "Allowed values: <br/>ENABLED: the A1-Information producer is able to deliver result for the Information Job <br/>DISABLED: the A1-Information producer is unable to deliver result for the Information Job",
+                    "type": "string",
+                    "enum": [
+                        "ENABLED",
+                        "DISABLED"
+                    ]
+                },
+                "producers": {
+                    "description": "An array of all registered Information Producer Identifiers.",
+                    "type": "array",
+                    "items": {
+                        "description": "An array of all registered Information Producer Identifiers.",
+                        "type": "string"
+                    }
+                }
+            }
+        },
+        "EiJobObject": {
+            "description": "Information for an Enrichment Information Job",
+            "type": "object",
+            "required": [
+                "eiTypeId",
+                "jobDefinition",
+                "jobOwner",
+                "jobResultUri"
+            ],
+            "properties": {
+                "eiTypeId": {
+                    "description": "EI type Idenitifier of the EI job",
+                    "type": "string"
+                },
+                "jobResultUri": {
+                    "description": "The target URI of the EI data",
+                    "type": "string"
+                },
+                "jobOwner": {
+                    "description": "Identity of the owner of the job",
+                    "type": "string"
+                },
+                "statusNotificationUri": {
+                    "description": "The target of EI job status notifications",
+                    "type": "string"
+                },
+                "jobDefinition": {
+                    "description": "EI type specific job data",
+                    "type": "object"
+                }
+            }
+        },
+        "producer_info_type_info": {
+            "description": "Information for an Information Type",
+            "type": "object",
+            "required": ["info_job_data_schema"],
+            "properties": {
+                "info_type_information": {
+                    "description": "Type specific information for the information type",
+                    "type": "object"
+                },
+                "info_job_data_schema": {
+                    "description": "Json schema for the job data",
+                    "type": "object"
+                }
+            }
+        },
+        "producer_info_job_request": {
+            "description": "The body of the Information Producer callbacks for Information Job creation and deletion",
+            "type": "object",
+            "required": ["info_job_identity"],
+            "properties": {
+                "owner": {
+                    "description": "The owner of the job",
+                    "type": "string"
+                },
+                "last_updated": {
+                    "description": "The time when the job was last updated or created (ISO-8601)",
+                    "type": "string"
+                },
+                "info_job_identity": {
+                    "description": "Identity of the Information Job",
+                    "type": "string"
+                },
+                "target_uri": {
+                    "description": "URI for the target of the produced Information",
+                    "type": "string"
+                },
+                "info_job_data": {
+                    "description": "Json for the job data",
+                    "type": "object"
+                },
+                "info_type_identity": {
+                    "description": "Type identity for the job",
+                    "type": "string"
+                }
+            }
+        },
+        "consumer_job": {
+            "description": "Information for an Information Job",
+            "type": "object",
+            "required": [
+                "info_type_id",
+                "job_definition",
+                "job_owner",
+                "job_result_uri"
+            ],
+            "properties": {
+                "info_type_id": {
+                    "description": "Information type Idenitifier of the subscription job",
+                    "type": "string"
+                },
+                "job_result_uri": {
+                    "description": "The target URI of the subscribed information",
+                    "type": "string"
+                },
+                "job_owner": {
+                    "description": "Identity of the owner of the job",
+                    "type": "string"
+                },
+                "job_definition": {
+                    "description": "Information type specific job data",
+                    "type": "object"
+                },
+                "status_notification_uri": {
+                    "description": "The target of Information subscription job status notifications",
+                    "type": "string"
+                }
+            }
+        },
+        "producer_status": {
+            "description": "Status for an Info Producer",
+            "type": "object",
+            "required": ["operational_state"],
+            "properties": {"operational_state": {
+                "description": "Represents the operational states",
+                "type": "string",
+                "enum": [
+                    "ENABLED",
+                    "DISABLED"
+                ]
+            }}
+        },
+        "Void": {
+            "description": "Void/empty ",
+            "type": "object"
+        },
+        "Link": {
+            "type": "object",
+            "properties": {
+                "templated": {"type": "boolean"},
+                "href": {"type": "string"}
+            }
+        },
+        "consumer_type_subscription_info": {
+            "description": "Information for an information type subscription",
+            "type": "object",
+            "required": [
+                "owner",
+                "status_result_uri"
+            ],
+            "properties": {
+                "owner": {
+                    "description": "Identity of the owner of the subscription",
+                    "type": "string"
+                },
+                "status_result_uri": {
+                    "description": "The target URI of the subscribed information",
+                    "type": "string"
+                }
+            }
+        }
+    }},
+    "openapi": "3.0.1",
+    "paths": {
+        "/data-producer/v1/info-types": {"get": {
+            "summary": "Info Type identifiers",
+            "operationId": "getInfoTypdentifiers",
+            "responses": {"200": {
+                "description": "Info Type identifiers",
+                "content": {"application/json": {"schema": {
+                    "type": "array",
+                    "items": {"type": "string"}
+                }}}
+            }},
+            "tags": ["Data producer (registration)"]
+        }},
+        "/actuator/threaddump": {"get": {
+            "summary": "Actuator web endpoint 'threaddump'",
+            "operationId": "threaddump_2",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/A1-EI/v1/eitypes/{eiTypeId}": {"get": {
+            "summary": "Individual EI type",
+            "operationId": "getEiType",
+            "responses": {
+                "200": {
+                    "description": "EI type",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EiTypeObject"}}}
+                },
+                "404": {
+                    "description": "Enrichment Information type is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "eiTypeId",
+                "required": true
+            }],
+            "tags": ["A1-EI (registration)"]
+        }},
+        "/data-producer/v1/info-types/{infoTypeId}": {
+            "get": {
+                "summary": "Individual Information Type",
+                "operationId": "getInfoType",
+                "responses": {
+                    "200": {
+                        "description": "Info Type",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_info_type_info"}}}
+                    },
+                    "404": {
+                        "description": "Information type is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoTypeId",
+                    "required": true
+                }],
+                "tags": ["Data producer (registration)"]
+            },
+            "delete": {
+                "summary": "Individual Information Type",
+                "operationId": "deleteInfoType",
+                "responses": {
+                    "200": {
+                        "description": "Not used",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "204": {
+                        "description": "Producer deleted",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "404": {
+                        "description": "Information type is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    },
+                    "409": {
+                        "description": "The Information type has one or several active producers",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoTypeId",
+                    "required": true
+                }],
+                "tags": ["Data producer (registration)"]
+            },
+            "put": {
+                "summary": "Individual Information Type",
+                "requestBody": {
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_info_type_info"}}},
+                    "required": true
+                },
+                "operationId": "putInfoType",
+                "responses": {
+                    "200": {
+                        "description": "Type updated",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "201": {
+                        "description": "Type created",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "400": {
+                        "description": "Input validation failed",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoTypeId",
+                    "required": true
+                }],
+                "tags": ["Data producer (registration)"]
+            }
+        },
+        "/data-consumer/v1/info-type-subscription/{subscriptionId}": {
+            "get": {
+                "summary": "Individual subscription for information types (registration/deregistration)",
+                "operationId": "getIndividualTypeSubscription",
+                "responses": {
+                    "200": {
+                        "description": "Type subscription",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_type_subscription_info"}}}
+                    },
+                    "404": {
+                        "description": "Subscription is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "subscriptionId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            },
+            "delete": {
+                "summary": "Individual subscription for information types (registration/deregistration)",
+                "operationId": "deleteIndividualTypeSubscription",
+                "responses": {
+                    "200": {
+                        "description": "Not used",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "204": {
+                        "description": "Subscription deleted",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "404": {
+                        "description": "Subscription is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "subscriptionId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            },
+            "put": {
+                "summary": "Individual subscription for information types (registration/deregistration)",
+                "requestBody": {
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_type_subscription_info"}}},
+                    "required": true
+                },
+                "description": "This service operation is used to subscribe to notifications for changes in the availability of data types.",
+                "operationId": "putIndividualTypeSubscription",
+                "responses": {
+                    "200": {
+                        "description": "Subscription updated",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "201": {
+                        "description": "Subscription created",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "subscriptionId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            }
+        },
+        "/example-dataproducer/health-check": {"get": {
+            "summary": "Producer supervision",
+            "description": "The endpoint is provided by the Information Producer and is used for supervision of the producer.",
+            "operationId": "producerSupervision",
+            "responses": {"200": {
+                "description": "The producer is OK",
+                "content": {"application/json": {"schema": {"type": "string"}}}
+            }},
+            "tags": ["Data producer (callbacks)"]
+        }},
+        "/actuator/loggers": {"get": {
+            "summary": "Actuator web endpoint 'loggers'",
+            "operationId": "loggers",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/actuator/health/**": {"get": {
+            "summary": "Actuator web endpoint 'health-path'",
+            "operationId": "health-path",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/data-consumer/v1/info-types": {"get": {
+            "summary": "Information type identifiers",
+            "operationId": "getinfoTypeIdentifiers",
+            "responses": {"200": {
+                "description": "Information type identifiers",
+                "content": {"application/json": {"schema": {
+                    "type": "array",
+                    "items": {"type": "string"}
+                }}}
+            }},
+            "tags": ["Data consumer"]
+        }},
+        "/example-dataconsumer/info-type-status": {"post": {
+            "summary": "Callback for changed Information type registration status",
+            "requestBody": {
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_type_registration_info"}}},
+                "required": true
+            },
+            "description": "The primitive is implemented by the data consumer and is invoked when a Information type status has been changed. <br/>Subscription are managed by primitives in 'Data consumer'",
+            "operationId": "typeStatusCallback",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+            }},
+            "tags": ["Data consumer (callbacks)"]
+        }},
+        "/actuator/metrics/{requiredMetricName}": {"get": {
+            "summary": "Actuator web endpoint 'metrics-requiredMetricName'",
+            "operationId": "metrics-requiredMetricName",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "requiredMetricName",
+                "required": true
+            }],
+            "tags": ["Actuator"]
+        }},
+        "/actuator": {"get": {
+            "summary": "Actuator root web endpoint",
+            "operationId": "links",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {
+                    "additionalProperties": {
+                        "additionalProperties": {"$ref": "#/components/schemas/Link"},
+                        "type": "object"
+                    },
+                    "type": "object"
+                }}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/data-consumer/v1/info-jobs": {"get": {
+            "summary": "Information Job identifiers",
+            "description": "query for information job identifiers",
+            "operationId": "getJobIds",
+            "responses": {
+                "200": {
+                    "description": "Information information job identifiers",
+                    "content": {"application/json": {"schema": {
+                        "type": "array",
+                        "items": {"type": "string"}
+                    }}}
+                },
+                "404": {
+                    "description": "Information type is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [
+                {
+                    "schema": {"type": "string"},
+                    "in": "query",
+                    "name": "infoTypeId",
+                    "description": "selects subscription jobs of matching information type",
+                    "required": false
+                },
+                {
+                    "schema": {"type": "string"},
+                    "in": "query",
+                    "name": "owner",
+                    "description": "selects result for one owner",
+                    "required": false
+                }
+            ],
+            "tags": ["Data consumer"]
+        }},
+        "/actuator/loggers/{name}": {
+            "post": {
+                "summary": "Actuator web endpoint 'loggers-name'",
+                "operationId": "loggers-name",
+                "responses": {"200": {
+                    "description": "OK",
+                    "content": {"*/*": {"schema": {"type": "object"}}}
+                }},
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "name",
+                    "required": true
+                }],
+                "tags": ["Actuator"]
+            },
+            "get": {
+                "summary": "Actuator web endpoint 'loggers-name'",
+                "operationId": "loggers-name_2",
+                "responses": {"200": {
+                    "description": "OK",
+                    "content": {"*/*": {"schema": {"type": "object"}}}
+                }},
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "name",
+                    "required": true
+                }],
+                "tags": ["Actuator"]
+            }
+        },
+        "/example-dataproducer/info-job": {"post": {
+            "summary": "Callback for Information Job creation/modification",
+            "requestBody": {
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_info_job_request"}}},
+                "required": true
+            },
+            "description": "The call is invoked to activate or to modify a data subscription. The endpoint is provided by the Information Producer.",
+            "operationId": "jobCreatedCallback",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+            }},
+            "tags": ["Data producer (callbacks)"]
+        }},
+        "/example-dataproducer/info-job/{infoJobId}": {"delete": {
+            "summary": "Callback for Information Job deletion",
+            "description": "The call is invoked to terminate a data subscription. The endpoint is provided by the Information Producer.",
+            "operationId": "jobDeletedCallback",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+            }},
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "infoJobId",
+                "required": true
+            }],
+            "tags": ["Data producer (callbacks)"]
+        }},
+        "/A1-EI/v1/eijobs/{eiJobId}/status": {"get": {
+            "summary": "EI job status",
+            "operationId": "getEiJobStatus_1",
+            "responses": {
+                "200": {
+                    "description": "EI job status",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EiJobStatusObject"}}}
+                },
+                "404": {
+                    "description": "Enrichment Information job is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "eiJobId",
+                "required": true
+            }],
+            "tags": ["A1-EI (registration)"]
+        }},
+        "/data-producer/v1/info-producers/{infoProducerId}/status": {"get": {
+            "summary": "Information producer status",
+            "operationId": "getInfoProducerStatus",
+            "responses": {
+                "200": {
+                    "description": "Information producer status",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_status"}}}
+                },
+                "404": {
+                    "description": "Information producer is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "infoProducerId",
+                "required": true
+            }],
+            "tags": ["Data producer (registration)"]
+        }},
+        "/data-consumer/v1/info-jobs/{infoJobId}/status": {"get": {
+            "summary": "Job status",
+            "operationId": "getEiJobStatus",
+            "responses": {
+                "200": {
+                    "description": "Information subscription job status",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_job_status"}}}
+                },
+                "404": {
+                    "description": "Information subscription job is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "infoJobId",
+                "required": true
+            }],
+            "tags": ["Data consumer"]
+        }},
+        "/actuator/metrics": {"get": {
+            "summary": "Actuator web endpoint 'metrics'",
+            "operationId": "metrics",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/example-dataconsumer/info-jobs/{infoJobId}/status": {"post": {
+            "summary": "Callback for changed Information Job status",
+            "requestBody": {
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EiJobStatusObject"}}},
+                "required": true
+            },
+            "description": "The primitive is implemented by the data consumer and is invoked when a Information Job status has been changed.",
+            "operationId": "jobStatusCallback",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+            }},
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "infoJobId",
+                "required": true
+            }],
+            "tags": ["A1-EI (callbacks)"]
+        }},
+        "/actuator/info": {"get": {
+            "summary": "Actuator web endpoint 'info'",
+            "operationId": "info",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/A1-EI/v1/eitypes": {"get": {
+            "summary": "EI type identifiers",
+            "operationId": "getEiTypeIdentifiers",
+            "responses": {"200": {
+                "description": "EI type identifiers",
+                "content": {"application/json": {"schema": {
+                    "type": "array",
+                    "items": {"type": "string"}
+                }}}
+            }},
+            "tags": ["A1-EI (registration)"]
+        }},
+        "/data-producer/v1/info-producers/{infoProducerId}": {
+            "get": {
+                "summary": "Individual Information Producer",
+                "operationId": "getInfoProducer",
+                "responses": {
+                    "200": {
+                        "description": "Information producer",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_registration_info"}}}
+                    },
+                    "404": {
+                        "description": "Information producer is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoProducerId",
+                    "required": true
+                }],
+                "tags": ["Data producer (registration)"]
+            },
+            "delete": {
+                "summary": "Individual Information Producer",
+                "operationId": "deleteInfoProducer",
+                "responses": {
+                    "200": {
+                        "description": "Not used",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "204": {
+                        "description": "Producer deleted",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "404": {
+                        "description": "Producer is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoProducerId",
+                    "required": true
+                }],
+                "tags": ["Data producer (registration)"]
+            },
+            "put": {
+                "summary": "Individual Information Producer",
+                "requestBody": {
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/producer_registration_info"}}},
+                    "required": true
+                },
+                "operationId": "putInfoProducer",
+                "responses": {
+                    "200": {
+                        "description": "Producer updated",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "201": {
+                        "description": "Producer created",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "400": {
+                        "description": "Input validation failed",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    },
+                    "404": {
+                        "description": "Producer type not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoProducerId",
+                    "required": true
+                }],
+                "tags": ["Data producer (registration)"]
+            }
+        },
+        "/status": {"get": {
+            "summary": "Returns status and statistics of this service",
+            "operationId": "getStatus",
+            "responses": {"200": {
+                "description": "Service is living",
+                "content": {"application/json": {"schema": {"$ref": "#/components/schemas/service_status_info"}}}
+            }},
+            "tags": ["Service status"]
+        }},
+        "/data-consumer/v1/info-type-subscription": {"get": {
+            "summary": "Information type subscription identifiers",
+            "description": "query for information type subscription identifiers",
+            "operationId": "getInfoTypeSubscriptions",
+            "responses": {"200": {
+                "description": "Information type subscription identifiers",
+                "content": {"application/json": {"schema": {
+                    "type": "array",
+                    "items": {"type": "string"}
+                }}}
+            }},
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "query",
+                "name": "owner",
+                "description": "selects result for one owner",
+                "required": false
+            }],
+            "tags": ["Data consumer"]
+        }},
+        "/A1-EI/v1/eijobs/{eiJobId}": {
+            "get": {
+                "summary": "Individual EI job",
+                "operationId": "getIndividualEiJob_1",
+                "responses": {
+                    "200": {
+                        "description": "EI job",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EiJobObject"}}}
+                    },
+                    "404": {
+                        "description": "Enrichment Information job is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "eiJobId",
+                    "required": true
+                }],
+                "tags": ["A1-EI (registration)"]
+            },
+            "delete": {
+                "summary": "Individual EI job",
+                "operationId": "deleteIndividualEiJob_1",
+                "responses": {
+                    "200": {
+                        "description": "Not used",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "204": {
+                        "description": "Job deleted",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "404": {
+                        "description": "Enrichment Information job is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "eiJobId",
+                    "required": true
+                }],
+                "tags": ["A1-EI (registration)"]
+            },
+            "put": {
+                "summary": "Individual EI job",
+                "requestBody": {
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/EiJobObject"}}},
+                    "required": true
+                },
+                "operationId": "putIndividualEiJob",
+                "responses": {
+                    "200": {
+                        "description": "Job updated",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "201": {
+                        "description": "Job created",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "400": {
+                        "description": "Input validation failed",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    },
+                    "404": {
+                        "description": "Enrichment Information type is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    },
+                    "409": {
+                        "description": "Cannot modify job type",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "eiJobId",
+                    "required": true
+                }],
+                "tags": ["A1-EI (registration)"]
+            }
+        },
+        "/actuator/logfile": {"get": {
+            "summary": "Actuator web endpoint 'logfile'",
+            "operationId": "logfile",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/data-consumer/v1/info-jobs/{infoJobId}": {
+            "get": {
+                "summary": "Individual data subscription job",
+                "operationId": "getIndividualEiJob",
+                "responses": {
+                    "200": {
+                        "description": "Information subscription job",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_job"}}}
+                    },
+                    "404": {
+                        "description": "Information subscription job is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoJobId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            },
+            "delete": {
+                "summary": "Individual data subscription job",
+                "operationId": "deleteIndividualEiJob",
+                "responses": {
+                    "200": {
+                        "description": "Not used",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "204": {
+                        "description": "Job deleted",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "404": {
+                        "description": "Information subscription job is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [{
+                    "schema": {"type": "string"},
+                    "in": "path",
+                    "name": "infoJobId",
+                    "required": true
+                }],
+                "tags": ["Data consumer"]
+            },
+            "put": {
+                "summary": "Individual data subscription job",
+                "requestBody": {
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_job"}}},
+                    "required": true
+                },
+                "description": "The job will be enabled when a producer is available",
+                "operationId": "putIndividualInfoJob",
+                "responses": {
+                    "200": {
+                        "description": "Job updated",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "201": {
+                        "description": "Job created",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Void"}}}
+                    },
+                    "400": {
+                        "description": "Input validation failed",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    },
+                    "404": {
+                        "description": "Information type is not found",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    },
+                    "409": {
+                        "description": "Cannot modify job type",
+                        "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                    }
+                },
+                "parameters": [
+                    {
+                        "schema": {"type": "string"},
+                        "in": "path",
+                        "name": "infoJobId",
+                        "required": true
+                    },
+                    {
+                        "schema": {
+                            "default": false,
+                            "type": "boolean"
+                        },
+                        "in": "query",
+                        "name": "typeCheck",
+                        "description": "when true, a validation of that the type exists and that the job matches the type schema.",
+                        "required": false
+                    }
+                ],
+                "tags": ["Data consumer"]
+            }
+        },
+        "/data-producer/v1/info-producers": {"get": {
+            "summary": "Information producer identifiers",
+            "operationId": "getInfoProducerIdentifiers",
+            "responses": {"200": {
+                "description": "Information producer identifiers",
+                "content": {"application/json": {"schema": {
+                    "type": "array",
+                    "items": {"type": "string"}
+                }}}
+            }},
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "query",
+                "name": "infoTypeId",
+                "description": "If given, only the producers for the EI Data type is returned.",
+                "required": false
+            }],
+            "tags": ["Data producer (registration)"]
+        }},
+        "/data-consumer/v1/info-types/{infoTypeId}": {"get": {
+            "summary": "Individual information type",
+            "operationId": "getInfoType_1",
+            "responses": {
+                "200": {
+                    "description": "Information type",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/consumer_information_type"}}}
+                },
+                "404": {
+                    "description": "Information type is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "infoTypeId",
+                "required": true
+            }],
+            "tags": ["Data consumer"]
+        }},
+        "/actuator/health": {"get": {
+            "summary": "Actuator web endpoint 'health'",
+            "operationId": "health",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }},
+        "/A1-EI/v1/eijobs": {"get": {
+            "summary": "EI job identifiers",
+            "description": "query for EI job identifiers",
+            "operationId": "getEiJobIds",
+            "responses": {
+                "200": {
+                    "description": "EI job identifiers",
+                    "content": {"application/json": {"schema": {
+                        "type": "array",
+                        "items": {"type": "string"}
+                    }}}
+                },
+                "404": {
+                    "description": "Enrichment Information type is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [
+                {
+                    "schema": {"type": "string"},
+                    "in": "query",
+                    "name": "eiTypeId",
+                    "description": "selects EI jobs of matching EI type",
+                    "required": false
+                },
+                {
+                    "schema": {"type": "string"},
+                    "in": "query",
+                    "name": "owner",
+                    "description": "selects EI jobs for one EI job owner",
+                    "required": false
+                }
+            ],
+            "tags": ["A1-EI (registration)"]
+        }},
+        "/data-producer/v1/info-producers/{infoProducerId}/info-jobs": {"get": {
+            "summary": "Information Job definitions",
+            "description": "Information Job definitions for one Information Producer",
+            "operationId": "getInfoProducerJobs",
+            "responses": {
+                "200": {
+                    "description": "Information producer",
+                    "content": {"application/json": {"schema": {
+                        "type": "array",
+                        "items": {"$ref": "#/components/schemas/producer_info_job_request"}
+                    }}}
+                },
+                "404": {
+                    "description": "Information producer is not found",
+                    "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}}
+                }
+            },
+            "parameters": [{
+                "schema": {"type": "string"},
+                "in": "path",
+                "name": "infoProducerId",
+                "required": true
+            }],
+            "tags": ["Data producer (registration)"]
+        }},
+        "/actuator/heapdump": {"get": {
+            "summary": "Actuator web endpoint 'heapdump'",
+            "operationId": "heapdump",
+            "responses": {"200": {
+                "description": "OK",
+                "content": {"*/*": {"schema": {"type": "object"}}}
+            }},
+            "tags": ["Actuator"]
+        }}
+    },
+    "info": {
+        "license": {
+            "name": "Copyright (C) 2020-2022 Nordix Foundation. Licensed under the Apache License.",
+            "url": "http://www.apache.org/licenses/LICENSE-2.0"
+        },
+        "description": "<h1>API documentation<\/h1><h2>General<\/h2><p>  The service is mainly a broker between data producers and data consumers. A data producer has the ability to produce one or several types of data (Information Type). One type of data can be produced by zero to many producers. <br /><br />A data consumer can have several active data subscriptions (Information Job). One Information Job consists of the type of data to produce and additional parameters for filtering of the data. These parameters are different for different data types.<\/p><h2>APIs provided by the service<\/h2><h4>A1-EI<\/h4><p>  This API is between Near-RT RIC and the Non-RT RIC.  The Near-RT RIC is a data consumer, which creates Information Jobs to subscribe for data.  In this context, the information is referred to as 'Enrichment Information', EI.<\/p><h4>Data producer API<\/h4><p>  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The API is for use by different kinds of data producers and provides support for:<ul><li>Registry of supported information types and which parameters needed to setup a subscription.<\/li><li>Registry of existing data producers.<\/li><li>Callback API provided by producers to setup subscriptions.<\/li><\/ul><\/p><h4>Data consumer API<\/h4><p>  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The API is for use by different kinds of data consumers and provides support for:<ul><li>Querying of available types of data to consume.<\/li><li>Management of data subscription jobs<\/li><li>Optional callback API provided by consumers to get notification on added and removed information types.<\/li><\/ul><\/p><h4>Service status<\/h4><p>  This API provides a means to monitor the health of this service.<\/p>",
+        "title": "Data management and exposure",
+        "version": "1.0"
+    },
+    "tags": [
+        {
+            "name": "A1-EI (registration)",
+            "description": "Data consumer EI job registration"
+        },
+        {
+            "name": "A1-EI (callbacks)",
+            "description": "Data consumer EI job status callbacks"
+        },
+        {
+            "name": "Data consumer (callbacks)",
+            "description": "API for data consumers"
+        },
+        {
+            "name": "Data producer (registration)",
+            "description": "API for data producers"
+        },
+        {
+            "name": "Data producer (callbacks)",
+            "description": "API implemented by data producers"
+        },
+        {
+            "name": "Data consumer",
+            "description": "API for data consumers"
+        },
+        {
+            "name": "Service status",
+            "description": "API for monitoring of the service"
+        },
+        {
+            "name": "Actuator",
+            "description": "Monitor and interact",
+            "externalDocs": {
+                "description": "Spring Boot Actuator Web API Documentation",
+                "url": "https://docs.spring.io/spring-boot/docs/current/actuator-api/html/"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/api/ics-api.yaml b/api/ics-api.yaml
new file mode 100644 (file)
index 0000000..be8fc94
--- /dev/null
@@ -0,0 +1,1486 @@
+openapi: 3.0.1
+info:
+  title: Data management and exposure
+  description: <h1>API documentation</h1><h2>General</h2><p>  The service is mainly
+    a broker between data producers and data consumers. A data producer has the ability
+    to produce one or several types of data (Information Type). One type of data can
+    be produced by zero to many producers. <br /><br />A data consumer can have several
+    active data subscriptions (Information Job). One Information Job consists of the
+    type of data to produce and additional parameters for filtering of the data. These
+    parameters are different for different data types.</p><h2>APIs provided by the
+    service</h2><h4>A1-EI</h4><p>  This API is between Near-RT RIC and the Non-RT
+    RIC.  The Near-RT RIC is a data consumer, which creates Information Jobs to subscribe
+    for data.  In this context, the information is referred to as 'Enrichment Information',
+    EI.</p><h4>Data producer API</h4><p>  This API is provided by the Non-RT RIC platform
+    and is intended to be part of the O-RAN R1 interface.  The API is for use by different
+    kinds of data producers and provides support for:<ul><li>Registry of supported
+    information types and which parameters needed to setup a subscription.</li><li>Registry
+    of existing data producers.</li><li>Callback API provided by producers to setup
+    subscriptions.</li></ul></p><h4>Data consumer API</h4><p>  This API is provided
+    by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface.  The
+    API is for use by different kinds of data consumers and provides support for:<ul><li>Querying
+    of available types of data to consume.</li><li>Management of data subscription
+    jobs</li><li>Optional callback API provided by consumers to get notification on
+    added and removed information types.</li></ul></p><h4>Service status</h4><p>  This
+    API provides a means to monitor the health of this service.</p>
+  license:
+    name: Copyright (C) 2020-2022 Nordix Foundation. Licensed under the Apache License.
+    url: http://www.apache.org/licenses/LICENSE-2.0
+  version: "1.0"
+servers:
+- url: /
+tags:
+- name: A1-EI (registration)
+  description: Data consumer EI job registration
+- name: A1-EI (callbacks)
+  description: Data consumer EI job status callbacks
+- name: Data consumer (callbacks)
+  description: API for data consumers
+- name: Data producer (registration)
+  description: API for data producers
+- name: Data producer (callbacks)
+  description: API implemented by data producers
+- name: Data consumer
+  description: API for data consumers
+- name: Service status
+  description: API for monitoring of the service
+- name: Actuator
+  description: Monitor and interact
+  externalDocs:
+    description: Spring Boot Actuator Web API Documentation
+    url: https://docs.spring.io/spring-boot/docs/current/actuator-api/html/
+paths:
+  /data-producer/v1/info-types:
+    get:
+      tags:
+      - Data producer (registration)
+      summary: Info Type identifiers
+      operationId: getInfoTypdentifiers
+      responses:
+        200:
+          description: Info Type identifiers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+  /actuator/threaddump:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'threaddump'
+      operationId: threaddump_2
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /A1-EI/v1/eitypes/{eiTypeId}:
+    get:
+      tags:
+      - A1-EI (registration)
+      summary: Individual EI type
+      operationId: getEiType
+      parameters:
+      - name: eiTypeId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: EI type
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EiTypeObject'
+        404:
+          description: Enrichment Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /data-producer/v1/info-types/{infoTypeId}:
+    get:
+      tags:
+      - Data producer (registration)
+      summary: Individual Information Type
+      operationId: getInfoType
+      parameters:
+      - name: infoTypeId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Info Type
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/producer_info_type_info'
+        404:
+          description: Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    put:
+      tags:
+      - Data producer (registration)
+      summary: Individual Information Type
+      operationId: putInfoType
+      parameters:
+      - name: infoTypeId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/producer_info_type_info'
+        required: true
+      responses:
+        200:
+          description: Type updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        201:
+          description: Type created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        400:
+          description: Input validation failed
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    delete:
+      tags:
+      - Data producer (registration)
+      summary: Individual Information Type
+      operationId: deleteInfoType
+      parameters:
+      - name: infoTypeId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Not used
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        204:
+          description: Producer deleted
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        404:
+          description: Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+        409:
+          description: The Information type has one or several active producers
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /data-consumer/v1/info-type-subscription/{subscriptionId}:
+    get:
+      tags:
+      - Data consumer
+      summary: Individual subscription for information types (registration/deregistration)
+      operationId: getIndividualTypeSubscription
+      parameters:
+      - name: subscriptionId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Type subscription
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/consumer_type_subscription_info'
+        404:
+          description: Subscription is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    put:
+      tags:
+      - Data consumer
+      summary: Individual subscription for information types (registration/deregistration)
+      description: This service operation is used to subscribe to notifications for
+        changes in the availability of data types.
+      operationId: putIndividualTypeSubscription
+      parameters:
+      - name: subscriptionId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/consumer_type_subscription_info'
+        required: true
+      responses:
+        200:
+          description: Subscription updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        201:
+          description: Subscription created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+    delete:
+      tags:
+      - Data consumer
+      summary: Individual subscription for information types (registration/deregistration)
+      operationId: deleteIndividualTypeSubscription
+      parameters:
+      - name: subscriptionId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Not used
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        204:
+          description: Subscription deleted
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        404:
+          description: Subscription is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /example-dataproducer/health-check:
+    get:
+      tags:
+      - Data producer (callbacks)
+      summary: Producer supervision
+      description: The endpoint is provided by the Information Producer and is used
+        for supervision of the producer.
+      operationId: producerSupervision
+      responses:
+        200:
+          description: The producer is OK
+          content:
+            application/json:
+              schema:
+                type: string
+  /actuator/loggers:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'loggers'
+      operationId: loggers
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /actuator/health/**:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'health-path'
+      operationId: health-path
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /data-consumer/v1/info-types:
+    get:
+      tags:
+      - Data consumer
+      summary: Information type identifiers
+      operationId: getinfoTypeIdentifiers
+      responses:
+        200:
+          description: Information type identifiers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+  /example-dataconsumer/info-type-status:
+    post:
+      tags:
+      - Data consumer (callbacks)
+      summary: Callback for changed Information type registration status
+      description: The primitive is implemented by the data consumer and is invoked
+        when a Information type status has been changed. <br/>Subscription are managed
+        by primitives in 'Data consumer'
+      operationId: typeStatusCallback
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/consumer_type_registration_info'
+        required: true
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+  /actuator/metrics/{requiredMetricName}:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'metrics-requiredMetricName'
+      operationId: metrics-requiredMetricName
+      parameters:
+      - name: requiredMetricName
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /actuator:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator root web endpoint
+      operationId: links
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+                additionalProperties:
+                  type: object
+                  additionalProperties:
+                    $ref: '#/components/schemas/Link'
+  /data-consumer/v1/info-jobs:
+    get:
+      tags:
+      - Data consumer
+      summary: Information Job identifiers
+      description: query for information job identifiers
+      operationId: getJobIds
+      parameters:
+      - name: infoTypeId
+        in: query
+        description: selects subscription jobs of matching information type
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: string
+      - name: owner
+        in: query
+        description: selects result for one owner
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information information job identifiers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+        404:
+          description: Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /actuator/loggers/{name}:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'loggers-name'
+      operationId: loggers-name_2
+      parameters:
+      - name: name
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+    post:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'loggers-name'
+      operationId: loggers-name
+      parameters:
+      - name: name
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /example-dataproducer/info-job:
+    post:
+      tags:
+      - Data producer (callbacks)
+      summary: Callback for Information Job creation/modification
+      description: The call is invoked to activate or to modify a data subscription.
+        The endpoint is provided by the Information Producer.
+      operationId: jobCreatedCallback
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/producer_info_job_request'
+        required: true
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+  /example-dataproducer/info-job/{infoJobId}:
+    delete:
+      tags:
+      - Data producer (callbacks)
+      summary: Callback for Information Job deletion
+      description: The call is invoked to terminate a data subscription. The endpoint
+        is provided by the Information Producer.
+      operationId: jobDeletedCallback
+      parameters:
+      - name: infoJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+  /A1-EI/v1/eijobs/{eiJobId}/status:
+    get:
+      tags:
+      - A1-EI (registration)
+      summary: EI job status
+      operationId: getEiJobStatus_1
+      parameters:
+      - name: eiJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: EI job status
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EiJobStatusObject'
+        404:
+          description: Enrichment Information job is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /data-producer/v1/info-producers/{infoProducerId}/status:
+    get:
+      tags:
+      - Data producer (registration)
+      summary: Information producer status
+      operationId: getInfoProducerStatus
+      parameters:
+      - name: infoProducerId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information producer status
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/producer_status'
+        404:
+          description: Information producer is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /data-consumer/v1/info-jobs/{infoJobId}/status:
+    get:
+      tags:
+      - Data consumer
+      summary: Job status
+      operationId: getEiJobStatus
+      parameters:
+      - name: infoJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information subscription job status
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/consumer_job_status'
+        404:
+          description: Information subscription job is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /actuator/metrics:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'metrics'
+      operationId: metrics
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /example-dataconsumer/info-jobs/{infoJobId}/status:
+    post:
+      tags:
+      - A1-EI (callbacks)
+      summary: Callback for changed Information Job status
+      description: The primitive is implemented by the data consumer and is invoked
+        when a Information Job status has been changed.
+      operationId: jobStatusCallback
+      parameters:
+      - name: infoJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EiJobStatusObject'
+        required: true
+      responses:
+        200:
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+  /actuator/info:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'info'
+      operationId: info
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /A1-EI/v1/eitypes:
+    get:
+      tags:
+      - A1-EI (registration)
+      summary: EI type identifiers
+      operationId: getEiTypeIdentifiers
+      responses:
+        200:
+          description: EI type identifiers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+  /data-producer/v1/info-producers/{infoProducerId}:
+    get:
+      tags:
+      - Data producer (registration)
+      summary: Individual Information Producer
+      operationId: getInfoProducer
+      parameters:
+      - name: infoProducerId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information producer
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/producer_registration_info'
+        404:
+          description: Information producer is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    put:
+      tags:
+      - Data producer (registration)
+      summary: Individual Information Producer
+      operationId: putInfoProducer
+      parameters:
+      - name: infoProducerId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/producer_registration_info'
+        required: true
+      responses:
+        200:
+          description: Producer updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        201:
+          description: Producer created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        400:
+          description: Input validation failed
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+        404:
+          description: Producer type not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    delete:
+      tags:
+      - Data producer (registration)
+      summary: Individual Information Producer
+      operationId: deleteInfoProducer
+      parameters:
+      - name: infoProducerId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Not used
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        204:
+          description: Producer deleted
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        404:
+          description: Producer is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /status:
+    get:
+      tags:
+      - Service status
+      summary: Returns status and statistics of this service
+      operationId: getStatus
+      responses:
+        200:
+          description: Service is living
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/service_status_info'
+  /data-consumer/v1/info-type-subscription:
+    get:
+      tags:
+      - Data consumer
+      summary: Information type subscription identifiers
+      description: query for information type subscription identifiers
+      operationId: getInfoTypeSubscriptions
+      parameters:
+      - name: owner
+        in: query
+        description: selects result for one owner
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information type subscription identifiers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+  /A1-EI/v1/eijobs/{eiJobId}:
+    get:
+      tags:
+      - A1-EI (registration)
+      summary: Individual EI job
+      operationId: getIndividualEiJob_1
+      parameters:
+      - name: eiJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: EI job
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/EiJobObject'
+        404:
+          description: Enrichment Information job is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    put:
+      tags:
+      - A1-EI (registration)
+      summary: Individual EI job
+      operationId: putIndividualEiJob
+      parameters:
+      - name: eiJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/EiJobObject'
+        required: true
+      responses:
+        200:
+          description: Job updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        201:
+          description: Job created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        400:
+          description: Input validation failed
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+        404:
+          description: Enrichment Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+        409:
+          description: Cannot modify job type
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    delete:
+      tags:
+      - A1-EI (registration)
+      summary: Individual EI job
+      operationId: deleteIndividualEiJob_1
+      parameters:
+      - name: eiJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Not used
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        204:
+          description: Job deleted
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        404:
+          description: Enrichment Information job is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /actuator/logfile:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'logfile'
+      operationId: logfile
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /data-consumer/v1/info-jobs/{infoJobId}:
+    get:
+      tags:
+      - Data consumer
+      summary: Individual data subscription job
+      operationId: getIndividualEiJob
+      parameters:
+      - name: infoJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information subscription job
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/consumer_job'
+        404:
+          description: Information subscription job is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    put:
+      tags:
+      - Data consumer
+      summary: Individual data subscription job
+      description: The job will be enabled when a producer is available
+      operationId: putIndividualInfoJob
+      parameters:
+      - name: infoJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      - name: typeCheck
+        in: query
+        description: when true, a validation of that the type exists and that the
+          job matches the type schema.
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: boolean
+          default: false
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/consumer_job'
+        required: true
+      responses:
+        200:
+          description: Job updated
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        201:
+          description: Job created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        400:
+          description: Input validation failed
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+        404:
+          description: Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+        409:
+          description: Cannot modify job type
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+    delete:
+      tags:
+      - Data consumer
+      summary: Individual data subscription job
+      operationId: deleteIndividualEiJob
+      parameters:
+      - name: infoJobId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Not used
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        204:
+          description: Job deleted
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Void'
+        404:
+          description: Information subscription job is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /data-producer/v1/info-producers:
+    get:
+      tags:
+      - Data producer (registration)
+      summary: Information producer identifiers
+      operationId: getInfoProducerIdentifiers
+      parameters:
+      - name: infoTypeId
+        in: query
+        description: If given, only the producers for the EI Data type is returned.
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information producer identifiers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+  /data-consumer/v1/info-types/{infoTypeId}:
+    get:
+      tags:
+      - Data consumer
+      summary: Individual information type
+      operationId: getInfoType_1
+      parameters:
+      - name: infoTypeId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information type
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/consumer_information_type'
+        404:
+          description: Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /actuator/health:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'health'
+      operationId: health
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+  /A1-EI/v1/eijobs:
+    get:
+      tags:
+      - A1-EI (registration)
+      summary: EI job identifiers
+      description: query for EI job identifiers
+      operationId: getEiJobIds
+      parameters:
+      - name: eiTypeId
+        in: query
+        description: selects EI jobs of matching EI type
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: string
+      - name: owner
+        in: query
+        description: selects EI jobs for one EI job owner
+        required: false
+        style: form
+        explode: true
+        schema:
+          type: string
+      responses:
+        200:
+          description: EI job identifiers
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  type: string
+        404:
+          description: Enrichment Information type is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /data-producer/v1/info-producers/{infoProducerId}/info-jobs:
+    get:
+      tags:
+      - Data producer (registration)
+      summary: Information Job definitions
+      description: Information Job definitions for one Information Producer
+      operationId: getInfoProducerJobs
+      parameters:
+      - name: infoProducerId
+        in: path
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: Information producer
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/producer_info_job_request'
+        404:
+          description: Information producer is not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ProblemDetails'
+  /actuator/heapdump:
+    get:
+      tags:
+      - Actuator
+      summary: Actuator web endpoint 'heapdump'
+      operationId: heapdump
+      responses:
+        200:
+          description: OK
+          content:
+            '*/*':
+              schema:
+                type: object
+components:
+  schemas:
+    consumer_information_type:
+      required:
+      - job_data_schema
+      - no_of_producers
+      - type_status
+      type: object
+      properties:
+        no_of_producers:
+          type: integer
+          description: The number of registered producers for the type
+          format: int32
+        type_status:
+          type: string
+          description: 'Allowed values: <br/>ENABLED: one or several producers for
+            the information type are available <br/>DISABLED: no producers for the
+            information type are available'
+          enum:
+          - ENABLED
+          - DISABLED
+        job_data_schema:
+          type: object
+          description: Json schema for the job data
+      description: Information for an Information type
+    EiTypeObject:
+      type: object
+      description: Information for an EI type
+    service_status_info:
+      required:
+      - no_of_jobs
+      - no_of_producers
+      - no_of_types
+      - status
+      type: object
+      properties:
+        no_of_producers:
+          type: integer
+          description: Number of Information Producers
+          format: int32
+        no_of_types:
+          type: integer
+          description: Number of Information Types
+          format: int32
+        no_of_jobs:
+          type: integer
+          description: Number of Information Jobs
+          format: int32
+        status:
+          type: string
+          description: status text
+    producer_registration_info:
+      required:
+      - info_job_callback_url
+      - info_producer_supervision_callback_url
+      - supported_info_types
+      type: object
+      properties:
+        info_producer_supervision_callback_url:
+          type: string
+          description: callback for producer supervision
+        supported_info_types:
+          type: array
+          description: Supported Information Type IDs
+          items:
+            type: string
+            description: Supported Information Type IDs
+        info_job_callback_url:
+          type: string
+          description: callback for Information Job
+      description: Information for an Information Producer
+    consumer_type_registration_info:
+      required:
+      - info_type_id
+      - job_data_schema
+      - status
+      type: object
+      properties:
+        info_type_id:
+          type: string
+          description: Information type identifier
+        job_data_schema:
+          type: object
+          description: Json schema for the job data
+        status:
+          type: string
+          description: 'Allowed values: <br/>REGISTERED: the information type has
+            been registered <br/>DEREGISTERED: the information type has been removed'
+          enum:
+          - REGISTERED
+          - DEREGISTERED
+      description: Information for an Information type
+    ProblemDetails:
+      type: object
+      properties:
+        detail:
+          type: string
+          description: A human-readable explanation specific to this occurrence of
+            the problem.
+          example: Information Job type 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: A problem detail to carry details in a HTTP response according
+        to RFC 7807
+    EiJobStatusObject:
+      required:
+      - eiJobStatus
+      type: object
+      properties:
+        eiJobStatus:
+          type: string
+          description: 'Allowed values for EI job status: <br/>ENABLED: the A1-EI
+            producer is able to deliver EI result for the EI job <br/>DISABLED: the
+            A1-EI producer is unable to deliver EI result for the EI job'
+          enum:
+          - ENABLED
+          - DISABLED
+      description: Status for an EI job
+    consumer_job_status:
+      required:
+      - info_job_status
+      - producers
+      type: object
+      properties:
+        info_job_status:
+          type: string
+          description: 'Allowed values: <br/>ENABLED: the A1-Information producer
+            is able to deliver result for the Information Job <br/>DISABLED: the A1-Information
+            producer is unable to deliver result for the Information Job'
+          enum:
+          - ENABLED
+          - DISABLED
+        producers:
+          type: array
+          description: An array of all registered Information Producer Identifiers.
+          items:
+            type: string
+            description: An array of all registered Information Producer Identifiers.
+      description: Status for an Information Job
+    EiJobObject:
+      required:
+      - eiTypeId
+      - jobDefinition
+      - jobOwner
+      - jobResultUri
+      type: object
+      properties:
+        eiTypeId:
+          type: string
+          description: EI type Idenitifier of the EI job
+        jobResultUri:
+          type: string
+          description: The target URI of the EI data
+        jobOwner:
+          type: string
+          description: Identity of the owner of the job
+        statusNotificationUri:
+          type: string
+          description: The target of EI job status notifications
+        jobDefinition:
+          type: object
+          description: EI type specific job data
+      description: Information for an Enrichment Information Job
+    producer_info_type_info:
+      required:
+      - info_job_data_schema
+      type: object
+      properties:
+        info_type_information:
+          type: object
+          description: Type specific information for the information type
+        info_job_data_schema:
+          type: object
+          description: Json schema for the job data
+      description: Information for an Information Type
+    producer_info_job_request:
+      required:
+      - info_job_identity
+      type: object
+      properties:
+        owner:
+          type: string
+          description: The owner of the job
+        last_updated:
+          type: string
+          description: The time when the job was last updated or created (ISO-8601)
+        info_job_identity:
+          type: string
+          description: Identity of the Information Job
+        target_uri:
+          type: string
+          description: URI for the target of the produced Information
+        info_job_data:
+          type: object
+          description: Json for the job data
+        info_type_identity:
+          type: string
+          description: Type identity for the job
+      description: The body of the Information Producer callbacks for Information
+        Job creation and deletion
+    consumer_job:
+      required:
+      - info_type_id
+      - job_definition
+      - job_owner
+      - job_result_uri
+      type: object
+      properties:
+        info_type_id:
+          type: string
+          description: Information type Idenitifier of the subscription job
+        job_result_uri:
+          type: string
+          description: The target URI of the subscribed information
+        job_owner:
+          type: string
+          description: Identity of the owner of the job
+        job_definition:
+          type: object
+          description: Information type specific job data
+        status_notification_uri:
+          type: string
+          description: The target of Information subscription job status notifications
+      description: Information for an Information Job
+    producer_status:
+      required:
+      - operational_state
+      type: object
+      properties:
+        operational_state:
+          type: string
+          description: Represents the operational states
+          enum:
+          - ENABLED
+          - DISABLED
+      description: Status for an Info Producer
+    Void:
+      type: object
+      description: 'Void/empty '
+    Link:
+      type: object
+      properties:
+        templated:
+          type: boolean
+        href:
+          type: string
+    consumer_type_subscription_info:
+      required:
+      - owner
+      - status_result_uri
+      type: object
+      properties:
+        owner:
+          type: string
+          description: Identity of the owner of the subscription
+        status_result_uri:
+          type: string
+          description: The target URI of the subscribed information
+      description: Information for an information type subscription
diff --git a/config/README b/config/README
new file mode 100644 (file)
index 0000000..140927f
--- /dev/null
@@ -0,0 +1,41 @@
+The keystore.jks and truststore.jks files are created by using the following commands (note that this is an example):
+
+1) Create a CA certificate and a private key:
+
+openssl genrsa -des3 -out CA-key.pem 2048
+openssl req -new -key CA-key.pem -x509 -days 1000 -out CA-cert.pem
+
+2) Create a keystore with a private key entry that is signed by the CA:
+
+keytool -genkeypair -alias policy_agent -keyalg RSA -keysize 2048 -keystore keystore.jks -validity 3650 -storepass policy_agent
+keytool -certreq -alias policy_agent -file request.csr -keystore keystore.jks -ext san=dns:your.domain.com -storepass policy_agent
+openssl x509 -req -days 365 -in request.csr -CA CA-cert.pem -CAkey CA-key.pem -CAcreateserial -out ca_signed-cert.pem
+keytool -importcert -alias ca_cert -file CA-cert.pem -keystore keystore.jks -trustcacerts -storepass policy_agent
+keytool -importcert -alias policy_agent -file ca_signed-cert.pem -keystore keystore.jks -trustcacerts -storepass policy_agent
+
+
+3) Create a trust store containing the CA cert (to trust all certs signed by the CA):
+
+keytool -genkeypair -alias not_used -keyalg RSA -keysize 2048 -keystore truststore.jks -validity 3650 -storepass policy_agent
+keytool -importcert -alias ca_cert -file CA-cert.pem -keystore truststore.jks -trustcacerts -storepass policy_agent
+
+
+4) Command for listing of the contents of jks files, examples:
+keytool -list -v -keystore keystore.jks -storepass policy_agent
+keytool -list -v -keystore truststore.jks -storepass policy_agent
+
+## 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/config/application.yaml b/config/application.yaml
new file mode 100644 (file)
index 0000000..372b61c
--- /dev/null
@@ -0,0 +1,52 @@
+spring:
+  profiles:
+    active: prod
+  main:
+    allow-bean-definition-overriding: true
+  aop:
+    auto: false
+springdoc:
+  show-actuator: true
+management:
+  endpoints:
+    web:
+      exposure:
+        # Enabling of springboot actuator features. See springboot documentation.
+        include: "loggers,logfile,health,info,metrics,threaddump,heapdump"
+
+logging:
+  # Configuration of logging
+  level:
+    ROOT: ERROR
+    org.springframework: ERROR
+    org.springframework.data: ERROR
+    org.springframework.web.reactive.function.client.ExchangeFunctions: ERROR
+    org.oransc.ics: INFO
+  file:
+    name: /var/log/information-coordinator-service/application.log
+server:
+   # Configuration of the HTTP/REST server. The parameters are defined and handeled by the springboot framework.
+   # See springboot documentation.
+   port : 8434
+   http-port: 8083
+   ssl:
+      key-store-type: JKS
+      key-store-password: policy_agent
+      key-store: /opt/app/information-coordinator-service/etc/cert/keystore.jks
+      key-password: policy_agent
+      key-alias: policy_agent
+app:
+  webclient:
+    # Configuration of the trust store used for the HTTP client (outgoing requests)
+    # The file location and the password for the truststore is only relevant if trust-store-used == true
+    # Note that the same keystore as for the server is used.
+    trust-store-used: false
+    trust-store-password: policy_agent
+    trust-store: /opt/app/information-coordinator-service/etc/cert/truststore.jks
+    # Configuration of usage of HTTP Proxy for the southbound accesses.
+    # The HTTP proxy (if configured) will only be used for accessing NearRT RIC:s
+    http.proxy-host:
+    http.proxy-port: 0
+  vardata-directory: /var/information-coordinator-service
+  # If the file name is empty, no authorzation token is sent
+  auth-token-file:
\ No newline at end of file
diff --git a/config/keystore.jks b/config/keystore.jks
new file mode 100644 (file)
index 0000000..122997a
Binary files /dev/null and b/config/keystore.jks differ
diff --git a/config/truststore.jks b/config/truststore.jks
new file mode 100644 (file)
index 0000000..60d6288
Binary files /dev/null and b/config/truststore.jks differ
diff --git a/docs/_static/logo.png b/docs/_static/logo.png
new file mode 100644 (file)
index 0000000..c3b6ce5
Binary files /dev/null and b/docs/_static/logo.png differ
diff --git a/docs/api-docs.rst b/docs/api-docs.rst
new file mode 100644 (file)
index 0000000..fee0f20
--- /dev/null
@@ -0,0 +1,31 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2021 Nordix
+
+.. _api_docs:
+
+.. |swagger-icon| image:: ./images/swagger.png
+                  :width: 40px
+
+.. |yaml-icon| image:: ./images/yaml_logo.png
+                  :width: 40px
+
+
+========
+API-Docs
+========
+
+Here we describe the APIs to access the Non-RT RIC Information Coordination Service.
+
+Information Coordinator Service
+===============================
+
+See `A1 Information Information Coordination Service API <./ics-api.html>`_ for full details of the API.
+
+The API is also described in Swagger-JSON and YAML:
+
+.. csv-table::
+   :header: "API name", "|swagger-icon|", "|yaml-icon|"
+   :widths: 10,5,5
+
+   "A1 Information Coordination Service API", ":download:`link <../api/ics-api.json>`", ":download:`link <../api/ics-api.yaml>`"
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..496a7a3
--- /dev/null
@@ -0,0 +1,30 @@
+from docs_conf.conf import *
+
+#branch configuration
+
+branch = 'latest'
+
+linkcheck_ignore = [
+    'http://localhost.*',
+    'http://127.0.0.1.*',
+    'https://gerrit.o-ran-sc.org.*',
+    './ics-api.html', #Generated file that doesn't exist at link check.
+]
+
+extensions = ['sphinxcontrib.redoc', 'sphinx.ext.intersphinx',]
+
+redoc = [
+            {
+                'name': 'ICS API',
+                'page': 'ics-api',
+                'spec': '../api/ics-api.json',
+                'embed': True,
+            }
+        ]
+
+redoc_uri = 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js'
+
+#intershpinx mapping with other projects
+intersphinx_mapping = {}
+
+intersphinx_mapping['nonrtric-controlpanel'] = ('https://docs.o-ran-sc.org/projects/o-ran-sc-portal-nonrtric-controlpanel/en/%s' % branch, None)
diff --git a/docs/conf.yaml b/docs/conf.yaml
new file mode 100644 (file)
index 0000000..0e8d0f9
--- /dev/null
@@ -0,0 +1,3 @@
+---
+project_cfg: oran
+project: nonrtric-plt-informationcoordinatorservice
diff --git a/docs/developer-guide.rst b/docs/developer-guide.rst
new file mode 100644 (file)
index 0000000..d4f4cf2
--- /dev/null
@@ -0,0 +1,65 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2021 Nordix
+
+Developer Guide
+===============
+
+This document provides a quickstart for developers of the Non-RT RIC Information Coordinator Service.
+
+Additional developer guides are available on the `O-RAN SC NONRTRIC Developer wiki <https://wiki.o-ran-sc.org/display/RICNR/Release+E>`_.
+
+Information Coordinator Service
+-------------------------------
+The Information Coordinator Service is a Java 11 web application built using the Spring Framework. Using Spring Boot
+dependencies, it runs as a standalone application.
+
+Its main functionality is to act as a data subscription broker and to decouple data producer from data consumers.
+
+See the ./config/README file in the *information-coordinator-service* directory Gerrit repo on how to create and setup
+the certificates and private keys needed for HTTPS.
+
+Start standalone
+++++++++++++++++
+
+The project uses Maven. To start the Information Coordinator Service as a freestanding application, run the following
+command in the *information-coordinator-service* directory:
+
+    +-----------------------------+
+    | mvn spring-boot:run         |
+    +-----------------------------+
+
+There are a few files that needs to be available to run. These are referred to from the application.yaml file.
+The following properties have to be modified:
+
+* server.ssl.key-store=./config/keystore.jks
+* app.webclient.trust-store=./config/truststore.jks
+* app.vardata-directory=./target
+
+Start in Docker
++++++++++++++++
+
+To build and deploy the Information Coordinator Service, go to the "information-coordinator-service" folder and run the
+following command:
+
+    +-----------------------------+
+    | mvn clean install           |
+    +-----------------------------+
+
+Then start the container by running the following command:
+
+    +--------------------------------------------------------------------+
+    | docker run nonrtric-information-coordinator-service                |
+    +--------------------------------------------------------------------+
+
+Kubernetes deployment
+---------------------
+
+Non-RT RIC can be also deployed in a Kubernetes cluster, `it/dep repository <https://gerrit.o-ran-sc.org/r/admin/repos/it/dep>`_.
+hosts deployment and integration artifacts. Instructions and helm charts to deploy the Non-RT-RIC functions in the
+OSC NONRTRIC integrated test environment can be found in the *./nonrtric* directory.
+
+For more information on installation of NonRT-RIC in Kubernetes, see `Deploy NONRTRIC in Kubernetes <https://wiki.o-ran-sc.org/display/RICNR/Deploy+NONRTRIC+in+Kubernetes>`_.
+
+For more information see `Integration and Testing documentation on the O-RAN-SC wiki <https://docs.o-ran-sc.org/projects/o-ran-sc-it-dep/en/latest/index.html>`_.
+
diff --git a/docs/favicon.ico b/docs/favicon.ico
new file mode 100644 (file)
index 0000000..00b0fd0
Binary files /dev/null and b/docs/favicon.ico differ
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
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..0d33cd5
--- /dev/null
@@ -0,0 +1,18 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2021 Nordix
+
+Non-RT RIC Information Coordination Service
+===========================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   ./overview.rst
+   ./developer-guide.rst
+   ./api-docs.rst
+   ./installation-guide.rst
+   ./release-notes.rst
+
+* :ref:`search`
diff --git a/docs/installation-guide.rst b/docs/installation-guide.rst
new file mode 100644 (file)
index 0000000..1837152
--- /dev/null
@@ -0,0 +1,41 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2021 Nordix
+
+Installation Guide
+==================
+
+Abstract
+--------
+
+This document describes how to install the Non-RT RIC components, their dependencies and required system resources.
+
+Software Installation and Deployment
+------------------------------------
+
+Install with Docker
++++++++++++++++++++
+
+Docker compose files are provided, in the "docker-compose" folder, to install the components. Run the following
+command to start the components:
+
+      .. code-block:: bash
+
+         docker-compose -f docker-compose.yaml
+           -f policy-service/docker-compose.yaml
+           -f ics/docker-compose.yaml
+
+The example above is just an example to start some of the components.
+For more information on running and configuring the functions can be found in the README file in the "`docker-compose <https://gerrit.o-ran-sc.org/r/gitweb?p=nonrtric.git;a=tree;f=docker-compose>`__" folder, and on the `wiki page <https://wiki.o-ran-sc.org/display/RICNR/Release+E+-+Run+in+Docker>`_
+
+Install with Helm
++++++++++++++++++
+
+Helm charts and an example recipe are provided in the `it/dep repo <https://gerrit.o-ran-sc.org/r/admin/repos/it/dep>`_,
+under "nonrtric". By modifying the variables named "installXXX" in the beginning of the example recipe file, which
+components that will be installed can be controlled. Then the components can be installed and started by running the
+following command:
+
+      .. code-block:: bash
+
+        bin/deploy-nonrtric -f nonrtric/RECIPE_EXAMPLE/example_recipe.yaml
diff --git a/docs/overview.rst b/docs/overview.rst
new file mode 100644 (file)
index 0000000..a2c15b1
--- /dev/null
@@ -0,0 +1,26 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2021 Nordix
+
+Information Coordination Service
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Coordinate/Register Information Types, Producers, Consumers, and Jobs.
+
+Coordinate/Register A1-EI Types, Producers, Consumers, and Jobs (A1 Enrichment Information Job Coordination).
+
+* Maintains a registry of:
+
+  + Information Types / schemas
+  + Information Producers
+  + Information Consumers
+  + Information Jobs
+
+* Information Query API (e.g. per producer, per consumer, per types).
+* Query status of Information jobs.
+* After Information-type/Producer/Consumer/Job is successfully registered delivery/flow can happen directly between Information Producers and Information Consumers.
+* The Information Coordinator Service natively supports the O-RAN A1 Enrichment Information (A1-EI) interface, supporting coordination A1-EI Jobs where information (A1-EI)flow from the SMO/Non-RT-RIC/rApps to near-RT-RICs over the A1 interface.
+
+Implementation:
+
+* Implemented as a Java Spring Boot application.
diff --git a/docs/release-notes.rst b/docs/release-notes.rst
new file mode 100644 (file)
index 0000000..7d02112
--- /dev/null
@@ -0,0 +1,133 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2021 Nordix
+
+=============
+Release-Notes
+=============
+
+
+This document provides the release notes for the release of the Non-RT RIC Information Coordination Service.
+
+
+Version history Information Coordinator Service
+===============================================
+
++------------+----------+------------------+--------------------+
+| **Date**   | **Ver.** | **Author**       | **Comment**        |
+|            |          |                  |                    |
++------------+----------+------------------+--------------------+
+| 2020-12-03 | 1.0.0    | Henrik Andersson | Cherry Release     |
+|            |          |                  |                    |
++------------+----------+------------------+--------------------+
+| 2021-06-23 | 1.1.0    | Henrik Andersson | D Release          |
+|            |          |                  |                    |
++------------+----------+------------------+--------------------+
+| 2021-12-13 | 1.2.0    | Henrik Andersson | E Release          |
+|            |          |                  | Renamed and minor  |
+|            |          |                  | improvements       |
++------------+----------+------------------+--------------------+
+| 2022-02-07 | 1.2.1    | Henrik Andersson | E Maintenance      |
+|            |          |                  | Release            |
+|            |          |                  | Non root user in   |
+|            |          |                  | Docker             |
++------------+----------+------------------+--------------------+
+
+
+Release Data
+============
+
+Cherry
+------
++-----------------------------+---------------------------------------------------+
+| **Project**                 | Non-RT RIC                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Repo/commit-ID**          | nonrtric/90ce16238dd6970153e1c0fbddb15e32c68c504f |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release designation**     | Cherry                                            |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release date**            | 2020-12-03                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Purpose of the delivery** | Introduction of Enrichment Service Coordinator    |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+
+D
+-
++-----------------------------+---------------------------------------------------+
+| **Project**                 | Non-RT RIC                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Repo/commit-ID**          | nonrtric/dd3ebfd784e96919a00ddd745826f8a8e074c66f |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release designation**     | D                                                 |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release date**            | 2021-06-23                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Purpose of the delivery** | Improvements                                      |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+
+D Maintenance
+-------------
++-----------------------------+---------------------------------------------------+
+| **Project**                 | Non-RT RIC                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Repo/commit-ID**          | nonrtric/973ae56894fb29a929fba9e344cae42e7607087b |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release designation**     | D                                                 |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release date**            | 2021-08-10                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Purpose of the delivery** | Minor bug fixes                                   |
++-----------------------------+---------------------------------------------------+
+
+E Release
+---------
++-----------------------------+---------------------------------------------------+
+| **Project**                 | Non-RT RIC                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Repo/commit-ID**          | nonrtric/b472c167413a55a42fc7bfa08d2138f967a204fb |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release designation**     | E                                                 |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release date**            | 2021-12-13                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Purpose of the delivery** | Improvements and renaming.                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+
+E Maintenance Release
+---------------------
++-----------------------------+---------------------------------------------------+
+| **Project**                 | Non-RT RIC                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Repo/commit-ID**          | nonrtric/4df1f9ca4cd1ebc21e0c5ea57bcb0b7ef096d067 |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release designation**     | E                                                 |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Release date**            | 2022-02-09                                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+| **Purpose of the delivery** | Improvements and bug fixes                        |
+|                             |                                                   |
++-----------------------------+---------------------------------------------------+
+
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
new file mode 100644 (file)
index 0000000..692a79f
--- /dev/null
@@ -0,0 +1,12 @@
+tox
+Sphinx
+doc8
+docutils
+setuptools
+six
+sphinx_rtd_theme
+sphinxcontrib-needs
+sphinxcontrib-swaggerdoc
+sphinx_bootstrap_theme
+sphinxcontrib-redoc
+lfdocs-conf
\ No newline at end of file
diff --git a/eclipse-formatter.xml b/eclipse-formatter.xml
new file mode 100644 (file)
index 0000000..c8cca2e
--- /dev/null
@@ -0,0 +1,314 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+  -->
+<profiles version="13">
+<profile kind="CodeFormatterProfile" name="java-formatter" version="12">
+<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.8"/>
+<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.8"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.compiler.source" value="1.8"/>
+<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="48"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="120"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
+<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
+<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="120"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
+</profile>
+</profiles>
diff --git a/pom.xml b/pom.xml
new file mode 100644 (file)
index 0000000..ce3abf3
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,354 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+* ========================LICENSE_START=================================
+* O-RAN-SC
+* %%
+* Copyright (C) 2019 Nordix Foundation
+* %%
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+* ========================LICENSE_END===================================
+-->
+<project
+    xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.6.6</version>
+        <relativePath />
+    </parent>
+    <groupId>org.o-ran-sc.nonrtric</groupId>
+    <artifactId>nonrtric-plt-informationcoordinatorservice</artifactId>
+    <version>1.3.0-SNAPSHOT</version>
+    <licenses>
+        <license>
+            <name>The Apache Software License, Version 2.0</name>
+            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+        </license>
+    </licenses>
+    <repositories>
+        <repository>
+            <id>onap-releases</id>
+            <name>onap-releases</name>
+            <url>https://nexus.onap.org/content/repositories/releases/</url>
+        </repository>
+    </repositories>
+    <properties>
+        <java.version>11</java.version>
+        <springfox.version>3.0.0</springfox.version>
+        <immutable.version>2.8.2</immutable.version>
+        <swagger.version>2.1.13</swagger.version>
+        <json.version>20211205</json.version>
+        <maven-compiler-plugin.version>3.8.0</maven-compiler-plugin.version>
+        <formatter-maven-plugin.version>2.12.2</formatter-maven-plugin.version>
+        <spotless-maven-plugin.version>1.24.3</spotless-maven-plugin.version>
+        <swagger-codegen-maven-plugin.version>3.0.11</swagger-codegen-maven-plugin.version>
+        <docker-maven-plugin>0.30.0</docker-maven-plugin>
+        <sonar-maven-plugin.version>3.7.0.1746</sonar-maven-plugin.version>
+        <jacoco-maven-plugin.version>0.8.5</jacoco-maven-plugin.version>
+        <exec.skip>true</exec.skip>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.6.6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webflux</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-jaxrs2</artifactId>
+            <version>${swagger.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-jaxrs2-servlet-initializer</artifactId>
+            <version>${swagger.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.immutables</groupId>
+            <artifactId>value</artifactId>
+            <version>${immutable.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.immutables</groupId>
+            <artifactId>gson</artifactId>
+            <version>${immutable.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>${json.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.github.erosb/everit-json-schema -->
+        <dependency>
+            <groupId>com.github.erosb</groupId>
+            <artifactId>everit-json-schema</artifactId>
+            <version>1.12.1</version>
+        </dependency>
+        <!-- Actuator dependencies -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <!--REQUIRED TO GENERATE DOCUMENTATION -->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <version>${springfox.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>${springfox.version}</version>
+        </dependency>
+        <!-- For development help -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <!-- TEST -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>mockwebserver</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>net.revelc.code.formatter</groupId>
+                <artifactId>formatter-maven-plugin</artifactId>
+                <version>${formatter-maven-plugin.version}</version>
+                <configuration>
+                    <configFile>${project.basedir}/eclipse-formatter.xml</configFile>
+                </configuration>
+                <!-- https://code.revelc.net/formatter-maven-plugin/ use mvn formatter:format
+                                       spotless:apply process-sources -->
+            </plugin>
+            <plugin>
+                <groupId>com.diffplug.spotless</groupId>
+                <artifactId>spotless-maven-plugin</artifactId>
+                <version>${spotless-maven-plugin.version}</version>
+                <configuration>
+                    <java>
+                        <removeUnusedImports />
+                        <importOrder>
+                            <order>com,java,org</order>
+                        </importOrder>
+                    </java>
+                </configuration>
+                <!-- https://github.com/diffplug/spotless/tree/master/plugin-maven use
+                                       mvn spotless:apply to rewrite source files use mvn spotless:check to validate
+                                       source files -->
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <skipTests>false</skipTests>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-failsafe-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>add-source</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>add-source</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>${project.build.directory}/generated-sources/annotations/</source>
+                            </sources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>${jacoco-maven-plugin.version}</version>
+                <executions>
+                    <execution>
+                        <id>default-prepare-agent</id>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>default-report</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>io.swagger.codegen.v3</groupId>
+                <artifactId>swagger-codegen-maven-plugin</artifactId>
+                <version>${swagger-codegen-maven-plugin.version}</version>
+                <executions>
+                    <execution>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>generate</goal>
+                        </goals>
+                        <configuration>
+                            <inputSpec>${project.basedir}/api/ics-api.json</inputSpec>
+                            <language>openapi-yaml</language>
+                            <output>${project.basedir}/api</output>
+                            <configOptions>
+                                <outputFile>ics-api.yaml</outputFile>
+                            </configOptions>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>io.fabric8</groupId>
+                <artifactId>docker-maven-plugin</artifactId>
+                <version>${docker-maven-plugin}</version>
+                <inherited>false</inherited>
+                <executions>
+                    <execution>
+                        <id>generate-nonrtric-plt-informationcoordinatorservice-image</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>build</goal>
+                        </goals>
+                        <configuration>
+                            <pullRegistry>${env.CONTAINER_PULL_REGISTRY}</pullRegistry>
+                            <images>
+                                <image>
+                                    <name>o-ran-sc/nonrtric-plt-informationcoordinatorservice:${project.version}</name>
+                                    <build>
+                                        <cleanup>try</cleanup>
+                                        <contextDir>${basedir}</contextDir>
+                                        <dockerFile>Dockerfile</dockerFile>
+                                        <args>
+                                            <JAR>${project.build.finalName}.jar</JAR>
+                                        </args>
+                                        <tags>
+                                            <tag>${project.version}</tag>
+                                        </tags>
+                                    </build>
+                                </image>
+                            </images>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>push-nonrtric-plt-informationcoordinatorservice-image</id>
+                        <goals>
+                            <goal>build</goal>
+                            <goal>push</goal>
+                        </goals>
+                        <configuration>
+                            <pullRegistry>${env.CONTAINER_PULL_REGISTRY}</pullRegistry>
+                            <pushRegistry>${env.CONTAINER_PUSH_REGISTRY}</pushRegistry>
+                            <images>
+                                <image>
+                                    <name>o-ran-sc/nonrtric-plt-informationcoordinatorservice:${project.version}</name>
+                                    <build>
+                                        <contextDir>${basedir}</contextDir>
+                                        <dockerFile>Dockerfile</dockerFile>
+                                        <args>
+                                            <JAR>${project.build.finalName}.jar</JAR>
+                                        </args>
+                                        <tags>
+                                            <tag>${project.version}</tag>
+                                            <tag>latest</tag>
+                                        </tags>
+                                    </build>
+                                </image>
+                            </images>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+         </plugins>
+    </build>
+    <issueManagement>
+        <system>JIRA</system>
+        <url>https://jira.o-ran-sc.org/</url>
+    </issueManagement>
+</project>
diff --git a/src/main/java/org/oransc/ics/Application.java b/src/main/java/org/oransc/ics/Application.java
new file mode 100644 (file)
index 0000000..46caac1
--- /dev/null
@@ -0,0 +1,33 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+    public static void main(String[] args) {
+        SpringApplication.run(Application.class);
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/BeanFactory.java b/src/main/java/org/oransc/ics/BeanFactory.java
new file mode 100644 (file)
index 0000000..3847cc3
--- /dev/null
@@ -0,0 +1,115 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.lang.invoke.MethodHandles;
+
+import org.apache.catalina.connector.Connector;
+import org.oransc.ics.clients.SecurityContext;
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoTypes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class BeanFactory {
+
+    @Value("${server.http-port}")
+    private int httpPort = 0;
+
+    private final ApplicationConfig applicationConfig = new ApplicationConfig();
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    private ProducerCallbacks producerCallbacks;
+    private InfoTypes infoTypes;
+    private InfoJobs infoJobs;
+
+    @Bean
+    public ObjectMapper mapper() {
+        return new ObjectMapper();
+    }
+
+    @Bean
+    public ServletWebServerFactory servletContainer() {
+        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
+        if (httpPort > 0) {
+            tomcat.addAdditionalTomcatConnectors(getHttpConnector(httpPort));
+        }
+        return tomcat;
+    }
+
+    @Bean
+    public InfoJobs infoJobs(SecurityContext securityContext) {
+        if (infoJobs == null) {
+            infoJobs = new InfoJobs(getApplicationConfig(), producerCallbacks(securityContext));
+            try {
+                infoJobs.restoreJobsFromDatabase();
+            } catch (Exception e) {
+                logger.error("Could not restore jobs from database: {}", e.getMessage());
+            }
+        }
+        return infoJobs;
+    }
+
+    @Bean
+    public InfoTypes infoTypes() {
+        if (this.infoTypes == null) {
+            infoTypes = new InfoTypes(getApplicationConfig());
+            try {
+                infoTypes.restoreTypesFromDatabase();
+            } catch (Exception e) {
+                logger.error("Could not restore Information Types from database: {}", e.getMessage());
+            }
+        }
+        return infoTypes;
+    }
+
+    @Bean
+    public ProducerCallbacks producerCallbacks(SecurityContext securityContext) {
+        if (this.producerCallbacks == null) {
+            producerCallbacks = new ProducerCallbacks(getApplicationConfig(), securityContext);
+        }
+        return this.producerCallbacks;
+    }
+
+    @Bean
+    public ApplicationConfig getApplicationConfig() {
+        return this.applicationConfig;
+    }
+
+    private static Connector getHttpConnector(int httpPort) {
+        Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
+        connector.setScheme("http");
+        connector.setPort(httpPort);
+        connector.setSecure(false);
+        return connector;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/SwaggerConfig.java b/src/main/java/org/oransc/ics/SwaggerConfig.java
new file mode 100644 (file)
index 0000000..30c0d4a
--- /dev/null
@@ -0,0 +1,84 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.info.License;
+
+/**
+ * Swagger configuration class that uses swagger documentation type and scans
+ * all the controllers. To access the swagger gui go to
+ * http://ip:port/swagger-ui.html
+ */
+
+@OpenAPIDefinition( //
+    info = @Info(
+        title = SwaggerConfig.API_TITLE, //
+        version = "1.0", //
+        description = SwaggerConfig.DESCRIPTION, //
+        license = @License(
+            name = "Copyright (C) 2020-2022 Nordix Foundation. Licensed under the Apache License.",
+            url = "http://www.apache.org/licenses/LICENSE-2.0")))
+public class SwaggerConfig {
+    private SwaggerConfig() {
+    }
+
+    static final String API_TITLE = "Data management and exposure";
+
+    static final String DESCRIPTION = "<h1>API documentation</h1>" //
+        + "<h2>General</h2>" //
+        + "<p>" //
+        + "  The service is mainly a broker between data producers and data consumers. A data producer has the ability to produce one or several types of data (Information Type). One type of data can be produced by zero to many producers. <br /><br />A data consumer can have several active data subscriptions (Information Job). One Information Job consists of the type of data to produce and additional parameters for filtering of the data. These parameters are different for different data types." //
+        + "</p>" //
+        + "<h2>APIs provided by the service</h2>" //
+        + "<h4>A1-EI</h4>" //
+        + "<p>" //
+        + "  This API is between Near-RT RIC and the Non-RT RIC." //
+        + "  The Near-RT RIC is a data consumer, which creates Information Jobs to subscribe for data." //
+        + "  In this context, the information is referred to as 'Enrichment Information', EI." //
+        + "</p>" //
+        + "<h4>Data producer API</h4>" //
+        + "<p>" //
+        + "  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface." //
+        + "  The API is for use by different kinds of data producers and provides support for:" //
+        + "<ul>" //
+        + "<li>Registry of supported information types and which parameters needed to setup a subscription.</li>" //
+        + "<li>Registry of existing data producers.</li>" //
+        + "<li>Callback API provided by producers to setup subscriptions.</li>" //
+        + "</ul>" //
+        + "</p>" //
+        + "<h4>Data consumer API</h4>" //
+        + "<p>" //
+        + "  This API is provided by the Non-RT RIC platform and is intended to be part of the O-RAN R1 interface." //
+        + "  The API is for use by different kinds of data consumers and provides support for:" //
+        + "<ul>" //
+        + "<li>Querying of available types of data to consume.</li>" //
+        + "<li>Management of data subscription jobs</li>" //
+        + "<li>Optional callback API provided by consumers to get notification on added and removed information types.</li>" //
+        + "</ul>" //
+        + "</p>" //
+        + "<h4>Service status</h4>" //
+        + "<p>" //
+        + "  This API provides a means to monitor the health of this service." //
+        + "</p>";
+
+}
diff --git a/src/main/java/org/oransc/ics/clients/AsyncRestClient.java b/src/main/java/org/oransc/ics/clients/AsyncRestClient.java
new file mode 100644 (file)
index 0000000..2879092
--- /dev/null
@@ -0,0 +1,213 @@
+/*-
+ * ========================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.ics.clients;
+
+import io.netty.channel.ChannelOption;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.timeout.ReadTimeoutHandler;
+import io.netty.handler.timeout.WriteTimeoutHandler;
+
+import java.lang.invoke.MethodHandles;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.oransc.ics.configuration.WebClientConfig.HttpProxyConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.lang.Nullable;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.ExchangeStrategies;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
+
+import reactor.core.publisher.Mono;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.transport.ProxyProvider;
+
+/**
+ * Generic reactive REST client.
+ */
+public class AsyncRestClient {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private WebClient webClient = null;
+    private final String baseUrl;
+    private static final AtomicInteger sequenceNumber = new AtomicInteger();
+    private final SslContext sslContext;
+    private final HttpProxyConfig httpProxyConfig;
+    private final SecurityContext securityContext;
+
+    public AsyncRestClient(String baseUrl, @Nullable SslContext sslContext, @Nullable HttpProxyConfig httpProxyConfig,
+        SecurityContext securityContext) {
+        this.baseUrl = baseUrl;
+        this.sslContext = sslContext;
+        this.httpProxyConfig = httpProxyConfig;
+        this.securityContext = securityContext;
+    }
+
+    public Mono<ResponseEntity<String>> postForEntity(String uri, @Nullable String body) {
+        Mono<String> bodyProducer = body != null ? Mono.just(body) : Mono.empty();
+
+        RequestHeadersSpec<?> request = getWebClient() //
+            .post() //
+            .uri(uri) //
+            .contentType(MediaType.APPLICATION_JSON) //
+            .body(bodyProducer, String.class);
+        return retrieve(request);
+    }
+
+    public Mono<String> post(String uri, @Nullable String body) {
+        return postForEntity(uri, body) //
+            .map(this::toBody);
+    }
+
+    public Mono<String> postWithAuthHeader(String uri, String body, String username, String password) {
+        RequestHeadersSpec<?> request = getWebClient() //
+            .post() //
+            .uri(uri) //
+            .headers(headers -> headers.setBasicAuth(username, password)) //
+            .contentType(MediaType.APPLICATION_JSON) //
+            .bodyValue(body);
+        return retrieve(request) //
+            .map(this::toBody);
+    }
+
+    public Mono<ResponseEntity<String>> putForEntity(String uri, String body) {
+        RequestHeadersSpec<?> request = getWebClient() //
+            .put() //
+            .uri(uri) //
+            .contentType(MediaType.APPLICATION_JSON) //
+            .bodyValue(body);
+        return retrieve(request);
+    }
+
+    public Mono<ResponseEntity<String>> putForEntity(String uri) {
+        RequestHeadersSpec<?> request = getWebClient() //
+            .put() //
+            .uri(uri);
+        return retrieve(request);
+    }
+
+    public Mono<String> put(String uri, String body) {
+        return putForEntity(uri, body) //
+            .map(this::toBody);
+    }
+
+    public Mono<ResponseEntity<String>> getForEntity(String uri) {
+        RequestHeadersSpec<?> request = getWebClient().get().uri(uri);
+        return retrieve(request);
+    }
+
+    public Mono<String> get(String uri) {
+        return getForEntity(uri) //
+            .map(this::toBody);
+    }
+
+    public Mono<ResponseEntity<String>> deleteForEntity(String uri) {
+        RequestHeadersSpec<?> request = getWebClient().delete().uri(uri);
+        return retrieve(request);
+    }
+
+    public Mono<String> delete(String uri) {
+        return deleteForEntity(uri) //
+            .map(this::toBody);
+    }
+
+    private Mono<ResponseEntity<String>> retrieve(RequestHeadersSpec<?> request) {
+        if (securityContext.isConfigured()) {
+            request.headers(h -> h.setBearerAuth(securityContext.getBearerAuthToken()));
+        }
+        return request.retrieve() //
+            .toEntity(String.class);
+    }
+
+    private static Object createTraceTag() {
+        return sequenceNumber.incrementAndGet();
+    }
+
+    private String toBody(ResponseEntity<String> entity) {
+        if (entity.getBody() == null) {
+            return "";
+        } else {
+            return entity.getBody();
+        }
+    }
+
+    private boolean isHttpProxyConfigured() {
+        return httpProxyConfig != null && httpProxyConfig.httpProxyPort() > 0
+            && !httpProxyConfig.httpProxyHost().isEmpty();
+    }
+
+    private HttpClient buildHttpClient() {
+        HttpClient httpClient = HttpClient.create() //
+            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000) //
+            .doOnConnected(connection -> {
+                connection.addHandlerLast(new ReadTimeoutHandler(30));
+                connection.addHandlerLast(new WriteTimeoutHandler(30));
+            });
+
+        if (this.sslContext != null) {
+            httpClient = httpClient.secure(ssl -> ssl.sslContext(sslContext));
+        }
+
+        if (isHttpProxyConfigured()) {
+            httpClient = httpClient.proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP)
+                .host(httpProxyConfig.httpProxyHost()).port(httpProxyConfig.httpProxyPort()));
+        }
+        return httpClient;
+    }
+
+    public WebClient buildWebClient(String baseUrl) {
+        Object traceTag = createTraceTag();
+
+        final HttpClient httpClient = buildHttpClient();
+        ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() //
+            .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)) //
+            .build();
+
+        ExchangeFilterFunction reqLogger = ExchangeFilterFunction.ofRequestProcessor(req -> {
+            logger.debug("{} {} uri = '{}''", traceTag, req.method(), req.url());
+            return Mono.just(req);
+        });
+
+        ExchangeFilterFunction respLogger = ExchangeFilterFunction.ofResponseProcessor(resp -> {
+            logger.debug("{} resp: {}", traceTag, resp.statusCode());
+            return Mono.just(resp);
+        });
+
+        return WebClient.builder() //
+            .clientConnector(new ReactorClientHttpConnector(httpClient)) //
+            .baseUrl(baseUrl) //
+            .exchangeStrategies(exchangeStrategies) //
+            .filter(reqLogger) //
+            .filter(respLogger) //
+            .build();
+    }
+
+    private WebClient getWebClient() {
+        if (this.webClient == null) {
+            this.webClient = buildWebClient(baseUrl);
+        }
+        return this.webClient;
+    }
+}
diff --git a/src/main/java/org/oransc/ics/clients/AsyncRestClientFactory.java b/src/main/java/org/oransc/ics/clients/AsyncRestClientFactory.java
new file mode 100644 (file)
index 0000000..9a6c4f7
--- /dev/null
@@ -0,0 +1,193 @@
+/*-
+ * ========================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.ics.clients;
+
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.invoke.MethodHandles;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.net.ssl.KeyManagerFactory;
+
+import org.oransc.ics.configuration.WebClientConfig;
+import org.oransc.ics.configuration.WebClientConfig.HttpProxyConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.ResourceUtils;
+
+/**
+ * Factory for a generic reactive REST client.
+ */
+public class AsyncRestClientFactory {
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    private final SslContextFactory sslContextFactory;
+    private final HttpProxyConfig httpProxyConfig;
+    private final SecurityContext securityContext;
+
+    public AsyncRestClientFactory(WebClientConfig clientConfig, SecurityContext securityContext) {
+        if (clientConfig != null) {
+            this.sslContextFactory = new CachingSslContextFactory(clientConfig);
+            this.httpProxyConfig = clientConfig.httpProxyConfig();
+        } else {
+            logger.warn("No configuration for web client defined, HTTPS will not work");
+            this.sslContextFactory = null;
+            this.httpProxyConfig = null;
+        }
+        this.securityContext = securityContext;
+    }
+
+    public AsyncRestClient createRestClientNoHttpProxy(String baseUrl) {
+        return createRestClient(baseUrl, false);
+    }
+
+    public AsyncRestClient createRestClientUseHttpProxy(String baseUrl) {
+        return createRestClient(baseUrl, true);
+    }
+
+    private AsyncRestClient createRestClient(String baseUrl, boolean useHttpProxy) {
+        if (this.sslContextFactory != null) {
+            try {
+                return new AsyncRestClient(baseUrl, this.sslContextFactory.createSslContext(),
+                    useHttpProxy ? httpProxyConfig : null, this.securityContext);
+            } catch (Exception e) {
+                String exceptionString = e.toString();
+                logger.error("Could not init SSL context, reason: {}", exceptionString);
+            }
+        }
+        return new AsyncRestClient(baseUrl, null, httpProxyConfig, this.securityContext);
+    }
+
+    private class SslContextFactory {
+        private final WebClientConfig clientConfig;
+
+        public SslContextFactory(WebClientConfig clientConfig) {
+            this.clientConfig = clientConfig;
+        }
+
+        public SslContext createSslContext() throws UnrecoverableKeyException, NoSuchAlgorithmException,
+            CertificateException, KeyStoreException, IOException {
+            return this.createSslContext(createKeyManager());
+        }
+
+        private SslContext createSslContext(KeyManagerFactory keyManager)
+            throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException {
+            if (this.clientConfig.isTrustStoreUsed()) {
+                return createSslContextRejectingUntrustedPeers(this.clientConfig.trustStore(),
+                    this.clientConfig.trustStorePassword(), keyManager);
+            } else {
+                // Trust anyone
+                return SslContextBuilder.forClient() //
+                    .keyManager(keyManager) //
+                    .trustManager(InsecureTrustManagerFactory.INSTANCE) //
+                    .build();
+            }
+        }
+
+        private SslContext createSslContextRejectingUntrustedPeers(String trustStorePath, String trustStorePass,
+            KeyManagerFactory keyManager)
+            throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
+
+            final KeyStore trustStore = getTrustStore(trustStorePath, trustStorePass);
+            List<Certificate> certificateList = Collections.list(trustStore.aliases()).stream() //
+                .filter(alias -> isCertificateEntry(trustStore, alias)) //
+                .map(alias -> getCertificate(trustStore, alias)) //
+                .collect(Collectors.toList());
+            final X509Certificate[] certificates = certificateList.toArray(new X509Certificate[certificateList.size()]);
+
+            return SslContextBuilder.forClient() //
+                .keyManager(keyManager) //
+                .trustManager(certificates) //
+                .build();
+        }
+
+        private boolean isCertificateEntry(KeyStore trustStore, String alias) {
+            try {
+                return trustStore.isCertificateEntry(alias);
+            } catch (KeyStoreException e) {
+                logger.error("Error reading truststore {}", e.getMessage());
+                return false;
+            }
+        }
+
+        private Certificate getCertificate(KeyStore trustStore, String alias) {
+            try {
+                return trustStore.getCertificate(alias);
+            } catch (KeyStoreException e) {
+                logger.error("Error reading truststore {}", e.getMessage());
+                return null;
+            }
+        }
+
+        private KeyManagerFactory createKeyManager() throws NoSuchAlgorithmException, CertificateException, IOException,
+            UnrecoverableKeyException, KeyStoreException {
+            final KeyManagerFactory keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            final KeyStore keyStore = KeyStore.getInstance(this.clientConfig.keyStoreType());
+            final String keyStoreFile = this.clientConfig.keyStore();
+            final String keyStorePassword = this.clientConfig.keyStorePassword();
+            final String keyPassword = this.clientConfig.keyPassword();
+            try (final InputStream inputStream = new FileInputStream(keyStoreFile)) {
+                keyStore.load(inputStream, keyStorePassword.toCharArray());
+            }
+            keyManager.init(keyStore, keyPassword.toCharArray());
+            return keyManager;
+        }
+
+        private synchronized KeyStore getTrustStore(String trustStorePath, String trustStorePass)
+            throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
+
+            KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
+            store.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());
+            return store;
+        }
+    }
+
+    public class CachingSslContextFactory extends SslContextFactory {
+        private SslContext cachedContext = null;
+
+        public CachingSslContextFactory(WebClientConfig clientConfig) {
+            super(clientConfig);
+        }
+
+        @Override
+        public SslContext createSslContext() throws UnrecoverableKeyException, NoSuchAlgorithmException,
+            CertificateException, KeyStoreException, IOException {
+            if (this.cachedContext == null) {
+                this.cachedContext = super.createSslContext();
+            }
+            return this.cachedContext;
+
+        }
+    }
+}
diff --git a/src/main/java/org/oransc/ics/clients/SecurityContext.java b/src/main/java/org/oransc/ics/clients/SecurityContext.java
new file mode 100644 (file)
index 0000000..aadc1bf
--- /dev/null
@@ -0,0 +1,76 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2022 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.ics.clients;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import lombok.Setter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@EnableConfigurationProperties
+@ConfigurationProperties()
+@Component
+public class SecurityContext {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    private long tokenTimestamp = 0;
+
+    private String authToken = "";
+
+    @Setter
+    private Path authTokenFilePath;
+
+    public SecurityContext(@Value("${app.auth-token-file:\"\"}") String authTokenFilename) {
+        if (!authTokenFilename.isEmpty()) {
+            this.authTokenFilePath = Path.of(authTokenFilename);
+        }
+    }
+
+    public boolean isConfigured() {
+        return authTokenFilePath != null;
+    }
+
+    public synchronized String getBearerAuthToken() {
+        if (!isConfigured()) {
+            return "";
+        }
+        try {
+            long lastModified = authTokenFilePath.toFile().lastModified();
+            if (lastModified != this.tokenTimestamp) {
+                this.authToken = Files.readString(authTokenFilePath);
+                this.tokenTimestamp = lastModified;
+            }
+        } catch (Exception e) {
+            logger.warn("Could not read auth token file: {}, reason: {}", authTokenFilePath, e.getMessage());
+        }
+        return this.authToken;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/configuration/ApplicationConfig.java b/src/main/java/org/oransc/ics/configuration/ApplicationConfig.java
new file mode 100644 (file)
index 0000000..0a63d42
--- /dev/null
@@ -0,0 +1,96 @@
+/*-
+ * ========================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.ics.configuration;
+
+import lombok.Getter;
+
+import org.oransc.ics.configuration.WebClientConfig.HttpProxyConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+
+@EnableConfigurationProperties
+@ConfigurationProperties()
+public class ApplicationConfig {
+
+    private static final Logger logger = LoggerFactory.getLogger(ApplicationConfig.class);
+
+    @Getter
+    @Value("${app.vardata-directory}")
+    private String vardataDirectory;
+
+    @Value("${server.ssl.key-store-type}")
+    private String sslKeyStoreType = "";
+
+    @Value("${server.ssl.key-store-password}")
+    private String sslKeyStorePassword = "";
+
+    @Value("${server.ssl.key-store}")
+    private String sslKeyStore = "";
+
+    @Value("${server.ssl.key-password}")
+    private String sslKeyPassword = "";
+
+    @Value("${app.webclient.trust-store-used}")
+    private boolean sslTrustStoreUsed = false;
+
+    @Value("${app.webclient.trust-store-password}")
+    private String sslTrustStorePassword = "";
+
+    @Value("${app.webclient.trust-store}")
+    private String sslTrustStore = "";
+
+    @Value("${app.webclient.http.proxy-host:\"\"}")
+    private String httpProxyHost = "";
+
+    @Value("${app.webclient.http.proxy-port:0}")
+    private int httpProxyPort = 0;
+
+    private WebClientConfig webClientConfig = null;
+
+    public WebClientConfig getWebClientConfig() {
+        if (this.webClientConfig == null) {
+            if (this.httpProxyPort == 0) {
+                logger.info("Http proxy is not used");
+            } else {
+                logger.info("Http proxy is used for RAN access {}:{}", httpProxyHost, httpProxyPort);
+            }
+            HttpProxyConfig httpProxyConfig = ImmutableHttpProxyConfig.builder() //
+                .httpProxyHost(this.httpProxyHost) //
+                .httpProxyPort(this.httpProxyPort) //
+                .build();
+            this.webClientConfig = ImmutableWebClientConfig.builder() //
+                .keyStoreType(this.sslKeyStoreType) //
+                .keyStorePassword(this.sslKeyStorePassword) //
+                .keyStore(this.sslKeyStore) //
+                .keyPassword(this.sslKeyPassword) //
+                .isTrustStoreUsed(this.sslTrustStoreUsed) //
+                .trustStore(this.sslTrustStore) //
+                .trustStorePassword(this.sslTrustStorePassword) //
+                .httpProxyConfig(httpProxyConfig) //
+                .build();
+        }
+        return this.webClientConfig;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/configuration/WebClientConfig.java b/src/main/java/org/oransc/ics/configuration/WebClientConfig.java
new file mode 100644 (file)
index 0000000..12e9cfd
--- /dev/null
@@ -0,0 +1,54 @@
+/*-
+ * ========================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.ics.configuration;
+
+import org.immutables.value.Value;
+
+@Value.Immutable
+@Value.Style(redactedMask = "####")
+public interface WebClientConfig {
+    public String keyStoreType();
+
+    @Value.Redacted
+    public String keyStorePassword();
+
+    public String keyStore();
+
+    @Value.Redacted
+    public String keyPassword();
+
+    public boolean isTrustStoreUsed();
+
+    @Value.Redacted
+    public String trustStorePassword();
+
+    public String trustStore();
+
+    @Value.Immutable
+    public interface HttpProxyConfig {
+        public String httpProxyHost();
+
+        public int httpProxyPort();
+    }
+
+    public HttpProxyConfig httpProxyConfig();
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/ErrorResponse.java b/src/main/java/org/oransc/ics/controllers/ErrorResponse.java
new file mode 100644 (file)
index 0000000..25b0c16
--- /dev/null
@@ -0,0 +1,110 @@
+/*-
+ * ========================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.ics.controllers;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.oransc.ics.exceptions.ServiceException;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import reactor.core.publisher.Mono;
+
+public class ErrorResponse {
+    private static Gson gson = new GsonBuilder().create();
+
+    // Returned as body for all failed REST calls
+    @Schema(
+        name = "ProblemDetails",
+        description = "A problem detail to carry details in a HTTP response according to RFC 7807")
+    public static class ErrorInfo {
+        @SerializedName("type")
+        private String type = "about:blank";
+
+        @SerializedName("title")
+        private String title = null;
+
+        @SerializedName("status")
+        private final Integer status;
+
+        @SerializedName("detail")
+        private String detail = null;
+
+        @SerializedName("instance")
+        private String instance = null;
+
+        public ErrorInfo(String detail, Integer status) {
+            this.detail = detail;
+            this.status = status;
+        }
+
+        @Schema(
+            example = "404",
+            description = "The HTTP status code generated by the origin server for this occurrence of the problem.")
+        public Integer getStatus() {
+            return status;
+        }
+
+        @Schema(
+            example = "Information Job type not found",
+            description = "A human-readable explanation specific to this occurrence of the problem.")
+        public String getDetail() {
+            return this.detail;
+        }
+    }
+
+    @Schema(name = "message")
+    public final String message;
+
+    ErrorResponse(String message) {
+        this.message = message;
+    }
+
+    public static Mono<ResponseEntity<Object>> createMono(Throwable e, HttpStatus code) {
+        return Mono.just(create(e, code));
+    }
+
+    public static ResponseEntity<Object> create(Throwable e, HttpStatus code) {
+        if (e instanceof RuntimeException) {
+            code = HttpStatus.INTERNAL_SERVER_ERROR;
+        } else if (e instanceof ServiceException) {
+            ServiceException se = (ServiceException) e;
+            if (se.getHttpStatus() != null) {
+                code = se.getHttpStatus();
+            }
+        }
+        return create(e.toString(), code);
+    }
+
+    public static ResponseEntity<Object> create(String str, HttpStatus code) {
+        ErrorInfo errorInfo = new ErrorInfo(str, code.value());
+        String json = gson.toJson(errorInfo);
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON);
+        return new ResponseEntity<>(json, headers, code);
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/StatusController.java b/src/main/java/org/oransc/ics/controllers/StatusController.java
new file mode 100644 (file)
index 0000000..0fe586c
--- /dev/null
@@ -0,0 +1,106 @@
+/*-
+ * ========================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.ics.controllers;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import org.immutables.gson.Gson;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducers;
+import org.oransc.ics.repository.InfoTypes;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@RestController("StatusController")
+@Tag(name = StatusController.API_NAME, description = StatusController.API_DESCRIPTION)
+public class StatusController {
+
+    public static final String API_NAME = "Service status";
+    public static final String API_DESCRIPTION = "API for monitoring of the service";
+
+    @Autowired
+    private InfoJobs infoJobs;
+
+    @Autowired
+    private InfoTypes infoTypes;
+
+    @Autowired
+    private InfoProducers infoProducers;
+
+    @Gson.TypeAdapters
+    @Schema(name = "service_status_info")
+    public static class StatusInfo {
+        @Schema(name = "status", description = "status text")
+        @SerializedName("status")
+        @JsonProperty(value = "status", required = true)
+        public final String status;
+
+        @Schema(name = "no_of_producers", description = "Number of Information Producers")
+        @SerializedName("no_of_producers")
+        @JsonProperty(value = "no_of_producers", required = true)
+        public final int noOfProducers;
+
+        @Schema(name = "no_of_types", description = "Number of Information Types")
+        @SerializedName("no_of_types")
+        @JsonProperty(value = "no_of_types", required = true)
+        public final int noOfTypes;
+
+        @Schema(name = "no_of_jobs", description = "Number of Information Jobs")
+        @SerializedName("no_of_jobs")
+        @JsonProperty(value = "no_of_jobs", required = true)
+        public final int noOfJobs;
+
+        public StatusInfo(String status, InfoProducers producers, InfoTypes types, InfoJobs jobs) {
+            this.status = status;
+            this.noOfJobs = jobs.size();
+            this.noOfProducers = producers.size();
+            this.noOfTypes = types.size();
+        }
+    }
+
+    @GetMapping(path = "/status", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Returns status and statistics of this service")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Service is living", //
+                content = @Content(schema = @Schema(implementation = StatusInfo.class))) //
+        })
+    public Mono<ResponseEntity<Object>> getStatus() {
+        StatusInfo info = new StatusInfo("hunky dory", this.infoProducers, this.infoTypes, this.infoJobs);
+        return Mono.just(new ResponseEntity<>(info, HttpStatus.OK));
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/VoidResponse.java b/src/main/java/org/oransc/ics/controllers/VoidResponse.java
new file mode 100644 (file)
index 0000000..8fb810d
--- /dev/null
@@ -0,0 +1,32 @@
+/*-
+ * ========================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.ics.controllers;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "Void", description = "Void/empty ")
+public class VoidResponse {
+    private VoidResponse() {
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eCallbacks.java
new file mode 100644 (file)
index 0000000..15394ab
--- /dev/null
@@ -0,0 +1,87 @@
+/*-
+ * ========================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.ics.controllers.a1e;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+
+import org.oransc.ics.clients.AsyncRestClient;
+import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.repository.InfoJob;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducers;
+import org.oransc.ics.repository.InfoType;
+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;
+
+/**
+ * Callbacks to the EiProducer
+ */
+@Component
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
+public class A1eCallbacks {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private static Gson gson = new GsonBuilder().create();
+
+    private final AsyncRestClient restClient;
+    private final InfoJobs eiJobs;
+
+    @Autowired
+    public A1eCallbacks(ApplicationConfig config, InfoJobs eiJobs, SecurityContext securityContext) {
+        AsyncRestClientFactory restClientFactory =
+            new AsyncRestClientFactory(config.getWebClientConfig(), securityContext);
+        this.restClient = restClientFactory.createRestClientUseHttpProxy("");
+        this.eiJobs = eiJobs;
+    }
+
+    public Flux<String> notifyJobStatus(Collection<InfoType> eiTypes, InfoProducers eiProducers) {
+        return Flux.fromIterable(eiTypes) //
+            .flatMap(eiType -> Flux.fromIterable(this.eiJobs.getJobsForType(eiType))) //
+            .filter(eiJob -> !eiJob.getJobStatusUrl().isEmpty()) //
+            .filter(eiJob -> eiProducers.isJobEnabled(eiJob) != eiJob.isLastStatusReportedEnabled())
+            .flatMap(eiJob -> noifyStatusToJobOwner(eiJob, eiProducers));
+    }
+
+    private Mono<String> noifyStatusToJobOwner(InfoJob job, InfoProducers eiProducers) {
+        boolean isJobEnabled = eiProducers.isJobEnabled(job);
+        A1eEiJobStatus status = isJobEnabled ? new A1eEiJobStatus(A1eEiJobStatus.EiJobStatusValues.ENABLED)
+            : new A1eEiJobStatus(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+        String body = gson.toJson(status);
+        return this.restClient.post(job.getJobStatusUrl(), body) //
+            .doOnNext(response -> logger.debug("Consumer notified OK {}", job.getId())) //
+            .doOnNext(response -> job.setLastReportedStatus(isJobEnabled)) //
+            .onErrorResume(throwable -> {
+                logger.warn("Consumer notify failed {} {}", job.getJobStatusUrl(), throwable.toString());
+                return Mono.empty();
+            });
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eConsts.java
new file mode 100644 (file)
index 0000000..03d8ce2
--- /dev/null
@@ -0,0 +1,40 @@
+/*-
+ * ========================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.ics.controllers.a1e;
+
+public class A1eConsts {
+
+    public static final String CONSUMER_API_NAME = "A1-EI (registration)";
+    public static final String CONSUMER_API_DESCRIPTION = "Data consumer EI job registration";
+
+    public static final String CONSUMER_API_CALLBACKS_NAME = "A1-EI (callbacks)";
+    public static final String CONSUMER_API_CALLBACKS_DESCRIPTION = "Data consumer EI job status callbacks";
+
+    public static final String API_ROOT = "/A1-EI/v1";
+    public static final String OWNER_PARAM = "owner";
+    public static final String OWNER_PARAM_DESCRIPTION = "selects EI jobs for one EI job owner";
+
+    public static final String EI_TYPE_ID_PARAM = "eiTypeId";
+    public static final String EI_TYPE_ID_PARAM_DESCRIPTION = "selects EI jobs of matching EI type";
+
+    private A1eConsts() {
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eController.java
new file mode 100644 (file)
index 0000000..699308d
--- /dev/null
@@ -0,0 +1,374 @@
+/*-
+ * ========================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.ics.controllers.a1e;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import java.lang.invoke.MethodHandles;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.json.JSONObject;
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.controllers.ErrorResponse;
+import org.oransc.ics.controllers.VoidResponse;
+import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
+import org.oransc.ics.exceptions.ServiceException;
+import org.oransc.ics.repository.InfoJob;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducers;
+import org.oransc.ics.repository.InfoType;
+import org.oransc.ics.repository.InfoTypes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+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.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
+@RestController("A1-EI")
+@Tag(name = A1eConsts.CONSUMER_API_NAME, description = A1eConsts.CONSUMER_API_DESCRIPTION)
+@RequestMapping(path = A1eConsts.API_ROOT, produces = MediaType.APPLICATION_JSON_VALUE)
+public class A1eController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Autowired
+    ApplicationConfig applicationConfig;
+
+    @Autowired
+    private InfoJobs eiJobs;
+
+    @Autowired
+    private InfoTypes eiTypes;
+
+    @Autowired
+    private InfoProducers infoProducers;
+
+    @Autowired
+    ProducerCallbacks producerCallbacks;
+
+    private static Gson gson = new GsonBuilder().create();
+
+    @GetMapping(path = "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "EI type identifiers", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "EI type identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), //
+        })
+    public ResponseEntity<Object> getEiTypeIdentifiers( //
+    ) {
+        List<String> result = new ArrayList<>();
+        for (InfoType eiType : this.eiTypes.getAllInfoTypes()) {
+            result.add(eiType.getId());
+        }
+
+        return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+    }
+
+    @GetMapping(path = "/eitypes/{eiTypeId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual EI type", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "EI type", //
+                content = @Content(schema = @Schema(implementation = A1eEiTypeInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Enrichment Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getEiType( //
+        @PathVariable("eiTypeId") String eiTypeId) {
+        try {
+            this.eiTypes.getType(eiTypeId); // Make sure that the type exists
+            A1eEiTypeInfo info = toEiTypeInfo();
+            return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/eijobs", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "EI job identifiers", description = "query for EI job identifiers")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "EI job identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))),
+            @ApiResponse(
+                responseCode = "404",
+                description = "Enrichment Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getEiJobIds( //
+        @Parameter(
+            name = A1eConsts.EI_TYPE_ID_PARAM,
+            required = false, //
+            description = A1eConsts.EI_TYPE_ID_PARAM_DESCRIPTION) //
+        @RequestParam(name = A1eConsts.EI_TYPE_ID_PARAM, required = false) String eiTypeId,
+        @Parameter(
+            name = A1eConsts.OWNER_PARAM,
+            required = false, //
+            description = A1eConsts.OWNER_PARAM_DESCRIPTION) //
+        @RequestParam(name = A1eConsts.OWNER_PARAM, required = false) String owner) {
+        try {
+            List<String> result = new ArrayList<>();
+            if (owner != null) {
+                for (InfoJob job : this.eiJobs.getJobsForOwner(owner)) {
+                    if (eiTypeId == null || job.getTypeId().equals(eiTypeId)) {
+                        result.add(job.getId());
+                    }
+                }
+            } else if (eiTypeId != null) {
+                this.eiJobs.getJobsForType(eiTypeId).forEach(job -> result.add(job.getId()));
+            } else {
+                this.eiJobs.getJobs().forEach(job -> result.add(job.getId()));
+            }
+            return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+        } catch (
+
+        Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/eijobs/{eiJobId}", produces = MediaType.APPLICATION_JSON_VALUE) //
+    @Operation(summary = "Individual EI job", description = "") //
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "EI job", //
+                content = @Content(schema = @Schema(implementation = A1eEiJobInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Enrichment Information job is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getIndividualEiJob( //
+        @PathVariable("eiJobId") String eiJobId) {
+        try {
+            InfoJob job = this.eiJobs.getJob(eiJobId);
+            return new ResponseEntity<>(gson.toJson(toEiJobInfo(job)), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/eijobs/{eiJobId}/status", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "EI job status", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "EI job status", //
+                content = @Content(schema = @Schema(implementation = A1eEiJobStatus.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Enrichment Information job is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getEiJobStatus( //
+        @PathVariable("eiJobId") String eiJobId) {
+        try {
+            InfoJob job = this.eiJobs.getJob(eiJobId);
+            return new ResponseEntity<>(gson.toJson(toEiJobStatus(job)), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    private A1eEiJobStatus toEiJobStatus(InfoJob job) {
+        return this.infoProducers.isJobEnabled(job) ? new A1eEiJobStatus(A1eEiJobStatus.EiJobStatusValues.ENABLED)
+            : new A1eEiJobStatus(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+
+    }
+
+    @DeleteMapping(path = "/eijobs/{eiJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual EI job", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Not used", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "204",
+                description = "Job deleted", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Enrichment Information job is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> deleteIndividualEiJob( //
+        @PathVariable("eiJobId") String eiJobId) {
+        try {
+            InfoJob job = this.eiJobs.getJob(eiJobId);
+            this.eiJobs.remove(job, this.infoProducers);
+            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PutMapping(
+        path = "/eijobs/{eiJobId}", //
+        produces = MediaType.APPLICATION_JSON_VALUE, //
+        consumes = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual EI job", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "201",
+                description = "Job created", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Job updated", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Enrichment Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))),
+            @ApiResponse(
+                responseCode = "400",
+                description = "Input validation failed", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
+            @ApiResponse(
+                responseCode = "409",
+                description = "Cannot modify job type", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))
+
+        })
+
+    public Mono<ResponseEntity<Object>> putIndividualEiJob( //
+        @PathVariable("eiJobId") String eiJobId, //
+        @RequestBody A1eEiJobInfo eiJobObject) {
+
+        final boolean isNewJob = this.eiJobs.get(eiJobId) == null;
+
+        return validatePutEiJob(eiJobId, eiJobObject) //
+            .flatMap(this::startEiJob) //
+            .doOnNext(newEiJob -> this.eiJobs.put(newEiJob)) //
+            .map(newEiJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) //
+            .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.INTERNAL_SERVER_ERROR)));
+    }
+
+    private Mono<InfoJob> startEiJob(InfoJob newEiJob) {
+        return this.producerCallbacks.startInfoSubscriptionJob(newEiJob, infoProducers) //
+            .doOnNext(noOfAcceptingProducers -> this.logger.debug(
+                "Started EI job {}, number of activated producers: {}", newEiJob.getId(), noOfAcceptingProducers)) //
+            .map(noOfAcceptingProducers -> newEiJob);
+    }
+
+    private Mono<InfoJob> validatePutEiJob(String eiJobId, A1eEiJobInfo eiJobInfo) {
+        try {
+            InfoType eiType = this.eiTypes.getType(eiJobInfo.eiTypeId);
+            validateJsonObjectAgainstSchema(eiType.getJobDataSchema(), eiJobInfo.jobDefinition);
+            InfoJob existingEiJob = this.eiJobs.get(eiJobId);
+            validateUri(eiJobInfo.jobResultUri);
+            validateUri(eiJobInfo.statusNotificationUri);
+
+            if (existingEiJob != null && !existingEiJob.getTypeId().equals(eiJobInfo.eiTypeId)) {
+                throw new ServiceException("Not allowed to change type for existing EI job", HttpStatus.CONFLICT);
+            }
+            return Mono.just(toEiJob(eiJobInfo, eiJobId, eiType));
+        } catch (Exception e) {
+            return Mono.error(e);
+        }
+    }
+
+    private void validateUri(String url) throws URISyntaxException, ServiceException {
+        if (url != null && !url.isEmpty()) {
+            URI uri = new URI(url);
+            if (!uri.isAbsolute()) {
+                throw new ServiceException("URI: " + url + " is not absolute", HttpStatus.BAD_REQUEST);
+            }
+        }
+    }
+
+    private void validateJsonObjectAgainstSchema(Object schemaObj, Object object) throws ServiceException {
+        if (schemaObj != null) { // schema is optional for now
+            try {
+                ObjectMapper mapper = new ObjectMapper();
+
+                String schemaAsString = mapper.writeValueAsString(schemaObj);
+                JSONObject schemaJSON = new JSONObject(schemaAsString);
+                var schema = org.everit.json.schema.loader.SchemaLoader.load(schemaJSON);
+
+                String objectAsString = mapper.writeValueAsString(object);
+                JSONObject json = new JSONObject(objectAsString);
+                schema.validate(json);
+            } catch (Exception e) {
+                throw new ServiceException("Json validation failure " + e.toString(), HttpStatus.BAD_REQUEST);
+            }
+        }
+    }
+
+    private InfoJob toEiJob(A1eEiJobInfo info, String id, InfoType type) {
+        return InfoJob.builder() //
+            .id(id) //
+            .typeId(type.getId()) //
+            .owner(info.owner) //
+            .jobData(info.jobDefinition) //
+            .targetUrl(info.jobResultUri) //
+            .jobStatusUrl(info.statusNotificationUri == null ? "" : info.statusNotificationUri) //
+            .build();
+    }
+
+    private A1eEiTypeInfo toEiTypeInfo() {
+        return new A1eEiTypeInfo();
+    }
+
+    private A1eEiJobInfo toEiJobInfo(InfoJob s) {
+        return new A1eEiJobInfo(s.getTypeId(), s.getJobData(), s.getOwner(), s.getTargetUrl(), s.getJobStatusUrl());
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eEiJobInfo.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eEiJobInfo.java
new file mode 100644 (file)
index 0000000..e677496
--- /dev/null
@@ -0,0 +1,69 @@
+/*-
+ * ========================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.ics.controllers.a1e;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "EiJobObject", description = "Information for an Enrichment Information Job")
+public class A1eEiJobInfo {
+
+    @Schema(name = "eiTypeId", description = "EI type Idenitifier of the EI job", required = true)
+    @SerializedName("eiTypeId")
+    @JsonProperty(value = "eiTypeId", required = true)
+    public String eiTypeId = "";
+
+    @Schema(name = "jobOwner", description = "Identity of the owner of the job", required = true)
+    @SerializedName("jobOwner")
+    @JsonProperty(value = "jobOwner", required = true)
+    public String owner = "";
+
+    @Schema(name = "jobDefinition", description = "EI type specific job data", required = true)
+    @SerializedName("jobDefinition")
+    @JsonProperty(value = "jobDefinition", required = true)
+    public Object jobDefinition;
+
+    @Schema(name = "jobResultUri", description = "The target URI of the EI data", required = true)
+    @SerializedName("jobResultUri")
+    @JsonProperty(value = "jobResultUri", required = true)
+    public String jobResultUri = "";
+
+    @Schema(name = "statusNotificationUri", description = "The target of EI job status notifications", required = false)
+    @SerializedName("jobStatusNotificationUri")
+    @JsonProperty(value = "jobStatusNotificationUri", required = false)
+    public String statusNotificationUri = "";
+
+    public A1eEiJobInfo() {
+    }
+
+    public A1eEiJobInfo(String eiTypeId, Object jobData, String owner, String targetUri, String statusNotificationUri) {
+        this.eiTypeId = eiTypeId;
+        this.jobDefinition = jobData;
+        this.owner = owner;
+        this.jobResultUri = targetUri;
+        this.statusNotificationUri = statusNotificationUri;
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eEiJobStatus.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eEiJobStatus.java
new file mode 100644 (file)
index 0000000..6593051
--- /dev/null
@@ -0,0 +1,56 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.controllers.a1e;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "EiJobStatusObject", description = "Status for an EI job")
+public class A1eEiJobStatus {
+
+    @Gson.TypeAdapters
+    @Schema(name = "EiJobStatusValues", description = OPERATIONAL_STATE_DESCRIPTION)
+    public enum EiJobStatusValues {
+        ENABLED, DISABLED
+    }
+
+    private static final String OPERATIONAL_STATE_DESCRIPTION = "Allowed values for EI job status: <br/>" //
+        + "ENABLED: the A1-EI producer is able to deliver EI result for the EI job <br/>" //
+        + "DISABLED: the A1-EI producer is unable to deliver EI result for the EI job";
+
+    @Schema(name = "eiJobStatus", description = OPERATIONAL_STATE_DESCRIPTION, required = true)
+    @SerializedName("eiJobStatus")
+    @JsonProperty(value = "eiJobStatus", required = true)
+    public EiJobStatusValues state;
+
+    public A1eEiJobStatus() {
+    }
+
+    public A1eEiJobStatus(EiJobStatusValues state) {
+        this.state = state;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/a1e/A1eEiTypeInfo.java b/src/main/java/org/oransc/ics/controllers/a1e/A1eEiTypeInfo.java
new file mode 100644 (file)
index 0000000..be5499f
--- /dev/null
@@ -0,0 +1,31 @@
+/*-
+ * ========================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.ics.controllers.a1e;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "EiTypeObject", description = "Information for an EI type")
+public class A1eEiTypeInfo {
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerCallbacks.java
new file mode 100644 (file)
index 0000000..3db904d
--- /dev/null
@@ -0,0 +1,76 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2021 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.controllers.r1consumer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import org.oransc.ics.clients.AsyncRestClient;
+import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.repository.InfoType;
+import org.oransc.ics.repository.InfoTypeSubscriptions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+/**
+ * Callbacks to the Consumer. Notifies consumer according to the API (which this
+ * class adapts to)
+ */
+@Component
+public class ConsumerCallbacks implements InfoTypeSubscriptions.ConsumerCallbackHandler {
+
+    private static Gson gson = new GsonBuilder().create();
+
+    private final AsyncRestClient restClient;
+
+    public static final String API_VERSION = "version_1";
+
+    @Autowired
+    public ConsumerCallbacks(ApplicationConfig config, InfoTypeSubscriptions infoTypeSubscriptions,
+        SecurityContext securityContext) {
+        AsyncRestClientFactory restClientFactory =
+            new AsyncRestClientFactory(config.getWebClientConfig(), securityContext);
+        this.restClient = restClientFactory.createRestClientNoHttpProxy("");
+        infoTypeSubscriptions.registerCallbackhandler(this, API_VERSION);
+    }
+
+    @Override
+    public Mono<String> notifyTypeRegistered(InfoType type, InfoTypeSubscriptions.SubscriptionInfo subscriptionInfo) {
+        String body = body(type, ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.REGISTERED);
+        return restClient.post(subscriptionInfo.getCallbackUrl(), body);
+    }
+
+    @Override
+    public Mono<String> notifyTypeRemoved(InfoType type, InfoTypeSubscriptions.SubscriptionInfo subscriptionInfo) {
+        String body = body(type, ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.DEREGISTERED);
+        return restClient.post(subscriptionInfo.getCallbackUrl(), body);
+    }
+
+    private String body(InfoType type, ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues status) {
+        ConsumerTypeRegistrationInfo info =
+            new ConsumerTypeRegistrationInfo(type.getJobDataSchema(), status, type.getId());
+        return gson.toJson(info);
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerConsts.java
new file mode 100644 (file)
index 0000000..7005a23
--- /dev/null
@@ -0,0 +1,59 @@
+/*-
+ * ========================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.ics.controllers.r1consumer;
+
+public class ConsumerConsts {
+
+    public static final String API_ROOT = "/data-consumer/v1";
+
+    public static final String CONSUMER_API_NAME = "Data consumer";
+    public static final String CONSUMER_API_CALLBACKS_NAME = "Data consumer (callbacks)";
+    public static final String CONSUMER_API_DESCRIPTION = "API for data consumers";
+
+    public static final String OWNER_PARAM = "owner";
+    public static final String OWNER_PARAM_DESCRIPTION = "selects result for one owner";
+
+    public static final String INDIVIDUAL_JOB = "Individual data subscription job";
+
+    public static final String PUT_INDIVIDUAL_JOB_DESCRIPTION = "The job will be enabled when a producer is available";
+
+    public static final String INFO_TYPE_ID_PARAM = "infoTypeId";
+    public static final String INFO_TYPE_ID_PARAM_DESCRIPTION =
+        "selects subscription jobs of matching information type";
+    public static final String INFO_TYPE_ID_PATH = "infoTypeId";
+
+    public static final String INFO_JOB_ID_PATH = "infoJobId";
+
+    public static final String SUBSCRIPTION_ID_PATH = "subscriptionId";
+
+    public static final String PERFORM_TYPE_CHECK_PARAM = "typeCheck";
+    public static final String PERFORM_TYPE_CHECK_PARAM_DESCRIPTION =
+        "when true, a validation of that the type exists and that the job matches the type schema.";
+
+    public static final String INDIVIDUAL_TYPE_SUBSCRIPTION =
+        "Individual subscription for information types (registration/deregistration)";
+
+    public static final String TYPE_SUBSCRIPTION_DESCRIPTION =
+        "This service operation is used to subscribe to notifications for changes in the availability of data types.";
+
+    private ConsumerConsts() {
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerController.java
new file mode 100644 (file)
index 0000000..fa335d9
--- /dev/null
@@ -0,0 +1,521 @@
+/*-
+ * ========================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.ics.controllers.r1consumer;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import java.lang.invoke.MethodHandles;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.json.JSONObject;
+import org.oransc.ics.controllers.ErrorResponse;
+import org.oransc.ics.controllers.VoidResponse;
+import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
+import org.oransc.ics.exceptions.ServiceException;
+import org.oransc.ics.repository.InfoJob;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducer;
+import org.oransc.ics.repository.InfoProducers;
+import org.oransc.ics.repository.InfoType;
+import org.oransc.ics.repository.InfoTypeSubscriptions;
+import org.oransc.ics.repository.InfoTypes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+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.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
+@RestController("Consumer API")
+@Tag(name = ConsumerConsts.CONSUMER_API_NAME, description = ConsumerConsts.CONSUMER_API_DESCRIPTION)
+@RequestMapping(path = ConsumerConsts.API_ROOT, produces = MediaType.APPLICATION_JSON_VALUE)
+public class ConsumerController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private final InfoJobs infoJobs;
+    private final InfoTypes infoTypes;
+    private final InfoProducers infoProducers;
+    private final ProducerCallbacks producerCallbacks;
+    private final InfoTypeSubscriptions infoTypeSubscriptions;
+    private static Gson gson = new GsonBuilder().create();
+
+    public ConsumerController(@Autowired InfoJobs jobs, @Autowired InfoTypes infoTypes,
+        @Autowired InfoProducers infoProducers, @Autowired ProducerCallbacks producerCallbacks,
+        @Autowired InfoTypeSubscriptions infoTypeSubscriptions) {
+        this.infoProducers = infoProducers;
+        this.infoJobs = jobs;
+        this.infoTypeSubscriptions = infoTypeSubscriptions;
+        this.infoTypes = infoTypes;
+        this.producerCallbacks = producerCallbacks;
+    }
+
+    @GetMapping(path = "/info-types", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Information type identifiers", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information type identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), //
+        })
+    public ResponseEntity<Object> getinfoTypeIdentifiers( //
+    ) {
+        List<String> result = new ArrayList<>();
+        for (InfoType infoType : this.infoTypes.getAllInfoTypes()) {
+            result.add(infoType.getId());
+        }
+
+        return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+    }
+
+    @GetMapping(path = "/info-types/{infoTypeId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual information type", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information type", //
+                content = @Content(schema = @Schema(implementation = ConsumerInfoTypeInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getInfoType( //
+        @PathVariable(ConsumerConsts.INFO_TYPE_ID_PATH) String infoTypeId) {
+        try {
+            InfoType type = this.infoTypes.getType(infoTypeId);
+            ConsumerInfoTypeInfo info = toInfoTypeInfo(type);
+            return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/info-jobs", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Information Job identifiers", description = "query for information job identifiers")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information information job identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))),
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getJobIds( //
+        @Parameter(
+            name = ConsumerConsts.INFO_TYPE_ID_PARAM,
+            required = false, //
+            description = ConsumerConsts.INFO_TYPE_ID_PARAM_DESCRIPTION) //
+        @RequestParam(name = ConsumerConsts.INFO_TYPE_ID_PARAM, required = false) String infoTypeId,
+        @Parameter(
+            name = ConsumerConsts.OWNER_PARAM,
+            required = false, //
+            description = ConsumerConsts.OWNER_PARAM_DESCRIPTION) //
+        @RequestParam(name = ConsumerConsts.OWNER_PARAM, required = false) String owner) {
+        try {
+            List<String> result = new ArrayList<>();
+            if (owner != null) {
+                for (InfoJob job : this.infoJobs.getJobsForOwner(owner)) {
+                    if (infoTypeId == null || job.getTypeId().equals(infoTypeId)) {
+                        result.add(job.getId());
+                    }
+                }
+            } else if (infoTypeId != null) {
+                this.infoJobs.getJobsForType(infoTypeId).forEach(job -> result.add(job.getId()));
+            } else {
+                this.infoJobs.getJobs().forEach(job -> result.add(job.getId()));
+            }
+            return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+        } catch (
+
+        Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/info-jobs/{infoJobId}", produces = MediaType.APPLICATION_JSON_VALUE) //
+    @Operation(summary = ConsumerConsts.INDIVIDUAL_JOB, description = "") //
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information subscription job", //
+                content = @Content(schema = @Schema(implementation = ConsumerJobInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information subscription job is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getIndividualEiJob( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) {
+        try {
+            InfoJob job = this.infoJobs.getJob(infoJobId);
+            return new ResponseEntity<>(gson.toJson(toInfoJobInfo(job)), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/info-jobs/{infoJobId}/status", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Job status", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information subscription job status", //
+                content = @Content(schema = @Schema(implementation = ConsumerJobStatus.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information subscription job is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getEiJobStatus( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId) {
+        try {
+            InfoJob job = this.infoJobs.getJob(jobId);
+            return new ResponseEntity<>(gson.toJson(toInfoJobStatus(job)), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    private ConsumerJobStatus toInfoJobStatus(InfoJob job) {
+        Collection<String> producerIds = new ArrayList<>();
+        this.infoProducers.getProducersForType(job.getTypeId()).forEach(producer -> producerIds.add(producer.getId()));
+        return this.infoProducers.isJobEnabled(job)
+            ? new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.ENABLED, producerIds)
+            : new ConsumerJobStatus(ConsumerJobStatus.InfoJobStatusValues.DISABLED, producerIds);
+
+    }
+
+    @DeleteMapping(path = "/info-jobs/{infoJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = ConsumerConsts.INDIVIDUAL_JOB, description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Not used", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "204",
+                description = "Job deleted", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), // "Individual
+                                                                                            // Information Job"
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information subscription job is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> deleteIndividualEiJob( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId) {
+        try {
+            InfoJob job = this.infoJobs.getJob(jobId);
+            this.infoJobs.remove(job, this.infoProducers);
+            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PutMapping(
+        path = "/info-jobs/{infoJobId}", //
+        produces = MediaType.APPLICATION_JSON_VALUE, //
+        consumes = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = ConsumerConsts.INDIVIDUAL_JOB, description = ConsumerConsts.PUT_INDIVIDUAL_JOB_DESCRIPTION)
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "201",
+                description = "Job created", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Job updated", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
+            @ApiResponse(
+                responseCode = "400",
+                description = "Input validation failed", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
+            @ApiResponse(
+                responseCode = "409",
+                description = "Cannot modify job type", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))})
+    public Mono<ResponseEntity<Object>> putIndividualInfoJob( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String jobId, //
+        @Parameter(
+            name = ConsumerConsts.PERFORM_TYPE_CHECK_PARAM,
+            required = false, //
+            description = ConsumerConsts.PERFORM_TYPE_CHECK_PARAM_DESCRIPTION) //
+        @RequestParam(
+            name = ConsumerConsts.PERFORM_TYPE_CHECK_PARAM,
+            required = false,
+            defaultValue = "false") boolean performTypeCheck,
+        @RequestBody ConsumerJobInfo informationJobObject) {
+
+        final boolean isNewJob = this.infoJobs.get(jobId) == null;
+
+        return validatePutInfoJob(jobId, informationJobObject, performTypeCheck) //
+            .flatMap(this::startInfoSubscriptionJob) //
+            .doOnNext(this.infoJobs::put) //
+            .map(newEiJob -> new ResponseEntity<>(isNewJob ? HttpStatus.CREATED : HttpStatus.OK)) //
+            .onErrorResume(throwable -> Mono.just(ErrorResponse.create(throwable, HttpStatus.NOT_FOUND)));
+    }
+
+    @GetMapping(path = "/info-type-subscription", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Information type subscription identifiers",
+        description = "query for information type subscription identifiers")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information type subscription identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))),})
+    public ResponseEntity<Object> getInfoTypeSubscriptions( //
+
+        @Parameter(
+            name = ConsumerConsts.OWNER_PARAM,
+            required = false, //
+            description = ConsumerConsts.OWNER_PARAM_DESCRIPTION) //
+        @RequestParam(name = ConsumerConsts.OWNER_PARAM, required = false) String owner) {
+        try {
+            List<String> result = new ArrayList<>();
+            if (owner != null) {
+                this.infoTypeSubscriptions.getSubscriptionsForOwner(owner)
+                    .forEach(subscription -> result.add(subscription.getId()));
+            } else {
+                this.infoTypeSubscriptions.getAllSubscriptions()
+                    .forEach(subscription -> result.add(subscription.getId()));
+            }
+            return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(path = "/info-type-subscription/{subscriptionId}", produces = MediaType.APPLICATION_JSON_VALUE) //
+    @Operation(summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION, description = "") //
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Type subscription", //
+                content = @Content(schema = @Schema(implementation = ConsumerTypeSubscriptionInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Subscription is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getIndividualTypeSubscription( //
+        @PathVariable(ConsumerConsts.SUBSCRIPTION_ID_PATH) String subscriptionId) {
+        try {
+            InfoTypeSubscriptions.SubscriptionInfo subscription =
+                this.infoTypeSubscriptions.getSubscription(subscriptionId);
+            return new ResponseEntity<>(gson.toJson(toTypeSuscriptionInfo(subscription)), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PutMapping(
+        path = "/info-type-subscription/{subscriptionId}", //
+        produces = MediaType.APPLICATION_JSON_VALUE, //
+        consumes = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION,
+        description = ConsumerConsts.TYPE_SUBSCRIPTION_DESCRIPTION)
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "201",
+                description = "Subscription created", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Subscription updated", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public Mono<ResponseEntity<Object>> putIndividualTypeSubscription( //
+        @PathVariable(ConsumerConsts.SUBSCRIPTION_ID_PATH) String subscriptionId, //
+        @RequestBody ConsumerTypeSubscriptionInfo subscription) {
+
+        final boolean isNewSubscription = this.infoTypeSubscriptions.get(subscriptionId) == null;
+        this.infoTypeSubscriptions.put(toTypeSuscriptionInfo(subscription, subscriptionId));
+        return Mono.just(new ResponseEntity<>(isNewSubscription ? HttpStatus.CREATED : HttpStatus.OK));
+    }
+
+    @DeleteMapping(path = "/info-type-subscription/{subscriptionId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = ConsumerConsts.INDIVIDUAL_TYPE_SUBSCRIPTION, description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Not used", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "204",
+                description = "Subscription deleted", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "404",
+                description = "Subscription is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> deleteIndividualTypeSubscription( //
+        @PathVariable(ConsumerConsts.SUBSCRIPTION_ID_PATH) String subscriptionId) {
+        try {
+            InfoTypeSubscriptions.SubscriptionInfo subscription =
+                this.infoTypeSubscriptions.getSubscription(subscriptionId);
+            this.infoTypeSubscriptions.remove(subscription);
+            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    private ConsumerTypeSubscriptionInfo toTypeSuscriptionInfo(InfoTypeSubscriptions.SubscriptionInfo s) {
+        return new ConsumerTypeSubscriptionInfo(s.getCallbackUrl(), s.getOwner());
+    }
+
+    private InfoTypeSubscriptions.SubscriptionInfo toTypeSuscriptionInfo(ConsumerTypeSubscriptionInfo s,
+        String subscriptionId) {
+        return InfoTypeSubscriptions.SubscriptionInfo.builder() //
+            .apiVersion(ConsumerCallbacks.API_VERSION) //
+            .owner(s.owner) //
+            .id(subscriptionId) //
+            .callbackUrl(s.statusResultUri).build();
+    }
+
+    private Mono<InfoJob> startInfoSubscriptionJob(InfoJob newInfoJob) {
+        return this.producerCallbacks.startInfoSubscriptionJob(newInfoJob, infoProducers) //
+            .doOnNext(noOfAcceptingProducers -> this.logger.debug("Started job {}, number of activated producers: {}",
+                newInfoJob.getId(), noOfAcceptingProducers)) //
+            .map(noOfAcceptingProducers -> newInfoJob);
+    }
+
+    private Mono<InfoJob> validatePutInfoJob(String jobId, ConsumerJobInfo jobInfo, boolean performTypeCheck) {
+        try {
+            if (performTypeCheck) {
+                InfoType infoType = this.infoTypes.getType(jobInfo.infoTypeId);
+                validateJsonObjectAgainstSchema(infoType.getJobDataSchema(), jobInfo.jobDefinition);
+            }
+            InfoJob existingEiJob = this.infoJobs.get(jobId);
+            validateUri(jobInfo.statusNotificationUri);
+            validateUri(jobInfo.jobResultUri);
+
+            if (existingEiJob != null && !existingEiJob.getTypeId().equals(jobInfo.infoTypeId)) {
+                throw new ServiceException("Not allowed to change type for existing job", HttpStatus.CONFLICT);
+            }
+            return Mono.just(toEiJob(jobInfo, jobId, jobInfo.infoTypeId));
+        } catch (Exception e) {
+            return Mono.error(e);
+        }
+    }
+
+    private void validateUri(String url) throws URISyntaxException, ServiceException {
+        if (url != null && !url.isEmpty()) {
+            URI uri = new URI(url);
+            if (!uri.isAbsolute()) {
+                throw new ServiceException("URI: " + url + " is not absolute", HttpStatus.BAD_REQUEST);
+            }
+        }
+    }
+
+    private void validateJsonObjectAgainstSchema(Object schemaObj, Object object) throws ServiceException {
+        if (schemaObj != null) { // schema is optional for now
+            try {
+                ObjectMapper mapper = new ObjectMapper();
+
+                String schemaAsString = mapper.writeValueAsString(schemaObj);
+                JSONObject schemaJSON = new JSONObject(schemaAsString);
+                var schema = org.everit.json.schema.loader.SchemaLoader.load(schemaJSON);
+
+                String objectAsString = mapper.writeValueAsString(object);
+                JSONObject json = new JSONObject(objectAsString);
+                schema.validate(json);
+            } catch (Exception e) {
+                throw new ServiceException("Json validation failure " + e.toString(), HttpStatus.BAD_REQUEST);
+            }
+        }
+    }
+
+    private InfoJob toEiJob(ConsumerJobInfo info, String id, String typeId) {
+        return InfoJob.builder() //
+            .id(id) //
+            .typeId(typeId) //
+            .owner(info.owner) //
+            .jobData(info.jobDefinition) //
+            .targetUrl(info.jobResultUri) //
+            .jobStatusUrl(info.statusNotificationUri == null ? "" : info.statusNotificationUri) //
+            .build();
+    }
+
+    private ConsumerInfoTypeInfo toInfoTypeInfo(InfoType type) {
+        return new ConsumerInfoTypeInfo(type.getJobDataSchema(), typeStatus(type),
+            this.infoProducers.getProducerIdsForType(type.getId()).size());
+    }
+
+    private ConsumerInfoTypeInfo.ConsumerTypeStatusValues typeStatus(InfoType type) {
+        for (InfoProducer producer : this.infoProducers.getProducersForType(type)) {
+            if (producer.isAvailable()) {
+                return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.ENABLED;
+            }
+        }
+        return ConsumerInfoTypeInfo.ConsumerTypeStatusValues.DISABLED;
+    }
+
+    private ConsumerJobInfo toInfoJobInfo(InfoJob s) {
+        return new ConsumerJobInfo(s.getTypeId(), s.getJobData(), s.getOwner(), s.getTargetUrl(), s.getJobStatusUrl());
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerInfoTypeInfo.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerInfoTypeInfo.java
new file mode 100644 (file)
index 0000000..682c62b
--- /dev/null
@@ -0,0 +1,68 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2021 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.controllers.r1consumer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "consumer_information_type", description = "Information for an Information type")
+public class ConsumerInfoTypeInfo {
+
+    @Schema(name = "job_data_schema", description = "Json schema for the job data", required = true)
+    @SerializedName("job_data_schema")
+    @JsonProperty(value = "job_data_schema", required = true)
+    public Object jobDataSchema;
+
+    @Gson.TypeAdapters
+    @Schema(name = "consumer_type_status_values", description = STATUS_DESCRIPTION)
+    public enum ConsumerTypeStatusValues {
+        ENABLED, DISABLED
+    }
+
+    private static final String STATUS_DESCRIPTION = "Allowed values: <br/>" //
+        + "ENABLED: one or several producers for the information type are available <br/>" //
+        + "DISABLED: no producers for the information type are available";
+
+    @Schema(name = "type_status", description = STATUS_DESCRIPTION, required = true)
+    @SerializedName("type_status")
+    @JsonProperty(value = "type_status", required = true)
+    public ConsumerTypeStatusValues state;
+
+    @Schema(name = "no_of_producers", description = "The number of registered producers for the type", required = true)
+    @SerializedName("no_of_producers")
+    @JsonProperty(value = "no_of_producers", required = true)
+    public int noOfProducers;
+
+    public ConsumerInfoTypeInfo(Object jobDataSchema, ConsumerTypeStatusValues state, int noOfProducers) {
+        this.jobDataSchema = jobDataSchema;
+        this.state = state;
+        this.noOfProducers = noOfProducers;
+    }
+
+    public ConsumerInfoTypeInfo() {
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerJobInfo.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerJobInfo.java
new file mode 100644 (file)
index 0000000..8dca3c7
--- /dev/null
@@ -0,0 +1,76 @@
+/*-
+ * ========================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.ics.controllers.r1consumer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "consumer_job", description = "Information for an Information Job")
+public class ConsumerJobInfo {
+
+    @Schema(
+        name = "info_type_id",
+        description = "Information type Idenitifier of the subscription job",
+        required = true)
+    @SerializedName("info_type_id")
+    @JsonProperty(value = "info_type_id", required = true)
+    public String infoTypeId = "";
+
+    @Schema(name = "job_owner", description = "Identity of the owner of the job", required = true)
+    @SerializedName("job_owner")
+    @JsonProperty(value = "job_owner", required = true)
+    public String owner = "";
+
+    @Schema(name = "job_definition", description = "Information type specific job data", required = true)
+    @SerializedName("job_definition")
+    @JsonProperty(value = "job_definition", required = true)
+    public Object jobDefinition;
+
+    @Schema(name = "job_result_uri", description = "The target URI of the subscribed information", required = true)
+    @SerializedName("job_result_uri")
+    @JsonProperty(value = "job_result_uri", required = true)
+    public String jobResultUri = "";
+
+    @Schema(
+        name = "status_notification_uri",
+        description = "The target of Information subscription job status notifications",
+        required = false)
+    @SerializedName("status_notification_uri")
+    @JsonProperty(value = "status_notification_uri", required = false)
+    public String statusNotificationUri = "";
+
+    public ConsumerJobInfo() {
+    }
+
+    public ConsumerJobInfo(String infoTypeId, Object jobData, String owner, String targetUri,
+        String statusNotificationUri) {
+        this.infoTypeId = infoTypeId;
+        this.jobDefinition = jobData;
+        this.owner = owner;
+        this.jobResultUri = targetUri;
+        this.statusNotificationUri = statusNotificationUri;
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerJobStatus.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerJobStatus.java
new file mode 100644 (file)
index 0000000..12d05ed
--- /dev/null
@@ -0,0 +1,66 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.controllers.r1consumer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Collection;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "consumer_job_status", description = "Status for an Information Job")
+public class ConsumerJobStatus {
+
+    @Gson.TypeAdapters
+    @Schema(name = "info_job_status_values", description = OPERATIONAL_STATE_DESCRIPTION)
+    public enum InfoJobStatusValues {
+        ENABLED, DISABLED
+    }
+
+    private static final String OPERATIONAL_STATE_DESCRIPTION = "Allowed values: <br/>" //
+        + "ENABLED: the A1-Information producer is able to deliver result for the Information Job <br/>" //
+        + "DISABLED: the A1-Information producer is unable to deliver result for the Information Job";
+
+    private static final String PRODUCERS_DESCRIPTION = "An array of all registered Information Producer Identifiers.";
+
+    @Schema(name = "info_job_status", description = OPERATIONAL_STATE_DESCRIPTION, required = true)
+    @SerializedName("info_job_status")
+    @JsonProperty(value = "info_job_status", required = true)
+    public InfoJobStatusValues state;
+
+    @Schema(name = "producers", description = PRODUCERS_DESCRIPTION, required = true)
+    @SerializedName("producers")
+    @JsonProperty(value = "producers", required = true)
+    public Collection<String> producers;
+
+    public ConsumerJobStatus() {
+    }
+
+    public ConsumerJobStatus(InfoJobStatusValues state, Collection<String> producers) {
+        this.state = state;
+        this.producers = producers;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerTypeRegistrationInfo.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerTypeRegistrationInfo.java
new file mode 100644 (file)
index 0000000..73263ec
--- /dev/null
@@ -0,0 +1,68 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2021 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.controllers.r1consumer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "consumer_type_registration_info", description = "Information for an Information type")
+public class ConsumerTypeRegistrationInfo {
+
+    @Schema(name = "info_type_id", description = "Information type identifier", required = true)
+    @SerializedName("info_type_id")
+    @JsonProperty(value = "info_type_id", required = true)
+    public String infoTypeId;
+
+    @Schema(name = "job_data_schema", description = "Json schema for the job data", required = true)
+    @SerializedName("job_data_schema")
+    @JsonProperty(value = "job_data_schema", required = true)
+    public Object jobDataSchema;
+
+    @Gson.TypeAdapters
+    @Schema(name = "consumer_type_registration_values", description = REGISTRATION_DESCRIPTION)
+    public enum ConsumerTypeStatusValues {
+        REGISTERED, DEREGISTERED
+    }
+
+    private static final String REGISTRATION_DESCRIPTION = "Allowed values: <br/>" //
+        + "REGISTERED: the information type has been registered <br/>" //
+        + "DEREGISTERED: the information type has been removed";
+
+    @Schema(name = "status", description = REGISTRATION_DESCRIPTION, required = true)
+    @SerializedName("status")
+    @JsonProperty(value = "status", required = true)
+    public ConsumerTypeStatusValues state;
+
+    public ConsumerTypeRegistrationInfo(Object jobDataSchema, ConsumerTypeStatusValues state, String infoTypeId) {
+        this.jobDataSchema = jobDataSchema;
+        this.state = state;
+        this.infoTypeId = infoTypeId;
+    }
+
+    public ConsumerTypeRegistrationInfo() {
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerTypeSubscriptionInfo.java b/src/main/java/org/oransc/ics/controllers/r1consumer/ConsumerTypeSubscriptionInfo.java
new file mode 100644 (file)
index 0000000..d420c57
--- /dev/null
@@ -0,0 +1,53 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2021 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.controllers.r1consumer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.EqualsAndHashCode;
+
+import org.immutables.gson.Gson;
+
+@EqualsAndHashCode
+@Gson.TypeAdapters
+@Schema(name = "consumer_type_subscription_info", description = "Information for an information type subscription")
+public class ConsumerTypeSubscriptionInfo {
+
+    @Schema(name = "status_result_uri", description = "The target URI of the subscribed information", required = true)
+    @SerializedName("status_result_uri")
+    @JsonProperty(value = "status_result_uri", required = true)
+    public String statusResultUri = "";
+
+    @Schema(name = "owner", description = "Identity of the owner of the subscription", required = true)
+    @SerializedName("owner")
+    @JsonProperty(value = "owner", required = true)
+    public String owner = "";
+
+    public ConsumerTypeSubscriptionInfo() {
+    }
+
+    public ConsumerTypeSubscriptionInfo(String statusResultUri, String owner) {
+        this.statusResultUri = statusResultUri;
+        this.owner = owner;
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerCallbacks.java
new file mode 100644 (file)
index 0000000..0881cd7
--- /dev/null
@@ -0,0 +1,127 @@
+/*-
+ * ========================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.ics.controllers.r1producer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.lang.invoke.MethodHandles;
+import java.time.Duration;
+import java.util.Collection;
+
+import org.oransc.ics.clients.AsyncRestClient;
+import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.repository.InfoJob;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducer;
+import org.oransc.ics.repository.InfoProducers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.util.retry.Retry;
+
+/**
+ * Callbacks to the Producer
+ */
+@SuppressWarnings("java:S3457") // No need to call "toString()" method as formatting and string ..
+public class ProducerCallbacks {
+
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private static Gson gson = new GsonBuilder().create();
+
+    private final AsyncRestClient restClient;
+
+    public ProducerCallbacks(ApplicationConfig config, SecurityContext securityContext) {
+        AsyncRestClientFactory restClientFactory =
+            new AsyncRestClientFactory(config.getWebClientConfig(), securityContext);
+        this.restClient = restClientFactory.createRestClientNoHttpProxy("");
+    }
+
+    public Mono<String> healthCheck(InfoProducer producer) {
+        return restClient.get(producer.getProducerSupervisionCallbackUrl());
+    }
+
+    public void stopInfoJob(InfoJob infoJob, InfoProducers infoProducers) {
+        for (InfoProducer producer : getProducersForJob(infoJob, infoProducers)) {
+            String url = producer.getJobCallbackUrl() + "/" + infoJob.getId();
+            producer.setJobDisabled(infoJob);
+            restClient.delete(url) //
+                .subscribe(response -> logger.debug("Producer job deleted OK {}", producer.getId()), //
+                    throwable -> logger.warn("Producer job delete failed {} {}", producer.getId(),
+                        throwable.getMessage()),
+                    null);
+        }
+    }
+
+    /**
+     * Start a job in all producers that suports the job type
+     *
+     * @param infoJob an Information Job
+     * @return the number of producers that returned OK
+     */
+    public Mono<Integer> startInfoSubscriptionJob(InfoJob infoJob, InfoProducers infoProducers) {
+        Retry retrySpec = Retry.fixedDelay(1, Duration.ofSeconds(1));
+        return Flux.fromIterable(getProducersForJob(infoJob, infoProducers)) //
+            .flatMap(infoProducer -> startInfoJob(infoProducer, infoJob, retrySpec)) //
+            .collectList() //
+            .map(okResponses -> Integer.valueOf(okResponses.size())); //
+    }
+
+    /**
+     * Start all jobs for one producer
+     *
+     * @param producer
+     * @param infoJobs
+     */
+    public Flux<String> startInfoJobs(InfoProducer producer, InfoJobs infoJobs) {
+        final int maxNoOfParalellRequests = 10;
+        Retry retrySpec = Retry.backoff(3, Duration.ofSeconds(1));
+
+        return Flux.fromIterable(producer.getInfoTypes()) //
+            .flatMap(type -> Flux.fromIterable(infoJobs.getJobsForType(type))) //
+            .flatMap(job -> startInfoJob(producer, job, retrySpec), maxNoOfParalellRequests);
+    }
+
+    public Mono<String> startInfoJob(InfoProducer producer, InfoJob infoJob, Retry retrySpec) {
+        ProducerJobInfo request = new ProducerJobInfo(infoJob);
+        String body = gson.toJson(request);
+
+        return restClient.post(producer.getJobCallbackUrl(), body) //
+            .retryWhen(retrySpec) //
+            .doOnNext(resp -> logger.debug("Job subscription {} started OK {}", infoJob.getId(), producer.getId())) //
+            .onErrorResume(throwable -> {
+                producer.setJobDisabled(infoJob);
+                logger.warn("Job subscription failed id: {} url: {}, reason: {}", producer.getId(),
+                    producer.getJobCallbackUrl(), throwable.toString());
+                return Mono.empty();
+            }) //
+            .doOnNext(resp -> producer.setJobEnabled(infoJob));
+    }
+
+    private Collection<InfoProducer> getProducersForJob(InfoJob infoJob, InfoProducers infoProducers) {
+        return infoProducers.getProducersForType(infoJob.getTypeId());
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerConsts.java
new file mode 100644 (file)
index 0000000..e3f667a
--- /dev/null
@@ -0,0 +1,41 @@
+/*-
+ * ========================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.ics.controllers.r1producer;
+
+import org.oransc.ics.controllers.r1consumer.ConsumerConsts;
+
+public class ProducerConsts {
+    public static final String PRODUCER_API_NAME = "Data producer (registration)";
+    public static final String API_ROOT = "/data-producer/v1";
+    public static final String PRODUCER_API_DESCRIPTION = "API for data producers";
+
+    public static final String PRODUCER_API_CALLBACKS_NAME = "Data producer (callbacks)";
+    public static final String PRODUCER_API_CALLBACKS_DESCRIPTION = "API implemented by data producers";
+
+    public static final String INFO_TYPE_ID_PARAM = ConsumerConsts.INFO_TYPE_ID_PARAM;
+    public static final String INFO_TYPE_ID_PATH = ConsumerConsts.INFO_TYPE_ID_PATH;
+
+    public static final String INFO_PRODUCER_ID_PATH = "infoProducerId";
+
+    private ProducerConsts() {
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerController.java
new file mode 100644 (file)
index 0000000..0372248
--- /dev/null
@@ -0,0 +1,425 @@
+/*-
+ * ========================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.ics.controllers.r1producer;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.oransc.ics.controllers.ErrorResponse;
+import org.oransc.ics.controllers.VoidResponse;
+import org.oransc.ics.exceptions.ServiceException;
+import org.oransc.ics.repository.InfoJob;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducer;
+import org.oransc.ics.repository.InfoProducers;
+import org.oransc.ics.repository.InfoType;
+import org.oransc.ics.repository.InfoTypeSubscriptions;
+import org.oransc.ics.repository.InfoTypes;
+import org.springframework.beans.factory.annotation.Autowired;
+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.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
+@RestController("Producer registry")
+@Tag(name = ProducerConsts.PRODUCER_API_NAME, description = ProducerConsts.PRODUCER_API_DESCRIPTION)
+public class ProducerController {
+
+    private static Gson gson = new GsonBuilder().create();
+
+    @Autowired
+    private InfoJobs infoJobs;
+
+    @Autowired
+    private InfoTypes infoTypes;
+
+    @Autowired
+    private InfoProducers infoProducers;
+
+    @Autowired
+    private InfoTypeSubscriptions typeSubscriptions;
+
+    @GetMapping(path = ProducerConsts.API_ROOT + "/info-types", produces = MediaType.APPLICATION_JSON_VALUE) //
+    @Operation(summary = "Info Type identifiers", description = "") //
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Info Type identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))) //
+        })
+    public ResponseEntity<Object> getInfoTypdentifiers( //
+    ) {
+        List<String> result = new ArrayList<>();
+        for (InfoType infoType : this.infoTypes.getAllInfoTypes()) {
+            result.add(infoType.getId());
+        }
+
+        return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+    }
+
+    @GetMapping(
+        path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual Information Type", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Info Type", //
+                content = @Content(schema = @Schema(implementation = ProducerInfoTypeInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))})
+    public ResponseEntity<Object> getInfoType( //
+        @PathVariable(ProducerConsts.INFO_TYPE_ID_PATH) String infoTypeId) {
+        try {
+            InfoType t = this.infoTypes.getType(infoTypeId);
+            ProducerInfoTypeInfo info = toInfoTypeInfo(t);
+            return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PutMapping(
+        path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Type updated", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "201",
+                description = "Type created", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "400",
+                description = "Input validation failed", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))})
+    @Operation(summary = "Individual Information Type", description = "")
+    public ResponseEntity<Object> putInfoType( //
+        @PathVariable(ProducerConsts.INFO_TYPE_ID_PATH) String infoTypeId, //
+        @RequestBody ProducerInfoTypeInfo registrationInfo) {
+
+        InfoType previousDefinition = this.infoTypes.get(infoTypeId);
+        if (registrationInfo.jobDataSchema == null) {
+            return ErrorResponse.create("No schema provided", HttpStatus.BAD_REQUEST);
+        }
+        InfoType newDefinition =
+            new InfoType(infoTypeId, registrationInfo.jobDataSchema, registrationInfo.typeSpecificInformation);
+        this.infoTypes.put(newDefinition);
+        this.typeSubscriptions.notifyTypeRegistered(newDefinition);
+        return new ResponseEntity<>(previousDefinition == null ? HttpStatus.CREATED : HttpStatus.OK);
+    }
+
+    @DeleteMapping(
+        path = ProducerConsts.API_ROOT + "/info-types/{infoTypeId}",
+        produces = MediaType.APPLICATION_JSON_VALUE) //
+    @Operation(summary = "Individual Information Type", description = "") //
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Not used", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "204",
+                description = "Producer deleted", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information type is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
+            @ApiResponse(
+                responseCode = "409",
+                description = "The Information type has one or several active producers", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> deleteInfoType( //
+        @PathVariable(ProducerConsts.INFO_TYPE_ID_PATH) String infoTypeId) {
+
+        InfoType type = this.infoTypes.get(infoTypeId);
+        if (type == null) {
+            return ErrorResponse.create("Information type not found", HttpStatus.NOT_FOUND);
+        }
+        if (!this.infoProducers.getProducersForType(type).isEmpty()) {
+            String firstProducerId = this.infoProducers.getProducersForType(type).iterator().next().getId();
+            return ErrorResponse.create("The type has active producers: " + firstProducerId, HttpStatus.CONFLICT);
+        }
+        this.infoTypes.remove(type);
+        infoJobs.getJobsForType(type).forEach(job -> infoJobs.remove(job, infoProducers)); // Delete jobs for the type
+        this.typeSubscriptions.notifyTypeRemoved(type);
+        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+    }
+
+    @GetMapping(path = ProducerConsts.API_ROOT + "/info-producers", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Information producer identifiers", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information producer identifiers", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))) //
+        })
+    public ResponseEntity<Object> getInfoProducerIdentifiers( //
+        @Parameter(
+            name = ProducerConsts.INFO_TYPE_ID_PARAM,
+            required = false,
+            description = "If given, only the producers for the EI Data type is returned.") //
+        @RequestParam(name = ProducerConsts.INFO_TYPE_ID_PARAM, required = false) String typeId //
+    ) {
+        List<String> result = new ArrayList<>();
+        for (InfoProducer infoProducer : typeId == null ? this.infoProducers.getAllProducers()
+            : this.infoProducers.getProducersForType(typeId)) {
+            result.add(infoProducer.getId());
+        }
+
+        return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK);
+    }
+
+    @GetMapping(
+        path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual Information Producer", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information producer", //
+                content = @Content(schema = @Schema(implementation = ProducerRegistrationInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information producer is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class)))//
+        })
+    public ResponseEntity<Object> getInfoProducer( //
+        @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
+        try {
+            InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
+            ProducerRegistrationInfo info = toProducerRegistrationInfo(producer);
+            return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(
+        path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}/info-jobs",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Information Job definitions",
+        description = "Information Job definitions for one Information Producer")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information producer is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information producer", //
+                content = @Content(array = @ArraySchema(schema = @Schema(implementation = ProducerJobInfo.class)))), //
+        })
+    public ResponseEntity<Object> getInfoProducerJobs( //
+        @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
+        try {
+            InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
+            Collection<ProducerJobInfo> producerJobs = new ArrayList<>();
+            for (InfoType type : producer.getInfoTypes()) {
+                for (InfoJob infoJob : this.infoJobs.getJobsForType(type)) {
+                    ProducerJobInfo request = new ProducerJobInfo(infoJob);
+                    producerJobs.add(request);
+                }
+            }
+
+            return new ResponseEntity<>(gson.toJson(producerJobs), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @GetMapping(
+        path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}/status",
+        produces = MediaType.APPLICATION_JSON_VALUE) //
+    @Operation(summary = "Information producer status") //
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Information producer status", //
+                content = @Content(schema = @Schema(implementation = ProducerStatusInfo.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Information producer is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> getInfoProducerStatus( //
+        @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
+        try {
+            InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
+            return new ResponseEntity<>(gson.toJson(producerStatusInfo(producer)), HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    private ProducerStatusInfo producerStatusInfo(InfoProducer producer) {
+        var opState = producer.isAvailable() ? ProducerStatusInfo.OperationalState.ENABLED
+            : ProducerStatusInfo.OperationalState.DISABLED;
+        return new ProducerStatusInfo(opState);
+    }
+
+    @PutMapping(
+        path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}", //
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual Information Producer", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "201",
+                description = "Producer created", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Producer updated", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))), //
+            @ApiResponse(
+                responseCode = "404",
+                description = "Producer type not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))), //
+            @ApiResponse(
+                responseCode = "400",
+                description = "Input validation failed", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> putInfoProducer( //
+        @PathVariable("infoProducerId") String infoProducerId, //
+        @RequestBody ProducerRegistrationInfo registrationInfo) {
+        try {
+            validateUri(registrationInfo.jobCallbackUrl);
+            validateUri(registrationInfo.producerSupervisionCallbackUrl);
+            InfoProducer previousDefinition = this.infoProducers.get(infoProducerId);
+            this.infoProducers.registerProducer(toProducerRegistrationInfo(infoProducerId, registrationInfo));
+            return new ResponseEntity<>(previousDefinition == null ? HttpStatus.CREATED : HttpStatus.OK);
+        } catch (ServiceException e) {
+            return ErrorResponse.create(e, e.getHttpStatus());
+        }
+    }
+
+    private void validateUri(String url) throws ServiceException {
+        if (url != null && !url.isEmpty()) {
+            try {
+                URI uri = new URI(url);
+                if (!uri.isAbsolute()) {
+                    throw new ServiceException("URI: " + url + " is not absolute", HttpStatus.BAD_REQUEST);
+                }
+            } catch (URISyntaxException e) {
+                throw new ServiceException(e.getMessage(), HttpStatus.BAD_REQUEST);
+            }
+        } else {
+            throw new ServiceException("Missing required URL", HttpStatus.BAD_REQUEST);
+        }
+    }
+
+    @DeleteMapping(
+        path = ProducerConsts.API_ROOT + "/info-producers/{infoProducerId}",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Individual Information Producer", description = "")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "Not used", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "204",
+                description = "Producer deleted", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))),
+            @ApiResponse(
+                responseCode = "404",
+                description = "Producer is not found", //
+                content = @Content(schema = @Schema(implementation = ErrorResponse.ErrorInfo.class))) //
+        })
+    public ResponseEntity<Object> deleteInfoProducer(
+        @PathVariable(ProducerConsts.INFO_PRODUCER_ID_PATH) String infoProducerId) {
+        try {
+            final InfoProducer producer = this.infoProducers.getProducer(infoProducerId);
+            this.infoProducers.deregisterProducer(producer);
+            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+        } catch (ServiceException e) {
+            return ErrorResponse.create(e, e.getHttpStatus());
+        }
+    }
+
+    private ProducerRegistrationInfo toProducerRegistrationInfo(InfoProducer p) {
+        Collection<String> types = new ArrayList<>();
+        for (InfoType type : p.getInfoTypes()) {
+            types.add(type.getId());
+        }
+        return new ProducerRegistrationInfo(types, p.getJobCallbackUrl(), p.getProducerSupervisionCallbackUrl());
+    }
+
+    private ProducerInfoTypeInfo toInfoTypeInfo(InfoType t) {
+        return new ProducerInfoTypeInfo(t.getJobDataSchema(), t.getTypeSpecificInfo());
+    }
+
+    private InfoProducers.InfoProducerRegistrationInfo toProducerRegistrationInfo(String infoProducerId,
+        ProducerRegistrationInfo info) throws ServiceException {
+        Collection<InfoType> supportedTypes = new ArrayList<>();
+        for (String typeId : info.supportedTypeIds) {
+            InfoType type = this.infoTypes.getType(typeId);
+            supportedTypes.add(type);
+        }
+
+        return InfoProducers.InfoProducerRegistrationInfo.builder() //
+            .id(infoProducerId) //
+            .jobCallbackUrl(info.jobCallbackUrl) //
+            .producerSupervisionCallbackUrl(info.producerSupervisionCallbackUrl) //
+            .supportedTypes(supportedTypes) //
+            .build();
+    }
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerInfoTypeInfo.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerInfoTypeInfo.java
new file mode 100644 (file)
index 0000000..64f17f9
--- /dev/null
@@ -0,0 +1,55 @@
+/*-
+ * ========================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.ics.controllers.r1producer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "producer_info_type_info", description = "Information for an Information Type")
+public class ProducerInfoTypeInfo {
+
+    @Schema(name = "info_job_data_schema", description = "Json schema for the job data", required = true)
+    @SerializedName("info_job_data_schema")
+    @JsonProperty(value = "info_job_data_schema", required = true)
+    public Object jobDataSchema;
+
+    @Schema(
+        name = "info_type_information",
+        description = "Type specific information for the information type",
+        required = false)
+    @SerializedName("info_type_information")
+    @JsonProperty(value = "info_type_information", required = false)
+    public Object typeSpecificInformation;
+
+    public ProducerInfoTypeInfo(Object jobDataSchema, Object typeSpecificInformation) {
+        this.jobDataSchema = jobDataSchema;
+        this.typeSpecificInformation = typeSpecificInformation;
+    }
+
+    public ProducerInfoTypeInfo() {
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerJobInfo.java
new file mode 100644 (file)
index 0000000..8b42ea8
--- /dev/null
@@ -0,0 +1,84 @@
+/*-
+ * ========================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.ics.controllers.r1producer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+import org.oransc.ics.repository.InfoJob;
+
+@Gson.TypeAdapters
+@Schema(
+    name = "producer_info_job_request",
+    description = "The body of the Information Producer callbacks for Information Job creation and deletion")
+public class ProducerJobInfo {
+
+    @Schema(name = "info_job_identity", description = "Identity of the Information Job", required = true)
+    @SerializedName("info_job_identity")
+    @JsonProperty("info_job_identity")
+    public String id = "";
+
+    @Schema(name = "info_type_identity", description = "Type identity for the job")
+    @SerializedName("info_type_identity")
+    @JsonProperty("info_type_identity")
+    public String typeId = "";
+
+    @Schema(name = "info_job_data", description = "Json for the job data")
+    @SerializedName("info_job_data")
+    @JsonProperty("info_job_data")
+    public Object jobData;
+
+    @Schema(name = "target_uri", description = "URI for the target of the produced Information")
+    @SerializedName("target_uri")
+    @JsonProperty("target_uri")
+    public String targetUri = "";
+
+    @Schema(name = "owner", description = "The owner of the job")
+    @SerializedName("owner")
+    @JsonProperty("owner")
+    public String owner = "";
+
+    @Schema(name = "last_updated", description = "The time when the job was last updated or created (ISO-8601)")
+    @SerializedName("last_updated")
+    @JsonProperty("last_updated")
+    public String lastUpdated = "";
+
+    public ProducerJobInfo(Object jobData, String id, String typeId, String targetUri, String owner,
+        String lastUpdated) {
+        this.id = id;
+        this.jobData = jobData;
+        this.typeId = typeId;
+        this.targetUri = targetUri;
+        this.owner = owner;
+        this.lastUpdated = lastUpdated;
+    }
+
+    public ProducerJobInfo(InfoJob job) {
+        this(job.getJobData(), job.getId(), job.getTypeId(), job.getTargetUrl(), job.getOwner(), job.getLastUpdated());
+    }
+
+    public ProducerJobInfo() {
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerRegistrationInfo.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerRegistrationInfo.java
new file mode 100644 (file)
index 0000000..2e395dc
--- /dev/null
@@ -0,0 +1,64 @@
+/*-
+ * ========================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.ics.controllers.r1producer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Collection;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "producer_registration_info", description = "Information for an Information Producer")
+public class ProducerRegistrationInfo {
+
+    @Schema(name = "supported_info_types", description = "Supported Information Type IDs", required = true)
+    @SerializedName("supported_info_types")
+    @JsonProperty(value = "supported_info_types", required = true)
+    public Collection<String> supportedTypeIds;
+
+    @Schema(name = "info_job_callback_url", description = "callback for Information Job", required = true)
+    @SerializedName("info_job_callback_url")
+    @JsonProperty(value = "info_job_callback_url", required = true)
+    public String jobCallbackUrl = "";
+
+    @Schema(
+        name = "info_producer_supervision_callback_url",
+        description = "callback for producer supervision",
+        required = true)
+    @SerializedName("info_producer_supervision_callback_url")
+    @JsonProperty(value = "info_producer_supervision_callback_url", required = true)
+    public String producerSupervisionCallbackUrl = "";
+
+    public ProducerRegistrationInfo(Collection<String> types, String jobCallbackUrl,
+        String producerSupervisionCallbackUrl) {
+        this.supportedTypeIds = types;
+        this.jobCallbackUrl = jobCallbackUrl;
+        this.producerSupervisionCallbackUrl = producerSupervisionCallbackUrl;
+    }
+
+    public ProducerRegistrationInfo() {
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/controllers/r1producer/ProducerStatusInfo.java b/src/main/java/org/oransc/ics/controllers/r1producer/ProducerStatusInfo.java
new file mode 100644 (file)
index 0000000..f4c49cd
--- /dev/null
@@ -0,0 +1,53 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.controllers.r1producer;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.gson.annotations.SerializedName;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import org.immutables.gson.Gson;
+
+@Gson.TypeAdapters
+@Schema(name = "producer_status", description = "Status for an Info Producer")
+public class ProducerStatusInfo {
+
+    @Gson.TypeAdapters
+    @Schema(name = "producer_operational_state", description = "Represents the operational states")
+    public enum OperationalState {
+        ENABLED, DISABLED
+    }
+
+    private static final String OPERATIONAL_STATE_DESCRIPTION = "Operational state, values:\n" //
+        + "ENABLED: the producer is operational\n" //
+        + "DISABLED: the producer is not operational";
+
+    @Schema(name = "operational_state", description = OPERATIONAL_STATE_DESCRIPTION, required = true)
+    @SerializedName("operational_state")
+    @JsonProperty(value = "operational_state", required = true)
+    public final OperationalState opState;
+
+    public ProducerStatusInfo(OperationalState state) {
+        this.opState = state;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/exceptions/ServiceException.java b/src/main/java/org/oransc/ics/exceptions/ServiceException.java
new file mode 100644 (file)
index 0000000..e6b0cc8
--- /dev/null
@@ -0,0 +1,40 @@
+/*-
+ * ============LICENSE_START======================================================================
+ * Copyright (C) 2019 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.
+ * ============LICENSE_END========================================================================
+ */
+
+package org.oransc.ics.exceptions;
+
+import lombok.Getter;
+
+import org.springframework.http.HttpStatus;
+
+public class ServiceException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+    @Getter
+    private final HttpStatus httpStatus;
+
+    public ServiceException(String message, Exception originalException) {
+        super(message, originalException);
+        this.httpStatus = null;
+    }
+
+    public ServiceException(String message, HttpStatus httpStatus) {
+        super(message);
+        this.httpStatus = httpStatus;
+    }
+}
diff --git a/src/main/java/org/oransc/ics/repository/InfoJob.java b/src/main/java/org/oransc/ics/repository/InfoJob.java
new file mode 100644 (file)
index 0000000..ffecdc9
--- /dev/null
@@ -0,0 +1,70 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import java.lang.invoke.MethodHandles;
+import java.time.Instant;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents the dynamic information about a information job
+ */
+@Builder
+public class InfoJob {
+    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    @Getter
+    private final String id;
+
+    @Getter
+    private final String typeId;
+
+    @Getter
+    private final String owner;
+
+    @Getter
+    private final Object jobData;
+
+    @Getter
+    private final String targetUrl;
+
+    @Getter
+    private final String jobStatusUrl;
+
+    @Getter
+    @Builder.Default
+    private String lastUpdated = Instant.now().toString();
+
+    @Getter
+    @Builder.Default
+    private boolean isLastStatusReportedEnabled = true;
+
+    public void setLastReportedStatus(boolean isEnabled) {
+        this.isLastStatusReportedEnabled = isEnabled;
+        logger.debug("Job status id: {}, enabled: {}", this.isLastStatusReportedEnabled, isEnabled);
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/repository/InfoJobs.java b/src/main/java/org/oransc/ics/repository/InfoJobs.java
new file mode 100644 (file)
index 0000000..1654e1f
--- /dev/null
@@ -0,0 +1,185 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapterFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Vector;
+
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
+import org.oransc.ics.exceptions.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.FileSystemUtils;
+
+/**
+ * Dynamic representation of all existing Information Jobs.
+ */
+public class InfoJobs {
+    private Map<String, InfoJob> allEiJobs = new HashMap<>();
+
+    private MultiMap<InfoJob> jobsByType = new MultiMap<>();
+    private MultiMap<InfoJob> jobsByOwner = new MultiMap<>();
+    private final Gson gson;
+
+    private final ApplicationConfig config;
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    private final ProducerCallbacks producerCallbacks;
+
+    public InfoJobs(ApplicationConfig config, ProducerCallbacks producerCallbacks) {
+        this.config = config;
+        GsonBuilder gsonBuilder = new GsonBuilder();
+        ServiceLoader.load(TypeAdapterFactory.class).forEach(gsonBuilder::registerTypeAdapterFactory);
+        this.gson = gsonBuilder.create();
+        this.producerCallbacks = producerCallbacks;
+    }
+
+    public synchronized void restoreJobsFromDatabase() throws IOException {
+        Files.createDirectories(Paths.get(getDatabaseDirectory()));
+        File dbDir = new File(getDatabaseDirectory());
+
+        for (File file : dbDir.listFiles()) {
+            String json = Files.readString(file.toPath());
+            InfoJob job = gson.fromJson(json, InfoJob.class);
+            this.doPut(job);
+        }
+    }
+
+    public synchronized void put(InfoJob job) {
+        this.doPut(job);
+        storeJobInFile(job);
+    }
+
+    public synchronized Collection<InfoJob> getJobs() {
+        return new Vector<>(allEiJobs.values());
+    }
+
+    public synchronized InfoJob getJob(String id) throws ServiceException {
+        InfoJob ric = allEiJobs.get(id);
+        if (ric == null) {
+            throw new ServiceException("Could not find Information job: " + id, HttpStatus.NOT_FOUND);
+        }
+        return ric;
+    }
+
+    public synchronized Collection<InfoJob> getJobsForType(String typeId) {
+        return jobsByType.get(typeId);
+    }
+
+    public synchronized Collection<InfoJob> getJobsForType(InfoType type) {
+        return jobsByType.get(type.getId());
+    }
+
+    public synchronized Collection<InfoJob> getJobsForOwner(String owner) {
+        return jobsByOwner.get(owner);
+    }
+
+    public synchronized InfoJob get(String id) {
+        return allEiJobs.get(id);
+    }
+
+    public synchronized InfoJob remove(String id, InfoProducers infoProducers) {
+        InfoJob job = allEiJobs.get(id);
+        if (job != null) {
+            remove(job, infoProducers);
+        }
+        return job;
+    }
+
+    public synchronized void remove(InfoJob job, InfoProducers infoProducers) {
+        this.allEiJobs.remove(job.getId());
+        jobsByType.remove(job.getTypeId(), job.getId());
+        jobsByOwner.remove(job.getOwner(), job.getId());
+
+        try {
+            Files.delete(getPath(job));
+        } catch (IOException e) {
+            logger.warn("Could not remove file: {}", e.getMessage());
+        }
+        this.producerCallbacks.stopInfoJob(job, infoProducers);
+    }
+
+    public synchronized int size() {
+        return allEiJobs.size();
+    }
+
+    public synchronized void clear() {
+        this.allEiJobs.clear();
+        this.jobsByType.clear();
+        jobsByOwner.clear();
+        clearDatabase();
+    }
+
+    private void clearDatabase() {
+        try {
+            FileSystemUtils.deleteRecursively(Path.of(getDatabaseDirectory()));
+            Files.createDirectories(Paths.get(getDatabaseDirectory()));
+        } catch (IOException e) {
+            logger.warn("Could not delete database : {}", e.getMessage());
+        }
+    }
+
+    private void doPut(InfoJob job) {
+        allEiJobs.put(job.getId(), job);
+        jobsByType.put(job.getTypeId(), job.getId(), job);
+        jobsByOwner.put(job.getOwner(), job.getId(), job);
+    }
+
+    private void storeJobInFile(InfoJob job) {
+        try {
+            try (PrintStream out = new PrintStream(new FileOutputStream(getFile(job)))) {
+                out.print(gson.toJson(job));
+            }
+        } catch (Exception e) {
+            logger.warn("Could not store job: {} {}", job.getId(), e.getMessage());
+        }
+    }
+
+    private File getFile(InfoJob job) {
+        return getPath(job).toFile();
+    }
+
+    private Path getPath(InfoJob job) {
+        return Path.of(getDatabaseDirectory(), job.getId());
+    }
+
+    private String getDatabaseDirectory() {
+        return config.getVardataDirectory() + "/database/eijobs";
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/repository/InfoProducer.java b/src/main/java/org/oransc/ics/repository/InfoProducer.java
new file mode 100644 (file)
index 0000000..01ec263
--- /dev/null
@@ -0,0 +1,85 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import lombok.Getter;
+
+public class InfoProducer {
+    @Getter
+    private final String id;
+
+    @Getter
+    private final Collection<InfoType> infoTypes;
+
+    @Getter
+    private final String jobCallbackUrl;
+
+    @Getter
+    private final String producerSupervisionCallbackUrl;
+
+    private final Set<String> enabledJobs = new HashSet<>();
+
+    private int unresponsiveCounter = 0;
+
+    public InfoProducer(String id, Collection<InfoType> infoTypes, String jobCallbackUrl,
+        String producerSupervisionCallbackUrl) {
+        this.id = id;
+        this.infoTypes = infoTypes;
+        this.jobCallbackUrl = jobCallbackUrl;
+        this.producerSupervisionCallbackUrl = producerSupervisionCallbackUrl;
+    }
+
+    public synchronized void setAliveStatus(boolean isAlive) {
+        if (isAlive) {
+            unresponsiveCounter = 0;
+        } else {
+            unresponsiveCounter++;
+        }
+    }
+
+    public synchronized boolean isDead() {
+        return this.unresponsiveCounter >= 3;
+    }
+
+    public synchronized boolean isAvailable() {
+        return this.unresponsiveCounter == 0;
+    }
+
+    public synchronized void setJobEnabled(InfoJob job) {
+        this.enabledJobs.add(job.getId());
+    }
+
+    public synchronized void setJobDisabled(InfoJob job) {
+        this.enabledJobs.remove(job.getId());
+    }
+
+    /**
+     * Is the job enabled for this producer?
+     */
+    public synchronized boolean isJobEnabled(InfoJob job) {
+        return this.enabledJobs.contains(job.getId());
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/repository/InfoProducers.java b/src/main/java/org/oransc/ics/repository/InfoProducers.java
new file mode 100644 (file)
index 0000000..19b2698
--- /dev/null
@@ -0,0 +1,168 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import org.oransc.ics.controllers.a1e.A1eCallbacks;
+import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
+import org.oransc.ics.exceptions.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+
+/**
+ * Dynamic representation of all EiProducers.
+ */
+@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
+@Component
+public class InfoProducers {
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private final Map<String, InfoProducer> allEiProducers = new HashMap<>();
+    private final MultiMap<InfoProducer> producersByType = new MultiMap<>();
+
+    @Autowired
+    private ProducerCallbacks producerCallbacks;
+
+    @Autowired
+    private A1eCallbacks consumerCallbacks;
+
+    @Autowired
+    private InfoJobs infoJobs;
+
+    @Builder
+    @Getter
+    public static class InfoProducerRegistrationInfo {
+        String id;
+
+        Collection<InfoType> supportedTypes;
+
+        String jobCallbackUrl;
+
+        String producerSupervisionCallbackUrl;
+    }
+
+    public InfoProducer registerProducer(InfoProducerRegistrationInfo producerInfo) {
+        final String producerId = producerInfo.getId();
+        InfoProducer previousDefinition = this.get(producerId);
+        if (previousDefinition != null) {
+            for (InfoType type : previousDefinition.getInfoTypes()) {
+                producersByType.remove(type.getId(), producerId);
+            }
+            allEiProducers.remove(producerId);
+        }
+
+        InfoProducer producer = createProducer(producerInfo);
+        allEiProducers.put(producer.getId(), producer);
+        for (InfoType type : producer.getInfoTypes()) {
+            producersByType.put(type.getId(), producer.getId(), producer);
+        }
+
+        Collection<InfoType> previousTypes =
+            previousDefinition != null ? previousDefinition.getInfoTypes() : new ArrayList<>();
+
+        producerCallbacks.startInfoJobs(producer, this.infoJobs) //
+            .collectList() //
+            .flatMapMany(list -> consumerCallbacks.notifyJobStatus(producer.getInfoTypes(), this)) //
+            .collectList() //
+            .flatMapMany(list -> consumerCallbacks.notifyJobStatus(previousTypes, this)) //
+            .subscribe();
+
+        return producer;
+    }
+
+    private InfoProducer createProducer(InfoProducerRegistrationInfo producerInfo) {
+        return new InfoProducer(producerInfo.getId(), producerInfo.getSupportedTypes(),
+            producerInfo.getJobCallbackUrl(), producerInfo.getProducerSupervisionCallbackUrl());
+    }
+
+    public synchronized Collection<InfoProducer> getAllProducers() {
+        return new Vector<>(allEiProducers.values());
+    }
+
+    public synchronized InfoProducer getProducer(String id) throws ServiceException {
+        InfoProducer p = allEiProducers.get(id);
+        if (p == null) {
+            throw new ServiceException("Could not find Information Producer: " + id, HttpStatus.NOT_FOUND);
+        }
+        return p;
+    }
+
+    public synchronized InfoProducer get(String id) {
+        return allEiProducers.get(id);
+    }
+
+    public synchronized int size() {
+        return allEiProducers.size();
+    }
+
+    public synchronized void clear() {
+        this.allEiProducers.clear();
+        this.producersByType.clear();
+    }
+
+    public void deregisterProducer(InfoProducer producer) {
+        allEiProducers.remove(producer.getId());
+        for (InfoType type : producer.getInfoTypes()) {
+            if (producersByType.remove(type.getId(), producer.getId()) == null) {
+                this.logger.error("Bug, no producer found");
+            }
+        }
+        this.consumerCallbacks.notifyJobStatus(producer.getInfoTypes(), this) //
+            .subscribe();
+    }
+
+    public synchronized Collection<InfoProducer> getProducersForType(InfoType type) {
+        return this.producersByType.get(type.getId());
+    }
+
+    public synchronized Collection<InfoProducer> getProducersForType(String typeId) {
+        return this.producersByType.get(typeId);
+    }
+
+    public synchronized Collection<String> getProducerIdsForType(String typeId) {
+        Collection<String> producerIds = new ArrayList<>();
+        for (InfoProducer p : this.getProducersForType(typeId)) {
+            producerIds.add(p.getId());
+        }
+        return producerIds;
+    }
+
+    public synchronized boolean isJobEnabled(InfoJob job) {
+        for (InfoProducer producer : this.producersByType.get(job.getTypeId())) {
+            if (producer.isJobEnabled(job)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/repository/InfoType.java b/src/main/java/org/oransc/ics/repository/InfoType.java
new file mode 100644 (file)
index 0000000..69ae67c
--- /dev/null
@@ -0,0 +1,41 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import lombok.Getter;
+
+public class InfoType {
+    @Getter
+    private final String id;
+
+    @Getter
+    private final Object jobDataSchema;
+
+    @Getter
+    private final Object typeSpecificInfo;
+
+    public InfoType(String id, Object jobDataSchema, Object typeSpecificInfo) {
+        this.id = id;
+        this.jobDataSchema = jobDataSchema;
+        this.typeSpecificInfo = typeSpecificInfo;
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java b/src/main/java/org/oransc/ics/repository/InfoTypeSubscriptions.java
new file mode 100644 (file)
index 0000000..a72a259
--- /dev/null
@@ -0,0 +1,287 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+import java.util.function.Function;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.exceptions.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.FileSystemUtils;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.util.retry.Retry;
+
+/**
+ * Subscriptions of callbacks for type registrations
+ */
+@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
+@Configuration
+public class InfoTypeSubscriptions {
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private final Map<String, SubscriptionInfo> allSubscriptions = new HashMap<>();
+    private final MultiMap<SubscriptionInfo> subscriptionsByOwner = new MultiMap<>();
+    private final Gson gson = new GsonBuilder().create();
+    private final ApplicationConfig config;
+    private final Map<String, ConsumerCallbackHandler> callbackHandlers = new HashMap<>();
+
+    public interface ConsumerCallbackHandler {
+        Mono<String> notifyTypeRegistered(InfoType type, SubscriptionInfo subscriptionInfo);
+
+        Mono<String> notifyTypeRemoved(InfoType type, SubscriptionInfo subscriptionInfo);
+    }
+
+    @Builder
+    @Getter
+    public static class SubscriptionInfo {
+        private String id;
+
+        private String callbackUrl;
+
+        private String owner;
+
+        private String apiVersion;
+    }
+
+    public InfoTypeSubscriptions(@Autowired ApplicationConfig config) {
+        this.config = config;
+
+        try {
+            this.restoreFromDatabase();
+        } catch (IOException e) {
+            logger.error("Could not restore info type subscriptions from database {}", this.getDatabaseDirectory());
+        }
+    }
+
+    public void registerCallbackhandler(ConsumerCallbackHandler handler, String apiVersion) {
+        callbackHandlers.put(apiVersion, handler);
+    }
+
+    public synchronized void put(SubscriptionInfo subscription) {
+        doPut(subscription);
+        storeInFile(subscription);
+        logger.debug("Added type status subscription {}", subscription.id);
+    }
+
+    public synchronized Collection<SubscriptionInfo> getAllSubscriptions() {
+        return new Vector<>(allSubscriptions.values());
+    }
+
+    /**
+     * Get a subscription and throw if not fond.
+     * 
+     * @param id the ID of the subscription to get.
+     * @return SubscriptionInfo
+     * @throws ServiceException if not found
+     */
+    public synchronized SubscriptionInfo getSubscription(String id) throws ServiceException {
+        SubscriptionInfo p = allSubscriptions.get(id);
+        if (p == null) {
+            throw new ServiceException("Could not find Information subscription: " + id, HttpStatus.NOT_FOUND);
+        }
+        return p;
+    }
+
+    /**
+     * Get a subscription or return null if not found. Equivalent to get in all java
+     * collections.
+     * 
+     * @param id the ID of the subscription to get.
+     * @return SubscriptionInfo
+     */
+    public synchronized SubscriptionInfo get(String id) {
+        return allSubscriptions.get(id);
+    }
+
+    public synchronized int size() {
+        return allSubscriptions.size();
+    }
+
+    public synchronized void clear() {
+        allSubscriptions.clear();
+        subscriptionsByOwner.clear();
+        clearDatabase();
+    }
+
+    public void remove(SubscriptionInfo subscription) {
+        allSubscriptions.remove(subscription.getId());
+        subscriptionsByOwner.remove(subscription.owner, subscription.id);
+
+        try {
+            Files.delete(getPath(subscription));
+        } catch (Exception e) {
+            logger.debug("Could not delete subscription from database: {}", e.getMessage());
+        }
+
+        logger.debug("Removed type status subscription {}", subscription.id);
+    }
+
+    /**
+     * returns all subscriptions for an owner. The colllection can contain 0..n
+     * subscriptions.
+     *
+     * @param owner
+     * @return
+     */
+    public synchronized Collection<SubscriptionInfo> getSubscriptionsForOwner(String owner) {
+        return subscriptionsByOwner.get(owner);
+    }
+
+    public synchronized void notifyTypeRegistered(InfoType type) {
+        notifyAllSubscribers(
+            subscription -> getCallbacksHandler(subscription.apiVersion).notifyTypeRegistered(type, subscription));
+    }
+
+    public synchronized void notifyTypeRemoved(InfoType type) {
+        notifyAllSubscribers(
+            subscription -> getCallbacksHandler(subscription.apiVersion).notifyTypeRemoved(type, subscription));
+    }
+
+    private ConsumerCallbackHandler getCallbacksHandler(String apiVersion) {
+        ConsumerCallbackHandler callbackHandler = this.callbackHandlers.get(apiVersion);
+        if (callbackHandler != null) {
+            return callbackHandler;
+        } else {
+            return new ConsumerCallbackHandler() {
+                @Override
+                public Mono<String> notifyTypeRegistered(InfoType type, SubscriptionInfo subscriptionInfo) {
+                    return error();
+                }
+
+                @Override
+                public Mono<String> notifyTypeRemoved(InfoType type, SubscriptionInfo subscriptionInfo) {
+                    return error();
+                }
+
+                private Mono<String> error() {
+                    return Mono.error(new ServiceException(
+                        "No notifyTypeRegistered handler found for interface version " + apiVersion,
+                        HttpStatus.INTERNAL_SERVER_ERROR));
+                }
+            };
+        }
+    }
+
+    private synchronized void notifyAllSubscribers(Function<? super SubscriptionInfo, Mono<String>> notifyFunc) {
+        final int MAX_CONCURRENCY = 5;
+        Flux.fromIterable(allSubscriptions.values()) //
+            .flatMap(subscription -> notifySubscriber(notifyFunc, subscription), MAX_CONCURRENCY) //
+            .subscribe();
+    }
+
+    /**
+     * Invoking one consumer. If the call fails after retries, the subscription is
+     * removed.
+     * 
+     * @param notifyFunc
+     * @param subscriptionInfo
+     * @return
+     */
+    private Mono<String> notifySubscriber(Function<? super SubscriptionInfo, Mono<String>> notifyFunc,
+        SubscriptionInfo subscriptionInfo) {
+        Retry retrySpec = Retry.backoff(3, Duration.ofSeconds(1));
+        return notifyFunc.apply(subscriptionInfo) //
+            .retryWhen(retrySpec) //
+            .onErrorResume(throwable -> {
+                logger.warn("Consumer callback failed {}, removing subscription {}", throwable.getMessage(),
+                    subscriptionInfo.id);
+                this.remove(subscriptionInfo);
+                return Mono.empty();
+            }); //
+    }
+
+    private void clearDatabase() {
+        try {
+            FileSystemUtils.deleteRecursively(Path.of(getDatabaseDirectory()));
+            Files.createDirectories(Paths.get(getDatabaseDirectory()));
+        } catch (IOException e) {
+            logger.warn("Could not delete database : {}", e.getMessage());
+        }
+    }
+
+    private void storeInFile(SubscriptionInfo subscription) {
+        try {
+            try (PrintStream out = new PrintStream(new FileOutputStream(getFile(subscription)))) {
+                String json = gson.toJson(subscription);
+                out.print(json);
+            }
+        } catch (Exception e) {
+            logger.warn("Could not save subscription: {} {}", subscription.getId(), e.getMessage());
+        }
+    }
+
+    public synchronized void restoreFromDatabase() throws IOException {
+        Files.createDirectories(Paths.get(getDatabaseDirectory()));
+        File dbDir = new File(getDatabaseDirectory());
+
+        for (File file : dbDir.listFiles()) {
+            String json = Files.readString(file.toPath());
+            SubscriptionInfo subscription = gson.fromJson(json, SubscriptionInfo.class);
+            doPut(subscription);
+        }
+    }
+
+    private void doPut(SubscriptionInfo subscription) {
+        allSubscriptions.put(subscription.getId(), subscription);
+        subscriptionsByOwner.put(subscription.owner, subscription.id, subscription);
+    }
+
+    private File getFile(SubscriptionInfo subscription) {
+        return getPath(subscription).toFile();
+    }
+
+    private Path getPath(SubscriptionInfo subscription) {
+        return getPath(subscription.getId());
+    }
+
+    private Path getPath(String subscriptionId) {
+        return Path.of(getDatabaseDirectory(), subscriptionId);
+    }
+
+    private String getDatabaseDirectory() {
+        return config.getVardataDirectory() + "/database/infotypesubscriptions";
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/repository/InfoTypes.java b/src/main/java/org/oransc/ics/repository/InfoTypes.java
new file mode 100644 (file)
index 0000000..9d2abc2
--- /dev/null
@@ -0,0 +1,149 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapterFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Vector;
+
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.exceptions.ServiceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.FileSystemUtils;
+
+/**
+ * Dynamic representation of all Information Types in the system.
+ */
+@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
+public class InfoTypes {
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+    private final Map<String, InfoType> allEiTypes = new HashMap<>();
+    private final ApplicationConfig config;
+    private final Gson gson;
+
+    public InfoTypes(ApplicationConfig config) {
+        this.config = config;
+        GsonBuilder gsonBuilder = new GsonBuilder();
+        ServiceLoader.load(TypeAdapterFactory.class).forEach(gsonBuilder::registerTypeAdapterFactory);
+        this.gson = gsonBuilder.create();
+    }
+
+    public synchronized void restoreTypesFromDatabase() throws IOException {
+        Files.createDirectories(Paths.get(getDatabaseDirectory()));
+        File dbDir = new File(getDatabaseDirectory());
+
+        for (File file : dbDir.listFiles()) {
+            String json = Files.readString(file.toPath());
+            InfoType type = gson.fromJson(json, InfoType.class);
+            allEiTypes.put(type.getId(), type);
+        }
+    }
+
+    public synchronized void put(InfoType type) {
+        allEiTypes.put(type.getId(), type);
+        storeInFile(type);
+    }
+
+    public synchronized Collection<InfoType> getAllInfoTypes() {
+        return new Vector<>(allEiTypes.values());
+    }
+
+    public synchronized InfoType getType(String id) throws ServiceException {
+        InfoType type = allEiTypes.get(id);
+        if (type == null) {
+            throw new ServiceException("Information type not found: " + id, HttpStatus.NOT_FOUND);
+        }
+        return type;
+    }
+
+    public synchronized InfoType get(String id) {
+        return allEiTypes.get(id);
+    }
+
+    public synchronized void remove(InfoType type) {
+        allEiTypes.remove(type.getId());
+        try {
+            Files.delete(getPath(type));
+        } catch (IOException e) {
+            logger.warn("Could not remove file: {} {}", type.getId(), e.getMessage());
+        }
+    }
+
+    public synchronized int size() {
+        return allEiTypes.size();
+    }
+
+    public synchronized void clear() {
+        this.allEiTypes.clear();
+        clearDatabase();
+    }
+
+    private void clearDatabase() {
+        try {
+            FileSystemUtils.deleteRecursively(Path.of(getDatabaseDirectory()));
+            Files.createDirectories(Paths.get(getDatabaseDirectory()));
+        } catch (IOException e) {
+            logger.warn("Could not delete database : {}", e.getMessage());
+        }
+    }
+
+    private void storeInFile(InfoType type) {
+        try {
+            try (PrintStream out = new PrintStream(new FileOutputStream(getFile(type)))) {
+                out.print(gson.toJson(type));
+            }
+        } catch (Exception e) {
+            logger.warn("Could not save type: {} {}", type.getId(), e.getMessage());
+        }
+    }
+
+    private File getFile(InfoType type) {
+        return getPath(type).toFile();
+    }
+
+    private Path getPath(InfoType type) {
+        return getPath(type.getId());
+    }
+
+    private Path getPath(String typeId) {
+        return Path.of(getDatabaseDirectory(), typeId);
+    }
+
+    private String getDatabaseDirectory() {
+        return config.getVardataDirectory() + "/database/eitypes";
+    }
+}
diff --git a/src/main/java/org/oransc/ics/repository/MultiMap.java b/src/main/java/org/oransc/ics/repository/MultiMap.java
new file mode 100644 (file)
index 0000000..0f3be0a
--- /dev/null
@@ -0,0 +1,65 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+
+package org.oransc.ics.repository;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * A map, where each key can be bound to may values (where each value has an own
+ * ID)
+ */
+public class MultiMap<T> {
+
+    private final Map<String, Map<String, T>> map = new HashMap<>();
+
+    public void put(String key, String id, T value) {
+        this.map.computeIfAbsent(key, k -> new HashMap<>()).put(id, value);
+    }
+
+    public T remove(String key, String id) {
+        Map<String, T> innerMap = this.map.get(key);
+        if (innerMap != null) {
+            T removedElement = innerMap.remove(id);
+            if (innerMap.isEmpty()) {
+                this.map.remove(key);
+            }
+            return removedElement;
+        }
+        return null;
+    }
+
+    public Collection<T> get(String key) {
+        Map<String, T> innerMap = this.map.get(key);
+        if (innerMap == null) {
+            return Collections.emptyList();
+        }
+        return new Vector<>(innerMap.values());
+    }
+
+    public void clear() {
+        this.map.clear();
+    }
+
+}
diff --git a/src/main/java/org/oransc/ics/tasks/ProducerSupervision.java b/src/main/java/org/oransc/ics/tasks/ProducerSupervision.java
new file mode 100644 (file)
index 0000000..36ca28e
--- /dev/null
@@ -0,0 +1,114 @@
+/*-
+ * ========================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.ics.tasks;
+
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.controllers.a1e.A1eCallbacks;
+import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
+import org.oransc.ics.repository.InfoJob;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducer;
+import org.oransc.ics.repository.InfoProducers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.util.retry.Retry;
+
+/**
+ * Regularly checks the availability of the Info Producers
+ */
+@Component
+@EnableScheduling
+@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally
+public class ProducerSupervision {
+    private static final Logger logger = LoggerFactory.getLogger(ProducerSupervision.class);
+
+    private final InfoProducers infoProducers;
+    private final InfoJobs infoJobs;
+    private final ProducerCallbacks producerCallbacks;
+    private final A1eCallbacks consumerCallbacks;
+
+    @Autowired
+    public ProducerSupervision(ApplicationConfig applicationConfig, InfoProducers infoProducers, InfoJobs infoJobs,
+        ProducerCallbacks producerCallbacks, A1eCallbacks consumerCallbacks) {
+        this.infoProducers = infoProducers;
+        this.infoJobs = infoJobs;
+        this.producerCallbacks = producerCallbacks;
+        this.consumerCallbacks = consumerCallbacks;
+    }
+
+    @Scheduled(fixedRate = 1000 * 60 * 5)
+    public void checkAllProducers() {
+        logger.debug("Checking producers starting");
+        createTask().subscribe(null, null, () -> logger.debug("Checking all Producers completed"));
+    }
+
+    public Flux<InfoProducer> createTask() {
+        return Flux.fromIterable(infoProducers.getAllProducers()) //
+            .flatMap(this::checkOneProducer);
+    }
+
+    private Mono<InfoProducer> checkOneProducer(InfoProducer producer) {
+        return this.producerCallbacks.healthCheck(producer) //
+            .onErrorResume(throwable -> {
+                handleNonRespondingProducer(throwable, producer);
+                return Mono.empty();
+            })//
+            .doOnNext(response -> handleRespondingProducer(response, producer))
+            .flatMap(response -> checkProducerJobs(producer)) //
+            .map(responses -> producer);
+    }
+
+    private Mono<?> checkProducerJobs(InfoProducer producer) {
+        final int MAX_CONCURRENCY = 10;
+        return getEiJobs(producer) //
+            .filter(infoJob -> !producer.isJobEnabled(infoJob)) //
+            .flatMap(infoJob -> producerCallbacks.startInfoJob(producer, infoJob, Retry.max(1)), MAX_CONCURRENCY) //
+            .collectList() //
+            .flatMapMany(startedJobs -> consumerCallbacks.notifyJobStatus(producer.getInfoTypes(), infoProducers)) //
+            .collectList();
+    }
+
+    private Flux<InfoJob> getEiJobs(InfoProducer producer) {
+        return Flux.fromIterable(producer.getInfoTypes()) //
+            .flatMap(infoType -> Flux.fromIterable(infoJobs.getJobsForType(infoType)));
+    }
+
+    private void handleNonRespondingProducer(Throwable throwable, InfoProducer producer) {
+        logger.warn("Unresponsive producer: {} exception: {}", producer.getId(), throwable.getMessage());
+        producer.setAliveStatus(false);
+        if (producer.isDead()) {
+            this.infoProducers.deregisterProducer(producer);
+        }
+    }
+
+    private void handleRespondingProducer(String response, InfoProducer producer) {
+        logger.debug("{}", response);
+        producer.setAliveStatus(true);
+    }
+
+}
diff --git a/src/test/java/org/oransc/ics/ApplicationTest.java b/src/test/java/org/oransc/ics/ApplicationTest.java
new file mode 100644 (file)
index 0000000..975bf81
--- /dev/null
@@ -0,0 +1,1284 @@
+/*-
+ * ========================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.ics;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParser;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.json.JSONObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.oransc.ics.clients.AsyncRestClient;
+import org.oransc.ics.clients.AsyncRestClientFactory;
+import org.oransc.ics.clients.SecurityContext;
+import org.oransc.ics.configuration.ApplicationConfig;
+import org.oransc.ics.configuration.ImmutableHttpProxyConfig;
+import org.oransc.ics.configuration.ImmutableWebClientConfig;
+import org.oransc.ics.configuration.WebClientConfig;
+import org.oransc.ics.configuration.WebClientConfig.HttpProxyConfig;
+import org.oransc.ics.controller.A1eCallbacksSimulatorController;
+import org.oransc.ics.controller.ConsumerSimulatorController;
+import org.oransc.ics.controller.ProducerSimulatorController;
+import org.oransc.ics.controllers.a1e.A1eConsts;
+import org.oransc.ics.controllers.a1e.A1eEiJobInfo;
+import org.oransc.ics.controllers.a1e.A1eEiJobStatus;
+import org.oransc.ics.controllers.a1e.A1eEiTypeInfo;
+import org.oransc.ics.controllers.r1consumer.ConsumerConsts;
+import org.oransc.ics.controllers.r1consumer.ConsumerInfoTypeInfo;
+import org.oransc.ics.controllers.r1consumer.ConsumerJobInfo;
+import org.oransc.ics.controllers.r1consumer.ConsumerJobStatus;
+import org.oransc.ics.controllers.r1consumer.ConsumerTypeRegistrationInfo;
+import org.oransc.ics.controllers.r1consumer.ConsumerTypeSubscriptionInfo;
+import org.oransc.ics.controllers.r1producer.ProducerCallbacks;
+import org.oransc.ics.controllers.r1producer.ProducerConsts;
+import org.oransc.ics.controllers.r1producer.ProducerInfoTypeInfo;
+import org.oransc.ics.controllers.r1producer.ProducerJobInfo;
+import org.oransc.ics.controllers.r1producer.ProducerRegistrationInfo;
+import org.oransc.ics.controllers.r1producer.ProducerStatusInfo;
+import org.oransc.ics.exceptions.ServiceException;
+import org.oransc.ics.repository.InfoJob;
+import org.oransc.ics.repository.InfoJobs;
+import org.oransc.ics.repository.InfoProducer;
+import org.oransc.ics.repository.InfoProducers;
+import org.oransc.ics.repository.InfoType;
+import org.oransc.ics.repository.InfoTypeSubscriptions;
+import org.oransc.ics.repository.InfoTypes;
+import org.oransc.ics.tasks.ProducerSupervision;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+@TestPropertySource(
+    properties = { //
+        "server.ssl.key-store=./config/keystore.jks", //
+        "app.webclient.trust-store=./config/truststore.jks", //
+        "app.vardata-directory=./target"})
+class ApplicationTest {
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    private final String TYPE_ID = "typeId";
+    private final String PRODUCER_ID = "producerId";
+    private final String EI_JOB_PROPERTY = "\"property1\"";
+    private final String EI_JOB_ID = "jobId";
+
+    @Autowired
+    ApplicationContext context;
+
+    @Autowired
+    InfoJobs infoJobs;
+
+    @Autowired
+    InfoTypes infoTypes;
+
+    @Autowired
+    InfoProducers infoProducers;
+
+    @Autowired
+    ApplicationConfig applicationConfig;
+
+    @Autowired
+    ProducerSimulatorController producerSimulator;
+
+    @Autowired
+    ConsumerSimulatorController consumerSimulator;
+
+    @Autowired
+    A1eCallbacksSimulatorController a1eCallbacksSimulator;
+
+    @Autowired
+    ProducerSupervision producerSupervision;
+
+    @Autowired
+    ProducerCallbacks producerCallbacks;
+
+    @Autowired
+    InfoTypeSubscriptions infoTypeSubscriptions;
+
+    @Autowired
+    SecurityContext securityContext;
+
+    private static Gson gson = new GsonBuilder().create();
+
+    /**
+     * Overrides the BeanFactory.
+     */
+    @TestConfiguration
+    static class TestBeanFactory {
+        @Bean
+        public ServletWebServerFactory servletContainer() {
+            return new TomcatServletWebServerFactory();
+        }
+    }
+
+    @LocalServerPort
+    private int port;
+
+    @BeforeEach
+    void reset() {
+        this.infoJobs.clear();
+        this.infoTypes.clear();
+        this.infoProducers.clear();
+        this.infoTypeSubscriptions.clear();
+        this.producerSimulator.getTestResults().reset();
+        this.consumerSimulator.getTestResults().reset();
+        this.a1eCallbacksSimulator.getTestResults().reset();
+        this.securityContext.setAuthTokenFilePath(null);
+    }
+
+    @AfterEach
+    void check() {
+        assertThat(this.producerSimulator.getTestResults().errorFound).isFalse();
+    }
+
+    @Test
+    void generateApiDoc() throws FileNotFoundException {
+        String url = "/v3/api-docs";
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+        JSONObject jsonObj = new JSONObject(resp.getBody());
+        assertThat(jsonObj.remove("servers")).isNotNull();
+
+        String indented = jsonObj.toString(4);
+        try (PrintStream out = new PrintStream(new FileOutputStream("api/ics-api.json"))) {
+            out.print(indented);
+        }
+    }
+
+    @Test
+    void a1eGetEiTypes() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, "test");
+        String url = A1eConsts.API_ROOT + "/eitypes";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[\"test\"]");
+    }
+
+    @Test
+    void consumerGetInfoTypes() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, "test");
+        String url = ConsumerConsts.API_ROOT + "/info-types";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[\"test\"]");
+    }
+
+    @Test
+    void a1eGetEiTypesEmpty() throws Exception {
+        String url = A1eConsts.API_ROOT + "/eitypes";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[]");
+    }
+
+    @Test
+    void consumerGetEiTypesEmpty() throws Exception {
+        String url = ConsumerConsts.API_ROOT + "/info-types";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[]");
+    }
+
+    @Test
+    void a1eGetEiType() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, "test");
+        String url = A1eConsts.API_ROOT + "/eitypes/test";
+        String rsp = restClient().get(url).block();
+        A1eEiTypeInfo info = gson.fromJson(rsp, A1eEiTypeInfo.class);
+        assertThat(info).isNotNull();
+    }
+
+    @Test
+    void consumerGetEiType() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, "test");
+        String url = ConsumerConsts.API_ROOT + "/info-types/test";
+        String rsp = restClient().get(url).block();
+        ConsumerInfoTypeInfo info = gson.fromJson(rsp, ConsumerInfoTypeInfo.class);
+        assertThat(info).isNotNull();
+        assertThat(info.jobDataSchema).isNotNull();
+        assertThat(info.state).isEqualTo(ConsumerInfoTypeInfo.ConsumerTypeStatusValues.ENABLED);
+        assertThat(info.noOfProducers).isEqualTo(1);
+    }
+
+    @Test
+    void a1eGetEiTypeNotFound() throws Exception {
+        String url = A1eConsts.API_ROOT + "/eitypes/junk";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Information type not found: junk");
+    }
+
+    @Test
+    void consumerGetEiTypeNotFound() throws Exception {
+        String url = ConsumerConsts.API_ROOT + "/info-types/junk";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Information type not found: junk");
+    }
+
+    @Test
+    void a1eGetEiJobsIds() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        final String JOB_ID_JSON = "[\"jobId\"]";
+        String url = A1eConsts.API_ROOT + "/eijobs?infoTypeId=typeId";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = A1eConsts.API_ROOT + "/eijobs?owner=owner";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = A1eConsts.API_ROOT + "/eijobs?owner=JUNK";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[]");
+
+        url = A1eConsts.API_ROOT + "/eijobs";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = A1eConsts.API_ROOT + "/eijobs?eiTypeId=typeId&&owner=owner";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = A1eConsts.API_ROOT + "/eijobs?eiTypeId=JUNK";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[]");
+    }
+
+    @Test
+    void consumerGetInformationJobsIds() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        final String JOB_ID_JSON = "[\"jobId\"]";
+        String url = ConsumerConsts.API_ROOT + "/info-jobs?infoTypeId=typeId";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = ConsumerConsts.API_ROOT + "/info-jobs?owner=owner";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = ConsumerConsts.API_ROOT + "/info-jobs?owner=JUNK";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[]");
+
+        url = ConsumerConsts.API_ROOT + "/info-jobs";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = ConsumerConsts.API_ROOT + "/info-jobs?infoTypeId=typeId&&owner=owner";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo(JOB_ID_JSON);
+
+        url = ConsumerConsts.API_ROOT + "/info-jobs?infoTypeId=JUNK";
+        rsp = restClient().get(url).block();
+        assertThat(rsp).isEqualTo("[]");
+    }
+
+    @Test
+    void a1eGetEiJob() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        String url = A1eConsts.API_ROOT + "/eijobs/jobId";
+        String rsp = restClient().get(url).block();
+        A1eEiJobInfo info = gson.fromJson(rsp, A1eEiJobInfo.class);
+        assertThat(info.owner).isEqualTo("owner");
+        assertThat(info.eiTypeId).isEqualTo(TYPE_ID);
+    }
+
+    @Test
+    void consumerGetEiJob() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId";
+        String rsp = restClient().get(url).block();
+        ConsumerJobInfo info = gson.fromJson(rsp, ConsumerJobInfo.class);
+        assertThat(info.owner).isEqualTo("owner");
+        assertThat(info.infoTypeId).isEqualTo(TYPE_ID);
+    }
+
+    @Test
+    void a1eGetEiJobNotFound() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        String url = A1eConsts.API_ROOT + "/eijobs/junk";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find Information job: junk");
+    }
+
+    @Test
+    void consumerGetInfoJobNotFound() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/junk";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find Information job: junk");
+    }
+
+    @Test
+    void a1eGetEiJobStatus() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+
+        verifyJobStatus("jobId", "ENABLED");
+    }
+
+    @Test
+    void consumerGetInfoJobStatus() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId/status";
+        String rsp = restClient().get(url).block();
+        assertThat(rsp) //
+            .contains("ENABLED") //
+            .contains(PRODUCER_ID);
+
+        ConsumerJobStatus status = gson.fromJson(rsp, ConsumerJobStatus.class);
+        assertThat(status.producers).contains(PRODUCER_ID);
+    }
+
+    @Test
+    void a1eDeleteEiJob() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        assertThat(this.infoJobs.size()).isEqualTo(1);
+        String url = A1eConsts.API_ROOT + "/eijobs/jobId";
+        restClient().delete(url).block();
+        assertThat(this.infoJobs.size()).isZero();
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStopped).hasSize(1));
+        assertThat(simulatorResults.jobsStopped.get(0)).isEqualTo("jobId");
+
+    }
+
+    @Test
+    void consumerDeleteEiJob() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        assertThat(this.infoJobs.size()).isEqualTo(1);
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId";
+        restClient().delete(url).block();
+        assertThat(this.infoJobs.size()).isZero();
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStopped).hasSize(1));
+        assertThat(simulatorResults.jobsStopped.get(0)).isEqualTo("jobId");
+
+        testErrorCode(restClient().delete(url), HttpStatus.NOT_FOUND, "Could not find Information job: jobId");
+    }
+
+    @Test
+    void a1eDeleteEiJobNotFound() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        String url = A1eConsts.API_ROOT + "/eijobs/junk";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find Information job: junk");
+    }
+
+    @Test
+    void consumerDeleteEiJobNotFound() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/junk";
+        testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find Information job: junk");
+    }
+
+    @Test
+    void a1ePutEiJob() throws Exception {
+        // Test that one producer accepting a job is enough
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoProducerWithOneTypeRejecting("simulateProducerError", TYPE_ID);
+
+        String url = A1eConsts.API_ROOT + "/eijobs/jobId";
+        String body = gson.toJson(infoJobInfo());
+        ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
+        assertThat(this.infoJobs.size()).isEqualTo(1);
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted).hasSize(1));
+        ProducerJobInfo request = simulatorResults.jobsStarted.get(0);
+        assertThat(request.id).isEqualTo("jobId");
+
+        // One retry --> two calls
+        await().untilAsserted(() -> assertThat(simulatorResults.noOfRejectedCreate).isEqualTo(2));
+        assertThat(simulatorResults.noOfRejectedCreate).isEqualTo(2);
+
+        resp = restClient().putForEntity(url, body).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+        InfoJob job = this.infoJobs.getJob("jobId");
+        assertThat(job.getOwner()).isEqualTo("owner");
+
+        verifyJobStatus(EI_JOB_ID, "ENABLED");
+    }
+
+    @Test
+    void consumerPutInformationJob() throws Exception {
+        // Test that one producer accepting a job is enough
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId";
+        String body = gson.toJson(consumerJobInfo());
+        ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
+        assertThat(this.infoJobs.size()).isEqualTo(1);
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted).hasSize(1));
+        ProducerJobInfo request = simulatorResults.jobsStarted.get(0);
+        assertThat(request.id).isEqualTo("jobId");
+
+        resp = restClient().putForEntity(url, body).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+        InfoJob job = this.infoJobs.getJob("jobId");
+        assertThat(job.getOwner()).isEqualTo("owner");
+
+        verifyJobStatus(EI_JOB_ID, "ENABLED");
+    }
+
+    @Test
+    void consumerPutInformationJob_noType() throws JsonMappingException, JsonProcessingException, ServiceException {
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId?typeCheck=false";
+        String body = gson.toJson(consumerJobInfo());
+        ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
+        assertThat(this.infoJobs.size()).isEqualTo(1);
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+        verifyJobStatus(EI_JOB_ID, "DISABLED");
+
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+
+        verifyJobStatus(EI_JOB_ID, "ENABLED");
+    }
+
+    @Test
+    void a1ePutEiJob_jsonSchemavalidationError() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+
+        String url = A1eConsts.API_ROOT + "/eijobs/jobId";
+        // The element with name "property1" is mandatory in the schema
+        A1eEiJobInfo jobInfo = new A1eEiJobInfo("typeId", jsonObject("{ \"XXstring\" : \"value\" }"), "owner",
+            "targetUri", "jobStatusUrl");
+        String body = gson.toJson(jobInfo);
+
+        testErrorCode(restClient().put(url, body), HttpStatus.BAD_REQUEST, "Json validation failure");
+
+        testErrorCode(restClient().put(url, "{jojo}"), HttpStatus.BAD_REQUEST, "", false);
+
+    }
+
+    @Test
+    void consumerPutJob_jsonSchemavalidationError() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId?typeCheck=true";
+        // The element with name "property1" is mandatory in the schema
+        ConsumerJobInfo jobInfo =
+            new ConsumerJobInfo("typeId", jsonObject("{ \"XXstring\" : \"value\" }"), "owner", "targetUri", null);
+        String body = gson.toJson(jobInfo);
+
+        testErrorCode(restClient().put(url, body), HttpStatus.BAD_REQUEST, "Json validation failure");
+    }
+
+    @Test
+    void consumerPutJob_uriError() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId?typeCheck=true";
+
+        ConsumerJobInfo jobInfo = new ConsumerJobInfo(TYPE_ID, jsonObject(), "owner", "junk", null);
+        String body = gson.toJson(jobInfo);
+
+        testErrorCode(restClient().put(url, body), HttpStatus.BAD_REQUEST, "URI: junk is not absolute");
+    }
+
+    @Test
+    void a1eChangingEiTypeGetRejected() throws Exception {
+        putInfoProducerWithOneType("producer1", "typeId1");
+        putInfoProducerWithOneType("producer2", "typeId2");
+        putInfoJob("typeId1", "jobId");
+
+        String url = A1eConsts.API_ROOT + "/eijobs/jobId";
+        String body = gson.toJson(infoJobInfo("typeId2", "jobId"));
+        testErrorCode(restClient().put(url, body), HttpStatus.CONFLICT,
+            "Not allowed to change type for existing EI job");
+    }
+
+    @Test
+    void consumerChangingInfoTypeGetRejected() throws Exception {
+        putInfoProducerWithOneType("producer1", "typeId1");
+        putInfoProducerWithOneType("producer2", "typeId2");
+        putInfoJob("typeId1", "jobId");
+
+        String url = ConsumerConsts.API_ROOT + "/info-jobs/jobId";
+        String body = gson.toJson(consumerJobInfo("typeId2", "jobId"));
+        testErrorCode(restClient().put(url, body), HttpStatus.CONFLICT, "Not allowed to change type for existing job");
+    }
+
+    @Test
+    void producerPutEiType() throws JsonMappingException, JsonProcessingException, ServiceException {
+        assertThat(putInfoType(TYPE_ID)).isEqualTo(HttpStatus.CREATED);
+        assertThat(putInfoType(TYPE_ID)).isEqualTo(HttpStatus.OK);
+    }
+
+    @Test
+    void producerPutEiType_noSchema() {
+        String url = ProducerConsts.API_ROOT + "/info-types/" + TYPE_ID;
+        String body = "{}";
+        testErrorCode(restClient().put(url, body), HttpStatus.BAD_REQUEST, "No schema provided");
+
+        testErrorCode(restClient().post(url, body), HttpStatus.METHOD_NOT_ALLOWED, "", false);
+    }
+
+    @Test
+    void producerDeleteEiType() throws Exception {
+        putInfoType(TYPE_ID);
+        this.putInfoJob(TYPE_ID, "job1");
+        this.putInfoJob(TYPE_ID, "job2");
+        deleteInfoType(TYPE_ID);
+
+        assertThat(this.infoTypes.size()).isZero();
+        assertThat(this.infoJobs.size()).isZero(); // Test that also the job is deleted
+
+        testErrorCode(restClient().delete(deleteInfoTypeUrl(TYPE_ID)), HttpStatus.NOT_FOUND,
+            "Information type not found");
+    }
+
+    @Test
+    void producerDeleteEiTypeExistingProducer() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        String url = ProducerConsts.API_ROOT + "/info-types/" + TYPE_ID;
+        testErrorCode(restClient().delete(url), HttpStatus.CONFLICT, "The type has active producers: " + PRODUCER_ID);
+        assertThat(this.infoTypes.size()).isEqualTo(1);
+    }
+
+    @Test
+    void producerPutProducerWithOneType_rejecting()
+        throws JsonMappingException, JsonProcessingException, ServiceException {
+        putInfoProducerWithOneTypeRejecting("simulateProducerError", TYPE_ID);
+        String url = A1eConsts.API_ROOT + "/eijobs/" + EI_JOB_ID;
+        String body = gson.toJson(infoJobInfo());
+        restClient().put(url, body).block();
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        // There is one retry -> 2 calls
+        await().untilAsserted(() -> assertThat(simulatorResults.noOfRejectedCreate).isEqualTo(2));
+        assertThat(simulatorResults.noOfRejectedCreate).isEqualTo(2);
+
+        verifyJobStatus(EI_JOB_ID, "DISABLED");
+    }
+
+    @Test
+    void producerGetInfoProducerTypes() throws Exception {
+        final String EI_TYPE_ID_2 = TYPE_ID + "_2";
+        putInfoProducerWithOneType("producer1", TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        putInfoProducerWithOneType("producer2", EI_TYPE_ID_2);
+        putInfoJob(EI_TYPE_ID_2, "jobId2");
+        String url = ProducerConsts.API_ROOT + "/info-types";
+
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+        assertThat(resp.getBody()).contains(TYPE_ID);
+        assertThat(resp.getBody()).contains(EI_TYPE_ID_2);
+    }
+
+    @Test
+    void producerPutInfoProducer() throws Exception {
+        this.putInfoType(TYPE_ID);
+        String url = ProducerConsts.API_ROOT + "/info-producers/infoProducerId";
+        String body = gson.toJson(producerInfoRegistratioInfo(TYPE_ID));
+
+        ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+
+        assertThat(this.infoTypes.size()).isEqualTo(1);
+        assertThat(this.infoProducers.getProducersForType(TYPE_ID)).hasSize(1);
+        assertThat(this.infoProducers.size()).isEqualTo(1);
+        assertThat(this.infoProducers.get("infoProducerId").getInfoTypes().iterator().next().getId())
+            .isEqualTo(TYPE_ID);
+
+        resp = restClient().putForEntity(url, body).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+        // GET info producer
+        resp = restClient().getForEntity(url).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+        assertThat(resp.getBody()).isEqualTo(body);
+
+        testErrorCode(restClient().get(url + "junk"), HttpStatus.NOT_FOUND, "Could not find Information Producer");
+    }
+
+    @Test
+    void producerPutInfoProducerExistingJob() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        String url = ProducerConsts.API_ROOT + "/info-producers/infoProducerId";
+        String body = gson.toJson(producerInfoRegistratioInfo(TYPE_ID));
+        restClient().putForEntity(url, body).block();
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted).hasSize(2));
+        ProducerJobInfo request = simulatorResults.jobsStarted.get(0);
+        assertThat(request.id).isEqualTo("jobId");
+    }
+
+    @Test
+    void testPutInfoProducer_noType() throws Exception {
+        String url = ProducerConsts.API_ROOT + "/info-producers/infoProducerId";
+        String body = gson.toJson(producerInfoRegistratioInfo(TYPE_ID));
+        testErrorCode(restClient().put(url, body), HttpStatus.NOT_FOUND, "Information type not found");
+    }
+
+    @Test
+    void producerPutProducerAndInfoJob() throws Exception {
+        this.putInfoType(TYPE_ID);
+        String url = ProducerConsts.API_ROOT + "/info-producers/infoProducerId";
+        String body = gson.toJson(producerInfoRegistratioInfo(TYPE_ID));
+        restClient().putForEntity(url, body).block();
+        assertThat(this.infoTypes.size()).isEqualTo(1);
+        this.infoTypes.getType(TYPE_ID);
+
+        url = A1eConsts.API_ROOT + "/eijobs/jobId";
+        body = gson.toJson(infoJobInfo());
+        restClient().putForEntity(url, body).block();
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStarted).hasSize(1));
+        ProducerJobInfo request = simulatorResults.jobsStarted.get(0);
+        assertThat(request.id).isEqualTo("jobId");
+    }
+
+    @Test
+    void producerGetInfoJobsForProducer() throws JsonMappingException, JsonProcessingException, ServiceException {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId1");
+        putInfoJob(TYPE_ID, "jobId2");
+
+        // PUT a consumerRestApiTestBase.java
+        String url = ProducerConsts.API_ROOT + "/info-producers/infoProducerId";
+        String body = gson.toJson(producerInfoRegistratioInfo(TYPE_ID));
+        restClient().putForEntity(url, body).block();
+
+        url = ProducerConsts.API_ROOT + "/info-producers/infoProducerId/info-jobs";
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+
+        ProducerJobInfo[] parsedResp = gson.fromJson(resp.getBody(), ProducerJobInfo[].class);
+        assertThat(parsedResp[0].typeId).isEqualTo(TYPE_ID);
+        assertThat(parsedResp[1].typeId).isEqualTo(TYPE_ID);
+    }
+
+    @Test
+    void producerDeleteInfoProducer() throws Exception {
+        putInfoProducerWithOneType("infoProducerId", TYPE_ID);
+        putInfoProducerWithOneType("infoProducerId2", TYPE_ID);
+
+        assertThat(this.infoProducers.size()).isEqualTo(2);
+        InfoType type = this.infoTypes.getType(TYPE_ID);
+        assertThat(this.infoProducers.getProducerIdsForType(type.getId())).contains("infoProducerId");
+        assertThat(this.infoProducers.getProducerIdsForType(type.getId())).contains("infoProducerId2");
+        putInfoJob(TYPE_ID, "jobId");
+        assertThat(this.infoJobs.size()).isEqualTo(1);
+
+        deleteInfoProducer("infoProducerId");
+        assertThat(this.infoProducers.size()).isEqualTo(1);
+        assertThat(this.infoProducers.getProducerIdsForType(TYPE_ID)).doesNotContain("infoProducerId");
+        verifyJobStatus("jobId", "ENABLED");
+
+        deleteInfoProducer("infoProducerId2");
+        assertThat(this.infoProducers.size()).isZero();
+        assertThat(this.infoTypes.size()).isEqualTo(1);
+        verifyJobStatus("jobId", "DISABLED");
+
+        String url = ProducerConsts.API_ROOT + "/info-producers/" + "junk";
+        testErrorCode(restClient().delete(url), HttpStatus.NOT_FOUND, "Could not find Information Producer");
+    }
+
+    @Test
+    void a1eJobStatusNotifications() throws JsonMappingException, JsonProcessingException, ServiceException {
+        A1eCallbacksSimulatorController.TestResults consumerCalls = this.a1eCallbacksSimulator.getTestResults();
+        ProducerSimulatorController.TestResults producerCalls = this.producerSimulator.getTestResults();
+
+        putInfoProducerWithOneType("infoProducerId", TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+        putInfoProducerWithOneType("infoProducerId2", TYPE_ID);
+        await().untilAsserted(() -> assertThat(producerCalls.jobsStarted).hasSize(2));
+
+        deleteInfoProducer("infoProducerId2");
+        assertThat(this.infoTypes.size()).isEqualTo(1); // The type remains, one producer left
+        deleteInfoProducer("infoProducerId");
+        assertThat(this.infoTypes.size()).isEqualTo(1); // The type remains
+        assertThat(this.infoJobs.size()).isEqualTo(1); // The job remains
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks).hasSize(1));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(0).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+
+        putInfoProducerWithOneType("infoProducerId", TYPE_ID);
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks).hasSize(2));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(1).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+    }
+
+    @Test
+    void a1eJobStatusNotifications2() throws JsonMappingException, JsonProcessingException, ServiceException {
+        // Test replacing a producer with new and removed types
+
+        // Create a job
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, EI_JOB_ID);
+
+        // change the type for the producer, the job shall be disabled
+        putInfoProducerWithOneType(PRODUCER_ID, "junk");
+        verifyJobStatus(EI_JOB_ID, "DISABLED");
+        A1eCallbacksSimulatorController.TestResults consumerCalls = this.a1eCallbacksSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks).hasSize(1));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(0).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        verifyJobStatus(EI_JOB_ID, "ENABLED");
+        await().untilAsserted(() -> assertThat(consumerCalls.eiJobStatusCallbacks).hasSize(2));
+        assertThat(consumerCalls.eiJobStatusCallbacks.get(1).state).isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+    }
+
+    @Test
+    void producerGetProducerInfoType() throws JsonMappingException, JsonProcessingException, ServiceException {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        String url = ProducerConsts.API_ROOT + "/info-types/" + TYPE_ID;
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        ProducerInfoTypeInfo info = gson.fromJson(resp.getBody(), ProducerInfoTypeInfo.class);
+        assertThat(info.jobDataSchema).isNotNull();
+        assertThat(info.typeSpecificInformation).isNotNull();
+
+        testErrorCode(restClient().get(url + "junk"), HttpStatus.NOT_FOUND, "Information type not found");
+    }
+
+    @Test
+    void producerGetProducerIdentifiers() throws JsonMappingException, JsonProcessingException, ServiceException {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        String url = ProducerConsts.API_ROOT + "/info-producers";
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        assertThat(resp.getBody()).contains(PRODUCER_ID);
+
+        url = ProducerConsts.API_ROOT + "/info-producers?infoTypeId=" + TYPE_ID;
+        resp = restClient().getForEntity(url).block();
+        assertThat(resp.getBody()).contains(PRODUCER_ID);
+
+        url = ProducerConsts.API_ROOT + "/info-producers?infoTypeId=junk";
+        resp = restClient().getForEntity(url).block();
+        assertThat(resp.getBody()).isEqualTo("[]");
+    }
+
+    @Test
+    void producerSupervision() throws JsonMappingException, JsonProcessingException, ServiceException {
+
+        A1eCallbacksSimulatorController.TestResults consumerResults = this.a1eCallbacksSimulator.getTestResults();
+        putInfoProducerWithOneTypeRejecting("simulateProducerError", TYPE_ID);
+
+        {
+            // Create a job
+            putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+            putInfoJob(TYPE_ID, EI_JOB_ID);
+            verifyJobStatus(EI_JOB_ID, "ENABLED");
+            deleteInfoProducer(PRODUCER_ID);
+            // A Job disabled status notification shall now be received
+            await().untilAsserted(() -> assertThat(consumerResults.eiJobStatusCallbacks).hasSize(1));
+            assertThat(consumerResults.eiJobStatusCallbacks.get(0).state)
+                .isEqualTo(A1eEiJobStatus.EiJobStatusValues.DISABLED);
+            verifyJobStatus(EI_JOB_ID, "DISABLED");
+        }
+
+        assertThat(this.infoProducers.size()).isEqualTo(1);
+        assertThat(this.infoTypes.size()).isEqualTo(1);
+        assertProducerOpState("simulateProducerError", ProducerStatusInfo.OperationalState.ENABLED);
+
+        this.producerSupervision.createTask().blockLast();
+        this.producerSupervision.createTask().blockLast();
+
+        // Now we have one producer that is disabled
+        assertThat(this.infoProducers.size()).isEqualTo(1);
+        assertProducerOpState("simulateProducerError", ProducerStatusInfo.OperationalState.DISABLED);
+
+        // After 3 failed checks, the producer shall be deregistered
+        this.producerSupervision.createTask().blockLast();
+        assertThat(this.infoProducers.size()).isZero(); // The producer is removed
+        assertThat(this.infoTypes.size()).isEqualTo(1); // The type remains
+
+        // Now we have one disabled job, and no producer.
+        // PUT a producer, then a Job ENABLED status notification shall be received
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        await().untilAsserted(() -> assertThat(consumerResults.eiJobStatusCallbacks).hasSize(2));
+        assertThat(consumerResults.eiJobStatusCallbacks.get(1).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+        verifyJobStatus(EI_JOB_ID, "ENABLED");
+    }
+
+    @Test
+    void producerSupervision2() throws JsonMappingException, JsonProcessingException, ServiceException {
+        // Test that supervision enables not enabled jobs and sends a notification when
+        // suceeded
+
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, EI_JOB_ID);
+
+        InfoProducer producer = this.infoProducers.getProducer(PRODUCER_ID);
+        InfoJob job = this.infoJobs.getJob(EI_JOB_ID);
+        // Pretend that the producer did reject the job and the a DISABLED notification
+        // is sent for the job
+        producer.setJobDisabled(job);
+        job.setLastReportedStatus(false);
+        verifyJobStatus(EI_JOB_ID, "DISABLED");
+
+        // Run the supervision and wait for the job to get started in the producer
+        this.producerSupervision.createTask().blockLast();
+        A1eCallbacksSimulatorController.TestResults consumerResults = this.a1eCallbacksSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(consumerResults.eiJobStatusCallbacks).hasSize(1));
+        assertThat(consumerResults.eiJobStatusCallbacks.get(0).state)
+            .isEqualTo(A1eEiJobStatus.EiJobStatusValues.ENABLED);
+        verifyJobStatus(EI_JOB_ID, "ENABLED");
+    }
+
+    @Test
+    void testGetStatus() throws JsonMappingException, JsonProcessingException, ServiceException {
+        putInfoProducerWithOneTypeRejecting("simulateProducerError", TYPE_ID);
+        putInfoProducerWithOneTypeRejecting("simulateProducerError2", TYPE_ID);
+
+        String url = "/status";
+        ResponseEntity<String> resp = restClient().getForEntity(url).block();
+        assertThat(resp.getBody()).contains("hunky dory");
+    }
+
+    @Test
+    void testEiJobDatabase() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId1");
+        putInfoJob(TYPE_ID, "jobId2");
+
+        assertThat(this.infoJobs.size()).isEqualTo(2);
+
+        {
+            InfoJob savedJob = this.infoJobs.getJob("jobId1");
+            // Restore the jobs
+            InfoJobs jobs = new InfoJobs(this.applicationConfig, this.producerCallbacks);
+            jobs.restoreJobsFromDatabase();
+            assertThat(jobs.size()).isEqualTo(2);
+            InfoJob restoredJob = jobs.getJob("jobId1");
+            assertThat(restoredJob.getId()).isEqualTo("jobId1");
+            assertThat(restoredJob.getLastUpdated()).isEqualTo(savedJob.getLastUpdated());
+
+            jobs.remove("jobId1", this.infoProducers);
+            jobs.remove("jobId2", this.infoProducers);
+        }
+        {
+            // Restore the jobs, no jobs in database
+            InfoJobs jobs = new InfoJobs(this.applicationConfig, this.producerCallbacks);
+            jobs.restoreJobsFromDatabase();
+            assertThat(jobs.size()).isZero();
+        }
+        logger.warn("Test removing a job when the db file is gone");
+        this.infoJobs.remove("jobId1", this.infoProducers);
+        assertThat(this.infoJobs.size()).isEqualTo(1);
+
+        ProducerSimulatorController.TestResults simulatorResults = this.producerSimulator.getTestResults();
+        await().untilAsserted(() -> assertThat(simulatorResults.jobsStopped).hasSize(3));
+    }
+
+    @Test
+    void testEiTypesDatabase() throws Exception {
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+
+        assertThat(this.infoTypes.size()).isEqualTo(1);
+
+        {
+            // Restore the types
+            InfoTypes types = new InfoTypes(this.applicationConfig);
+            types.restoreTypesFromDatabase();
+            assertThat(types.size()).isEqualTo(1);
+        }
+        {
+            // Restore the jobs, no jobs in database
+            InfoTypes types = new InfoTypes(this.applicationConfig);
+            types.clear();
+            types.restoreTypesFromDatabase();
+            assertThat(types.size()).isZero();
+        }
+        logger.warn("Test removing a job when the db file is gone");
+        this.infoTypes.remove(this.infoTypes.getType(TYPE_ID));
+        assertThat(this.infoJobs.size()).isZero();
+    }
+
+    @Test
+    void testConsumerTypeSubscriptionDatabase() {
+        final String callbackUrl = baseUrl() + ConsumerSimulatorController.getTypeStatusCallbackUrl();
+        final ConsumerTypeSubscriptionInfo info = new ConsumerTypeSubscriptionInfo(callbackUrl, "owner");
+
+        // PUT a subscription
+        String body = gson.toJson(info);
+        restClient().putForEntity(typeSubscriptionUrl() + "/subscriptionId", body).block();
+        assertThat(this.infoTypeSubscriptions.size()).isEqualTo(1);
+
+        InfoTypeSubscriptions restoredSubscriptions = new InfoTypeSubscriptions(this.applicationConfig);
+        assertThat(restoredSubscriptions.size()).isEqualTo(1);
+        assertThat(restoredSubscriptions.getSubscriptionsForOwner("owner")).hasSize(1);
+
+        // Delete the subscription
+        restClient().deleteForEntity(typeSubscriptionUrl() + "/subscriptionId").block();
+        restoredSubscriptions = new InfoTypeSubscriptions(this.applicationConfig);
+        assertThat(restoredSubscriptions.size()).isZero();
+    }
+
+    @Test
+    void testConsumerTypeSubscription() throws Exception {
+
+        final String callbackUrl = baseUrl() + ConsumerSimulatorController.getTypeStatusCallbackUrl();
+        final ConsumerTypeSubscriptionInfo info = new ConsumerTypeSubscriptionInfo(callbackUrl, "owner");
+
+        testErrorCode(restClient().get(typeSubscriptionUrl() + "/junk"), HttpStatus.NOT_FOUND,
+            "Could not find Information subscription: junk");
+
+        testErrorCode(restClient().delete(typeSubscriptionUrl() + "/junk"), HttpStatus.NOT_FOUND,
+            "Could not find Information subscription: junk");
+
+        {
+            // PUT a subscription
+            String body = gson.toJson(info);
+            ResponseEntity<String> resp =
+                restClient().putForEntity(typeSubscriptionUrl() + "/subscriptionId", body).block();
+            assertThat(this.infoTypeSubscriptions.size()).isEqualTo(1);
+            assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
+            resp = restClient().putForEntity(typeSubscriptionUrl() + "/subscriptionId", body).block();
+            assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK);
+        }
+        {
+            // GET IDs
+            ResponseEntity<String> resp = restClient().getForEntity(typeSubscriptionUrl()).block();
+            assertThat(resp.getBody()).isEqualTo("[\"subscriptionId\"]");
+            resp = restClient().getForEntity(typeSubscriptionUrl() + "?owner=owner").block();
+            assertThat(resp.getBody()).isEqualTo("[\"subscriptionId\"]");
+            resp = restClient().getForEntity(typeSubscriptionUrl() + "?owner=junk").block();
+            assertThat(resp.getBody()).isEqualTo("[]");
+        }
+
+        {
+            // GET the individual subscription
+            ResponseEntity<String> resp = restClient().getForEntity(typeSubscriptionUrl() + "/subscriptionId").block();
+            ConsumerTypeSubscriptionInfo respInfo = gson.fromJson(resp.getBody(), ConsumerTypeSubscriptionInfo.class);
+            assertThat(respInfo).isEqualTo(info);
+        }
+
+        {
+            // Test the callbacks
+            final ConsumerSimulatorController.TestResults consumerCalls = this.consumerSimulator.getTestResults();
+
+            // Test callback for PUT type
+            this.putInfoType(TYPE_ID);
+            await().untilAsserted(() -> assertThat(consumerCalls.typeRegistrationInfoCallbacks).hasSize(1));
+            assertThat(consumerCalls.typeRegistrationInfoCallbacks.get(0).state)
+                .isEqualTo(ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.REGISTERED);
+
+            // Test callback for DELETE type
+            this.deleteInfoType(TYPE_ID);
+            await().untilAsserted(() -> assertThat(consumerCalls.typeRegistrationInfoCallbacks).hasSize(2));
+            assertThat(consumerCalls.typeRegistrationInfoCallbacks.get(1).state)
+                .isEqualTo(ConsumerTypeRegistrationInfo.ConsumerTypeStatusValues.DEREGISTERED);
+        }
+
+        {
+            // DELETE the subscription
+            ResponseEntity<String> resp =
+                restClient().deleteForEntity(typeSubscriptionUrl() + "/subscriptionId").block();
+            assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
+            assertThat(this.infoTypeSubscriptions.size()).isZero();
+            resp = restClient().getForEntity(typeSubscriptionUrl()).block();
+            assertThat(resp.getBody()).isEqualTo("[]");
+        }
+    }
+
+    @Test
+    void testRemovingNonWorkingSubscription() throws Exception {
+        // Test that subscriptions are removed for a unresponsive consumer
+
+        // PUT a subscription with a junk callback
+        final ConsumerTypeSubscriptionInfo info = new ConsumerTypeSubscriptionInfo(baseUrl() + "/JUNK", "owner");
+        String body = gson.toJson(info);
+        restClient().putForEntity(typeSubscriptionUrl() + "/subscriptionId", body).block();
+        assertThat(this.infoTypeSubscriptions.size()).isEqualTo(1);
+
+        this.putInfoType(TYPE_ID);
+        // The callback will fail and the subscription will be removed
+        await().untilAsserted(() -> assertThat(this.infoTypeSubscriptions.size()).isZero());
+    }
+
+    @Test
+    void testTypeSubscriptionErrorCodes() throws Exception {
+
+        testErrorCode(restClient().get(typeSubscriptionUrl() + "/junk"), HttpStatus.NOT_FOUND,
+            "Could not find Information subscription: junk");
+
+        testErrorCode(restClient().delete(typeSubscriptionUrl() + "/junk"), HttpStatus.NOT_FOUND,
+            "Could not find Information subscription: junk");
+    }
+
+    @Test
+    void testAuthHeader() throws Exception {
+        final String AUTH_TOKEN = "testToken";
+        Path authFile = Files.createTempFile("icsTestAuthToken", ".txt");
+        Files.write(authFile, AUTH_TOKEN.getBytes());
+        this.securityContext.setAuthTokenFilePath(authFile);
+        putInfoProducerWithOneType(PRODUCER_ID, TYPE_ID);
+        putInfoJob(TYPE_ID, "jobId");
+
+        // Test that authorization header is sent to the producer.
+        await().untilAsserted(() -> assertThat(this.producerSimulator.getTestResults().receivedHeaders).hasSize(1));
+        Map<String, String> headers = this.producerSimulator.getTestResults().receivedHeaders.get(0);
+        assertThat(headers).containsEntry("authorization", "Bearer " + AUTH_TOKEN);
+
+        Files.delete(authFile);
+
+        // Test that it works. The cached header is used
+        putInfoJob(TYPE_ID, "jobId2");
+        await().untilAsserted(() -> assertThat(this.infoJobs.size()).isEqualByComparingTo(2));
+        headers = this.producerSimulator.getTestResults().receivedHeaders.get(1);
+        assertThat(headers).containsEntry("authorization", "Bearer " + AUTH_TOKEN);
+
+    }
+
+    private String typeSubscriptionUrl() {
+        return ConsumerConsts.API_ROOT + "/info-type-subscription";
+    }
+
+    private void deleteInfoProducer(String infoProducerId) {
+        String url = ProducerConsts.API_ROOT + "/info-producers/" + infoProducerId;
+        restClient().deleteForEntity(url).block();
+    }
+
+    private void verifyJobStatus(String jobId, String expStatus) {
+        String url = A1eConsts.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 + "/info-producers/" + producerId + "/status";
+        ResponseEntity<String> resp = restClient().getForEntity(statusUrl).block();
+        ProducerStatusInfo statusInfo = gson.fromJson(resp.getBody(), ProducerStatusInfo.class);
+        assertThat(statusInfo.opState).isEqualTo(expectedOperationalState);
+    }
+
+    ProducerInfoTypeInfo ProducerInfoTypeRegistrationInfo(String typeId)
+        throws JsonMappingException, JsonProcessingException {
+        return new ProducerInfoTypeInfo(jsonSchemaObject(), typeSpecifcInfoObject());
+    }
+
+    ProducerRegistrationInfo producerEiRegistratioInfoRejecting(String typeId)
+        throws JsonMappingException, JsonProcessingException {
+        return new ProducerRegistrationInfo(Arrays.asList(typeId), //
+            baseUrl() + ProducerSimulatorController.JOB_ERROR_URL,
+            baseUrl() + ProducerSimulatorController.SUPERVISION_ERROR_URL);
+    }
+
+    ProducerRegistrationInfo producerInfoRegistratioInfo(String typeId)
+        throws JsonMappingException, JsonProcessingException {
+        return new ProducerRegistrationInfo(Arrays.asList(typeId), //
+            baseUrl() + ProducerSimulatorController.JOB_URL, baseUrl() + ProducerSimulatorController.SUPERVISION_URL);
+    }
+
+    private ConsumerJobInfo consumerJobInfo() throws JsonMappingException, JsonProcessingException {
+        return consumerJobInfo(TYPE_ID, EI_JOB_ID);
+    }
+
+    ConsumerJobInfo consumerJobInfo(String typeId, String infoJobId)
+        throws JsonMappingException, JsonProcessingException {
+        return new ConsumerJobInfo(typeId, jsonObject(), "owner", "https://junk.com",
+            baseUrl() + A1eCallbacksSimulatorController.getJobStatusUrl(infoJobId));
+    }
+
+    private A1eEiJobInfo infoJobInfo() throws JsonMappingException, JsonProcessingException {
+        return infoJobInfo(TYPE_ID, EI_JOB_ID);
+    }
+
+    A1eEiJobInfo infoJobInfo(String typeId, String infoJobId) throws JsonMappingException, JsonProcessingException {
+        return new A1eEiJobInfo(typeId, jsonObject(), "owner", "https://junk.com",
+            baseUrl() + A1eCallbacksSimulatorController.getJobStatusUrl(infoJobId));
+    }
+
+    private Object jsonObject(String json) {
+        try {
+            return JsonParser.parseString(json).getAsJsonObject();
+        } catch (Exception e) {
+            throw new NullPointerException(e.toString());
+        }
+    }
+
+    private Object typeSpecifcInfoObject() {
+        return jsonObject("{ \"propertyName\" : \"value\" }");
+    }
+
+    private Object jsonSchemaObject() {
+        // a json schema with one mandatory property named "string"
+        String schemaStr = "{" //
+            + "\"$schema\": \"http://json-schema.org/draft-04/schema#\"," //
+            + "\"type\": \"object\"," //
+            + "\"properties\": {" //
+            + EI_JOB_PROPERTY + " : {" //
+            + "    \"type\": \"string\"" //
+            + "  }" //
+            + "}," //
+            + "\"required\": [" //
+            + EI_JOB_PROPERTY //
+            + "]" //
+            + "}"; //
+        return jsonObject(schemaStr);
+    }
+
+    private Object jsonObject() {
+        return jsonObject("{ " + EI_JOB_PROPERTY + " : \"value\" }");
+    }
+
+    private InfoJob putInfoJob(String infoTypeId, String jobId)
+        throws JsonMappingException, JsonProcessingException, ServiceException {
+
+        String url = A1eConsts.API_ROOT + "/eijobs/" + jobId;
+        String body = gson.toJson(infoJobInfo(infoTypeId, jobId));
+        restClient().putForEntity(url, body).block();
+
+        return this.infoJobs.getJob(jobId);
+    }
+
+    private HttpStatus putInfoType(String infoTypeId)
+        throws JsonMappingException, JsonProcessingException, ServiceException {
+        String url = ProducerConsts.API_ROOT + "/info-types/" + infoTypeId;
+        String body = gson.toJson(ProducerInfoTypeRegistrationInfo(infoTypeId));
+
+        ResponseEntity<String> resp = restClient().putForEntity(url, body).block();
+        this.infoTypes.getType(infoTypeId);
+        return resp.getStatusCode();
+    }
+
+    private String deleteInfoTypeUrl(String typeId) {
+        return ProducerConsts.API_ROOT + "/info-types/" + typeId;
+    }
+
+    private void deleteInfoType(String typeId) {
+        restClient().delete(deleteInfoTypeUrl(typeId)).block();
+    }
+
+    private InfoType putInfoProducerWithOneTypeRejecting(String producerId, String infoTypeId)
+        throws JsonMappingException, JsonProcessingException, ServiceException {
+        this.putInfoType(infoTypeId);
+        String url = ProducerConsts.API_ROOT + "/info-producers/" + producerId;
+        String body = gson.toJson(producerEiRegistratioInfoRejecting(infoTypeId));
+        restClient().putForEntity(url, body).block();
+        return this.infoTypes.getType(infoTypeId);
+    }
+
+    private InfoType putInfoProducerWithOneType(String producerId, String infoTypeId)
+        throws JsonMappingException, JsonProcessingException, ServiceException {
+        this.putInfoType(infoTypeId);
+
+        String url = ProducerConsts.API_ROOT + "/info-producers/" + producerId;
+        String body = gson.toJson(producerInfoRegistratioInfo(infoTypeId));
+
+        restClient().putForEntity(url, body).block();
+
+        return this.infoTypes.getType(infoTypeId);
+    }
+
+    private String baseUrl() {
+        return "https://localhost:" + this.port;
+    }
+
+    private AsyncRestClient restClient(boolean useTrustValidation) {
+        WebClientConfig config = this.applicationConfig.getWebClientConfig();
+        HttpProxyConfig httpProxyConfig = ImmutableHttpProxyConfig.builder() //
+            .httpProxyHost("") //
+            .httpProxyPort(0) //
+            .build();
+        config = ImmutableWebClientConfig.builder() //
+            .keyStoreType(config.keyStoreType()) //
+            .keyStorePassword(config.keyStorePassword()) //
+            .keyStore(config.keyStore()) //
+            .keyPassword(config.keyPassword()) //
+            .isTrustStoreUsed(useTrustValidation) //
+            .trustStore(config.trustStore()) //
+            .trustStorePassword(config.trustStorePassword()) //
+            .httpProxyConfig(httpProxyConfig).build();
+
+        AsyncRestClientFactory restClientFactory = new AsyncRestClientFactory(config, securityContext);
+        return restClientFactory.createRestClientNoHttpProxy(baseUrl());
+    }
+
+    private AsyncRestClient restClient() {
+        return restClient(false);
+    }
+
+    private void testErrorCode(Mono<?> request, HttpStatus expStatus, String responseContains) {
+        testErrorCode(request, expStatus, responseContains, true);
+    }
+
+    private void testErrorCode(Mono<?> request, HttpStatus expStatus, String responseContains,
+        boolean expectApplicationProblemJsonMediaType) {
+        StepVerifier.create(request) //
+            .expectSubscription() //
+            .expectErrorMatches(
+                t -> checkWebClientError(t, expStatus, responseContains, expectApplicationProblemJsonMediaType)) //
+            .verify();
+    }
+
+    private boolean checkWebClientError(Throwable throwable, HttpStatus expStatus, String responseContains,
+        boolean expectApplicationProblemJsonMediaType) {
+        assertTrue(throwable instanceof WebClientResponseException);
+        WebClientResponseException responseException = (WebClientResponseException) throwable;
+        assertThat(responseException.getStatusCode()).isEqualTo(expStatus);
+        assertThat(responseException.getResponseBodyAsString()).contains(responseContains);
+        if (expectApplicationProblemJsonMediaType) {
+            assertThat(responseException.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON);
+        }
+        return true;
+    }
+
+}
diff --git a/src/test/java/org/oransc/ics/MockInformationService.java b/src/test/java/org/oransc/ics/MockInformationService.java
new file mode 100644 (file)
index 0000000..65c19a8
--- /dev/null
@@ -0,0 +1,54 @@
+/*-
+ * ========================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.ics;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
+@TestPropertySource(
+    properties = { //
+        "server.ssl.key-store=./config/keystore.jks", //
+        "app.webclient.trust-store=./config/truststore.jks", "app.vardata-directory=./target"})
+@SuppressWarnings("squid:S3577") // Not containing any tests since it is a mock.
+class MockInformationService {
+    private static final Logger logger = LoggerFactory.getLogger(ApplicationTest.class);
+
+    @LocalServerPort
+    private int port;
+
+    @Test
+    @SuppressWarnings("squid:S2699")
+    void runMock() throws Exception {
+        logger.warn("**************** Keeping server alive! " + this.port);
+        synchronized (this) {
+            this.wait();
+        }
+    }
+}
diff --git a/src/test/java/org/oransc/ics/clients/AsyncRestClientTest.java b/src/test/java/org/oransc/ics/clients/AsyncRestClientTest.java
new file mode 100644 (file)
index 0000000..3f5f78d
--- /dev/null
@@ -0,0 +1,163 @@
+/*-
+ * ========================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.ics.clients;
+
+import io.netty.util.internal.logging.InternalLoggerFactory;
+import io.netty.util.internal.logging.JdkLoggerFactory;
+
+import java.io.IOException;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+import reactor.util.Loggers;
+
+class AsyncRestClientTest {
+    private static final String BASE_URL = "BaseUrl";
+    private static final String REQUEST_URL = "/test";
+    private static final String USERNAME = "username";
+    private static final String PASSWORD = "password";
+    private static final String TEST_JSON = "{\"type\":\"type1\"}";
+    private static final int SUCCESS_CODE = 200;
+    private static final int ERROR_CODE = 500;
+
+    private static MockWebServer mockWebServer;
+
+    private static AsyncRestClient clientUnderTest;
+
+    private static final SecurityContext securityContext = new SecurityContext("");
+
+    @BeforeAll
+    static void init() {
+        // skip a lot of unnecessary logs from MockWebServer
+        InternalLoggerFactory.setDefaultFactory(JdkLoggerFactory.INSTANCE);
+        Loggers.useJdkLoggers();
+        mockWebServer = new MockWebServer();
+        clientUnderTest = new AsyncRestClient(mockWebServer.url(BASE_URL).toString(), null, null, securityContext);
+    }
+
+    @AfterAll
+    static void tearDown() throws IOException {
+        mockWebServer.shutdown();
+    }
+
+    @Test
+    void testGetNoError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(SUCCESS_CODE) //
+            .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) //
+            .setBody(TEST_JSON));
+
+        Mono<String> returnedMono = clientUnderTest.get(REQUEST_URL);
+        StepVerifier.create(returnedMono).expectNext(TEST_JSON).expectComplete().verify();
+    }
+
+    @Test
+    void testGetError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(ERROR_CODE));
+
+        Mono<String> returnedMono = clientUnderTest.get(REQUEST_URL);
+        StepVerifier.create(returnedMono)
+            .expectErrorMatches(throwable -> throwable instanceof WebClientResponseException).verify();
+    }
+
+    @Test
+    void testPutNoError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(SUCCESS_CODE) //
+            .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) //
+            .setBody(TEST_JSON));
+
+        Mono<String> returnedMono = clientUnderTest.put(REQUEST_URL, TEST_JSON);
+        StepVerifier.create(returnedMono).expectNext(TEST_JSON).expectComplete().verify();
+    }
+
+    @Test
+    void testPutError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(ERROR_CODE));
+
+        Mono<String> returnedMono = clientUnderTest.put(REQUEST_URL, TEST_JSON);
+        StepVerifier.create(returnedMono)
+            .expectErrorMatches(throwable -> throwable instanceof WebClientResponseException).verify();
+    }
+
+    @Test
+    void testDeleteNoError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(SUCCESS_CODE));
+
+        Mono<String> returnedMono = clientUnderTest.delete(REQUEST_URL);
+        StepVerifier.create(returnedMono).expectNext("").expectComplete().verify();
+    }
+
+    @Test
+    void testDeleteError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(ERROR_CODE));
+
+        Mono<String> returnedMono = clientUnderTest.delete(REQUEST_URL);
+        StepVerifier.create(returnedMono)
+            .expectErrorMatches(throwable -> throwable instanceof WebClientResponseException).verify();
+    }
+
+    @Test
+    void testPostNoError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(SUCCESS_CODE) //
+            .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) //
+            .setBody(TEST_JSON));
+
+        Mono<String> returnedMono = clientUnderTest.post(REQUEST_URL, TEST_JSON);
+        StepVerifier.create(returnedMono).expectNext(TEST_JSON).expectComplete().verify();
+    }
+
+    @Test
+    void testPostError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(ERROR_CODE));
+
+        Mono<String> returnedMono = clientUnderTest.post(REQUEST_URL, TEST_JSON);
+        StepVerifier.create(returnedMono)
+            .expectErrorMatches(throwable -> throwable instanceof WebClientResponseException).verify();
+    }
+
+    @Test
+    void testPostWithAuthHeaderNoError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(SUCCESS_CODE) //
+            .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) //
+            .setBody(TEST_JSON));
+
+        Mono<String> returnedMono = clientUnderTest.postWithAuthHeader(REQUEST_URL, TEST_JSON, USERNAME, PASSWORD);
+        StepVerifier.create(returnedMono).expectNext(TEST_JSON).expectComplete().verify();
+    }
+
+    @Test
+    void testPostWithAuthHeaderError() {
+        mockWebServer.enqueue(new MockResponse().setResponseCode(ERROR_CODE));
+
+        Mono<String> returnedMono = clientUnderTest.postWithAuthHeader(REQUEST_URL, TEST_JSON, USERNAME, PASSWORD);
+        StepVerifier.create(returnedMono)
+            .expectErrorMatches(throwable -> throwable instanceof WebClientResponseException).verify();
+    }
+}
diff --git a/src/test/java/org/oransc/ics/controller/A1eCallbacksSimulatorController.java b/src/test/java/org/oransc/ics/controller/A1eCallbacksSimulatorController.java
new file mode 100644 (file)
index 0000000..8e09310
--- /dev/null
@@ -0,0 +1,94 @@
+/*-
+ * ========================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.ics.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import lombok.Getter;
+
+import org.oransc.ics.controllers.VoidResponse;
+import org.oransc.ics.controllers.a1e.A1eConsts;
+import org.oransc.ics.controllers.a1e.A1eEiJobStatus;
+import org.oransc.ics.controllers.r1consumer.ConsumerConsts;
+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("A1eCallbacksSimulatorController")
+@Tag(name = A1eConsts.CONSUMER_API_CALLBACKS_NAME, description = A1eConsts.CONSUMER_API_CALLBACKS_DESCRIPTION)
+public class A1eCallbacksSimulatorController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    public static class TestResults {
+
+        public List<A1eEiJobStatus> eiJobStatusCallbacks =
+            Collections.synchronizedList(new ArrayList<A1eEiJobStatus>());
+
+        public void reset() {
+            eiJobStatusCallbacks.clear();
+        }
+    }
+
+    @Getter
+    private TestResults testResults = new TestResults();
+
+    public static String getJobStatusUrl(String infoJobId) {
+        return "/example-dataconsumer/info-jobs/" + infoJobId + "/status";
+    }
+
+    @PostMapping(
+        path = "/example-dataconsumer/info-jobs/{infoJobId}/status",
+        produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Callback for changed Information Job status",
+        description = "The primitive is implemented by the data consumer and is invoked when a Information Job status has been changed.")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public ResponseEntity<Object> jobStatusCallback( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId, //
+        @RequestBody A1eEiJobStatus status) {
+        logger.info("Job status callback status: {} infoJobId: {}", status.state, infoJobId);
+        this.testResults.eiJobStatusCallbacks.add(status);
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+}
diff --git a/src/test/java/org/oransc/ics/controller/ConsumerSimulatorController.java b/src/test/java/org/oransc/ics/controller/ConsumerSimulatorController.java
new file mode 100644 (file)
index 0000000..4f2edcd
--- /dev/null
@@ -0,0 +1,93 @@
+/*-
+ * ========================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.ics.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import lombok.Getter;
+
+import org.oransc.ics.controllers.VoidResponse;
+import org.oransc.ics.controllers.r1consumer.ConsumerConsts;
+import org.oransc.ics.controllers.r1consumer.ConsumerTypeRegistrationInfo;
+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.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController("ConsumerSimulatorController")
+@Tag(name = ConsumerConsts.CONSUMER_API_CALLBACKS_NAME, description = ConsumerConsts.CONSUMER_API_DESCRIPTION)
+public class ConsumerSimulatorController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    public static class TestResults {
+
+        public List<ConsumerTypeRegistrationInfo> typeRegistrationInfoCallbacks =
+            Collections.synchronizedList(new ArrayList<ConsumerTypeRegistrationInfo>());
+
+        public void reset() {
+            typeRegistrationInfoCallbacks.clear();
+        }
+    }
+
+    @Getter
+    private TestResults testResults = new TestResults();
+
+    private static final String TYPE_STATUS_CALLBACK_URL = "/example-dataconsumer/info-type-status";
+
+    public static String getTypeStatusCallbackUrl() {
+        return TYPE_STATUS_CALLBACK_URL;
+    }
+
+    @PostMapping(path = TYPE_STATUS_CALLBACK_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Callback for changed Information type registration status",
+        description = "The primitive is implemented by the data consumer and is invoked when a Information type status has been changed. <br/>"
+            + "Subscription are managed by primitives in '" + ConsumerConsts.CONSUMER_API_NAME + "'")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public ResponseEntity<Object> typeStatusCallback( //
+        @RequestBody ConsumerTypeRegistrationInfo status) {
+        logger.info("Job type registration status callback status: {}", status);
+        this.testResults.typeRegistrationInfoCallbacks.add(status);
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+
+}
diff --git a/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java b/src/test/java/org/oransc/ics/controller/ProducerSimulatorController.java
new file mode 100644 (file)
index 0000000..a5ddc49
--- /dev/null
@@ -0,0 +1,215 @@
+/*-
+ * ========================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.ics.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import lombok.Getter;
+
+import org.oransc.ics.controllers.ErrorResponse;
+import org.oransc.ics.controllers.VoidResponse;
+import org.oransc.ics.controllers.r1consumer.ConsumerConsts;
+import org.oransc.ics.controllers.r1producer.ProducerConsts;
+import org.oransc.ics.controllers.r1producer.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.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController("ProducerSimulatorController")
+@Tag(name = ProducerConsts.PRODUCER_API_CALLBACKS_NAME, description = ProducerConsts.PRODUCER_API_CALLBACKS_DESCRIPTION)
+public class ProducerSimulatorController {
+
+    private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+    public static final String JOB_URL = "/example-dataproducer/info-job";
+    public static final String JOB_ERROR_URL = "/example-dataproducer/info-job-error";
+
+    public static final String SUPERVISION_URL = "/example-dataproducer/health-check";
+    public static final String SUPERVISION_ERROR_URL = "/example-dataproducer/health-check-error";
+
+    public static class TestResults {
+
+        public List<ProducerJobInfo> jobsStarted = Collections.synchronizedList(new ArrayList<ProducerJobInfo>());
+        public List<String> jobsStopped = Collections.synchronizedList(new ArrayList<String>());
+        public List<Map<String, String>> receivedHeaders =
+            Collections.synchronizedList(new ArrayList<Map<String, String>>());
+        public int noOfRejectedCreate = 0;
+        public int noOfRejectedDelete = 0;
+        public boolean errorFound = false;
+
+        public TestResults() {
+        }
+
+        public void reset() {
+            jobsStarted.clear();
+            jobsStopped.clear();
+            receivedHeaders.clear();
+            this.errorFound = false;
+            this.noOfRejectedCreate = 0;
+            this.noOfRejectedDelete = 0;
+        }
+    }
+
+    @Getter
+    private TestResults testResults = new TestResults();
+
+    @PostMapping(path = JOB_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Callback for Information Job creation/modification",
+        description = "The call is invoked to activate or to modify a data subscription. The endpoint is provided by the Information Producer.")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public ResponseEntity<Object> jobCreatedCallback( //
+        @RequestHeader Map<String, String> headers, //
+        @RequestBody ProducerJobInfo request) {
+        try {
+            logHeaders(headers);
+            this.testResults.jobsStarted.add(request);
+            this.testResults.receivedHeaders.add(headers);
+            logger.info("Job started callback {}", request.id);
+            if (request.id == null) {
+                throw new NullPointerException("Illegal argument");
+            }
+            return new ResponseEntity<>(HttpStatus.OK);
+        } catch (Exception e) {
+            this.testResults.errorFound = true;
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @DeleteMapping(path = JOB_URL + "/{infoJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Callback for Information Job deletion",
+        description = "The call is invoked to terminate a data subscription. The endpoint is provided by the Information Producer.")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public ResponseEntity<Object> jobDeletedCallback( //
+        @RequestHeader Map<String, String> headers, @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) {
+        try {
+            logHeaders(headers);
+            logger.info("Job deleted callback {}", infoJobId);
+            this.testResults.jobsStopped.add(infoJobId);
+            this.testResults.receivedHeaders.add(headers);
+            return new ResponseEntity<>(HttpStatus.OK);
+        } catch (Exception e) {
+            return ErrorResponse.create(e, HttpStatus.NOT_FOUND);
+        }
+    }
+
+    @PostMapping(path = JOB_ERROR_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Callback for Information Job creation, returns error", description = "", hidden = true)
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public ResponseEntity<Object> jobCreatedCallbackReturnError( //
+        @RequestBody ProducerJobInfo request) {
+        logger.info("Job created (returning error) callback {}", request.id);
+        this.testResults.noOfRejectedCreate += 1;
+        return ErrorResponse.create("Producer returns error on create job", HttpStatus.NOT_FOUND);
+    }
+
+    @DeleteMapping(path = JOB_ERROR_URL + "/{infoJobId}", produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Callback for Information Job deletion, returns error", description = "", hidden = true)
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = VoidResponse.class))) //
+        })
+    public ResponseEntity<Object> jobDeletedCallbackReturnError( //
+        @PathVariable(ConsumerConsts.INFO_JOB_ID_PATH) String infoJobId) {
+        logger.info("Job created (returning error) callback {}", infoJobId);
+        this.testResults.noOfRejectedDelete += 1;
+        return ErrorResponse.create("Producer returns error on delete job", HttpStatus.NOT_FOUND);
+    }
+
+    @GetMapping(path = SUPERVISION_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(
+        summary = "Producer supervision",
+        description = "The endpoint is provided by the Information Producer and is used for supervision of the producer.")
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "The producer is OK", //
+                content = @Content(schema = @Schema(implementation = String.class))) //
+        })
+    public ResponseEntity<Object> producerSupervision() {
+        logger.info("Producer supervision");
+        return new ResponseEntity<>("Hunky dory", HttpStatus.OK);
+    }
+
+    @GetMapping(path = SUPERVISION_ERROR_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+    @Operation(summary = "Producer supervision error", description = "", hidden = true)
+    @ApiResponses(
+        value = { //
+            @ApiResponse(
+                responseCode = "200",
+                description = "OK", //
+                content = @Content(schema = @Schema(implementation = String.class))) //
+        })
+    public ResponseEntity<Object> producerSupervisionError() {
+        logger.info("Producer supervision error");
+        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+    }
+
+    private void logHeaders(Map<String, String> headers) {
+        logger.debug("Header begin");
+        headers.forEach((key, value) -> logger.debug("  key: {}, value: {}", key, value));
+        logger.debug("Header end");
+    }
+
+}
diff --git a/tox.ini b/tox.ini
new file mode 100644 (file)
index 0000000..2705e16
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,37 @@
+# ==================================================================================
+#       Copyright (c) 2020 Nordix
+#
+#   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.
+# ==================================================================================
+
+# documentation only
+[tox]
+minversion = 2.0
+envlist =
+    docs,
+    docs-linkcheck,
+skipsdist = true
+
+[testenv:docs]
+basepython = python3
+deps = -r{toxinidir}/docs/requirements-docs.txt
+
+commands =
+    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 = -r{toxinidir}/docs/requirements-docs.txt
+commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck