From d1d085456c485599f6b8aba87b6d761b29c2ecd4 Mon Sep 17 00:00:00 2001 From: PatrikBuhr Date: Fri, 11 Sep 2020 16:50:37 +0200 Subject: [PATCH] Enrichment Coordination Service First part. Consumer API defined. Change-Id: I0841cba636c2e79ed0e54978641434e83aff77e5 Issue-ID: NONRTRIC-173 Signed-off-by: PatrikBuhr --- enrichment-coordinator-service/config/README | 41 +++ .../config/application.yaml | 37 ++ enrichment-coordinator-service/config/keystore.jks | Bin 0 -> 4987 bytes .../config/truststore.jks | Bin 0 -> 3683 bytes enrichment-coordinator-service/docs/api.yaml | 300 ++++++++++++++++ .../eclipse-formatter.xml | 314 +++++++++++++++++ enrichment-coordinator-service/pom.xml | 382 +++++++++++++++++++++ .../java/org/oransc/enrichment/Application.java | 33 ++ .../java/org/oransc/enrichment/BeanFactory.java | 80 +++++ .../java/org/oransc/enrichment/SwaggerConfig.java | 95 +++++ .../oransc/enrichment/clients/AsyncRestClient.java | 334 ++++++++++++++++++ .../configuration/ApplicationConfig.java | 72 ++++ .../enrichment/configuration/WebClientConfig.java | 45 +++ .../enrichment/controllers/ErrorResponse.java | 106 ++++++ .../controllers/consumer/ConsumerConsts.java | 32 ++ .../controllers/consumer/ConsumerController.java | 258 ++++++++++++++ .../controllers/consumer/ConsumerEiJobInfo.java | 52 +++ .../controllers/consumer/ConsumerEiJobStatus.java | 54 +++ .../controllers/consumer/ConsumerEiTypeInfo.java | 43 +++ .../enrichment/exceptions/ServiceException.java | 32 ++ .../org/oransc/enrichment/repository/EiJob.java | 40 +++ .../org/oransc/enrichment/repository/EiJobs.java | 106 ++++++ .../org/oransc/enrichment/repository/EiType.java | 32 ++ .../org/oransc/enrichment/repository/EiTypes.java | 68 ++++ .../org/oransc/enrichment/ApplicationTest.java | 275 +++++++++++++++ 25 files changed, 2831 insertions(+) create mode 100644 enrichment-coordinator-service/config/README create mode 100644 enrichment-coordinator-service/config/application.yaml create mode 100644 enrichment-coordinator-service/config/keystore.jks create mode 100644 enrichment-coordinator-service/config/truststore.jks create mode 100644 enrichment-coordinator-service/docs/api.yaml create mode 100644 enrichment-coordinator-service/eclipse-formatter.xml create mode 100644 enrichment-coordinator-service/pom.xml create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/Application.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/AsyncRestClient.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/ApplicationConfig.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/WebClientConfig.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiTypeInfo.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/exceptions/ServiceException.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java create mode 100644 enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java diff --git a/enrichment-coordinator-service/config/README b/enrichment-coordinator-service/config/README new file mode 100644 index 00000000..140927f7 --- /dev/null +++ b/enrichment-coordinator-service/config/README @@ -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/enrichment-coordinator-service/config/application.yaml b/enrichment-coordinator-service/config/application.yaml new file mode 100644 index 00000000..6279a3e2 --- /dev/null +++ b/enrichment-coordinator-service/config/application.yaml @@ -0,0 +1,37 @@ +spring: + profiles: + active: prod + main: + allow-bean-definition-overriding: true + aop: + auto: false +management: + endpoints: + web: + exposure: + include: "loggers,logfile,health,info,metrics,threaddump,heapdump" + +logging: + level: + ROOT: ERROR + org.springframework: ERROR + org.springframework.data: ERROR + org.springframework.web.reactive.function.client.ExchangeFunctions: ERROR + org.oransc.policyagent: INFO + file: /var/log/policy-agent/application.log +server: + port : 8433 + http-port: 8081 + ssl: + key-store-type: JKS + key-store-password: policy_agent + key-store: /opt/app/enrichment-coordinator-service/etc/cert/keystore.jks + key-password: policy_agent + key-alias: policy_agent +app: + filepath: /opt/app/enrichment-coordinator-service/data/application_configuration.json + webclient: + trust-store-used: false + trust-store-password: policy_agent + trust-store: /opt/app/enrichment-coordinator-service/etc/cert/truststore.jks + diff --git a/enrichment-coordinator-service/config/keystore.jks b/enrichment-coordinator-service/config/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..122997ac7b9ad8a58b7a167916bfdf808f3f3e4b GIT binary patch literal 4987 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 literal 0 HcmV?d00001 diff --git a/enrichment-coordinator-service/docs/api.yaml b/enrichment-coordinator-service/docs/api.yaml new file mode 100644 index 00000000..a5c07e7d --- /dev/null +++ b/enrichment-coordinator-service/docs/api.yaml @@ -0,0 +1,300 @@ +swagger: '2.0' +info: + description: This page lists all the rest apis for the service. + version: '1.0' + title: Enrichment Data service +host: 'localhost:8081' +basePath: / +tags: + - name: A1-E Enrichment Data Consumer API + description: Consumer Controller +paths: + /A1-EI/v1/eitypes: + get: + tags: + - A1-E Enrichment Data Consumer API + summary: Query EI type identifiers + description: DETAILS TBD + operationId: getEiTypeIdentifiersUsingGET + produces: + - application/json + responses: + '200': + description: EI type identifiers + schema: + type: array + items: + type: string + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Not Found + deprecated: false + '/A1-EI/v1/eitypes/{eiTypeId}': + get: + tags: + - A1-E Enrichment Data Consumer API + summary: Definitions for an individual EI Type + description: Query EI type + operationId: getEiTypeUsingGET + produces: + - application/json + parameters: + - name: eiTypeId + in: path + description: eiTypeId + required: true + type: string + responses: + '200': + description: EI type + schema: + $ref: '#/definitions/ei_type_info' + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Enrichment Information type is not found + schema: + $ref: '#/definitions/error_information' + deprecated: false + '/A1-EI/v1/eitypes/{eiTypeId}/eijobs': + get: + tags: + - A1-E Enrichment Data Consumer API + summary: Query EI job identifiers + description: Returns the identifiers for an EI Type + operationId: getEiJobIdsUsingGET + produces: + - application/json + parameters: + - name: eiTypeId + in: path + description: eiTypeId + required: true + type: string + - in: body + name: owner + description: identifies the owner of the job + required: false + schema: + type: string + responses: + '200': + description: EI type + schema: + type: array + items: + type: string + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Enrichment Information type is not found + schema: + $ref: '#/definitions/error_information' + deprecated: false + '/A1-EI/v1/eitypes/{eiTypeId}/eijobs/{eiJobId}': + get: + tags: + - A1-E Enrichment Data Consumer API + summary: Individual EI Job + operationId: getIndividualEiJobUsingGET + produces: + - application/json + parameters: + - name: eiJobId + in: path + description: eiJobId + required: true + type: string + - name: eiTypeId + in: path + description: eiTypeId + required: true + type: string + responses: + '200': + description: EI Job + schema: + $ref: '#/definitions/ei_job_info' + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Enrichment Information type or job is not found + schema: + $ref: '#/definitions/error_information' + deprecated: false + put: + tags: + - A1-E Enrichment Data Consumer API + summary: Individual EI Job + description: Create or update an EI Job + operationId: putIndividualEiJobUsingPUT + consumes: + - application/json + produces: + - application/json + parameters: + - name: eiJobId + in: path + description: eiJobId + required: true + type: string + - in: body + name: eiJobInfo + description: eiJobInfo + required: true + schema: + $ref: '#/definitions/ei_job_info' + - name: eiTypeId + in: path + description: eiTypeId + required: true + type: string + responses: + '200': + description: Job updated + schema: + type: object + '201': + description: Job created + schema: + type: object + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Enrichment Information type is not found + schema: + $ref: '#/definitions/error_information' + deprecated: false + delete: + tags: + - A1-E Enrichment Data Consumer API + summary: Individual EI Job + description: Delete an EI job + operationId: deleteIndividualEiJobUsingDELETE + produces: + - application/json + parameters: + - name: eiJobId + in: path + description: eiJobId + required: true + type: string + - name: eiTypeId + in: path + description: eiTypeId + required: true + type: string + responses: + '200': + description: Not used + schema: + type: object + '204': + description: Job deleted + schema: + type: object + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Enrichment Information type or job is not found + schema: + $ref: '#/definitions/error_information' + deprecated: false + '/A1-EI/v1/eitypes/{eiTypeId}/eijobs/{eiJobId}/status': + get: + tags: + - A1-E Enrichment Data Consumer API + summary: EI Job status + operationId: getEiJobStatusUsingGET + produces: + - application/json + parameters: + - name: eiJobId + in: path + description: eiJobId + required: true + type: string + - name: eiTypeId + in: path + description: eiTypeId + required: true + type: string + responses: + '200': + description: EI Job status + schema: + $ref: '#/definitions/ei_job_status' + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Enrichment Information type or job is not found + schema: + $ref: '#/definitions/error_information' + deprecated: false +definitions: + ei_job_info: + type: object + properties: + job_data: + type: object + description: EI Type specific job data + owner: + type: string + description: Identity of the owner of the job + result_target: + type: string + description: the deliver information for the EI. This is typically a URL. + title: ei_job_info + description: Information for a Enrichment Information Job + ei_job_status: + type: object + properties: + operational_state: + type: string + description: |- + Operational state, values: + ENABLED: TBD + DISABLED: TBD. + enum: + - ENABLED + - DISABLED + title: ei_job_status + description: Status for an EI Job + ei_type_info: + type: object + properties: + job_data_schema: + type: object + description: Json schema for the job data + title: ei_type_info + description: Information for an EI type + error_information: + type: object + properties: + detail: + type: string + example: EI job type not found + description: ' A human-readable explanation specific to this occurrence of the problem.' + status: + type: integer + format: int32 + example: 503 + description: 'The HTTP status code generated by the origin server for this occurrence of the problem. ' + title: error_information + description: 'Problem as defined in https://tools.ietf.org/html/rfc7807' + diff --git a/enrichment-coordinator-service/eclipse-formatter.xml b/enrichment-coordinator-service/eclipse-formatter.xml new file mode 100644 index 00000000..c8cca2ee --- /dev/null +++ b/enrichment-coordinator-service/eclipse-formatter.xml @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/enrichment-coordinator-service/pom.xml b/enrichment-coordinator-service/pom.xml new file mode 100644 index 00000000..e1884fc0 --- /dev/null +++ b/enrichment-coordinator-service/pom.xml @@ -0,0 +1,382 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.7.RELEASE + + + org.o-ran-sc.nonrtric + policy-agent + 2.1.0-SNAPSHOT + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + onap-releases + onap-releases + https://nexus.onap.org/content/repositories/releases/ + + + + 11 + 2.9.2 + 2.8.2 + 1.1.6 + 2.0.0 + 20190722 + 3.6 + 3.8.0 + 2.8.1 + 1.18.0 + 0.30.0 + 1.1.11 + 2.1.1 + 3.7.0.1746 + 0.8.5 + true + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework + spring-webflux + + + io.swagger.core.v3 + swagger-jaxrs2 + ${swagger.version} + + + io.swagger.core.v3 + swagger-jaxrs2-servlet-initializer + ${swagger.version} + + + javax.xml.bind + jaxb-api + + + org.immutables + value + ${immutable.version} + provided + + + org.immutables + gson + ${immutable.version} + + + org.json + json + ${json.version} + + + commons-net + commons-net + ${commons-net.version} + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.onap.dcaegen2.services.sdk.rest.services + cbs-client + ${sdk.version} + + + org.projectlombok + lombok + provided + + + org.onap.dmaap.messagerouter.dmaapclient + dmaapClient + ${version.dmaap} + + + javax.ws.rs + javax.ws.rs-api + ${javax.ws.rs-api.version} + + + org.glassfish.jersey.inject + jersey-hk2 + + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.springfox + springfox-swagger2 + ${springfox.version} + + + io.springfox + springfox-swagger-ui + ${springfox.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.awaitility + awaitility + test + + + io.projectreactor + reactor-test + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-junit-jupiter + test + + + org.mockito + mockito-core + test + + + com.squareup.okhttp3 + mockwebserver + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + net.revelc.code.formatter + formatter-maven-plugin + ${formatter-maven-plugin.version} + + ${project.basedir}/eclipse-formatter.xml + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + + + com,java,javax,org + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources/annotations/ + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + io.fabric8 + docker-maven-plugin + ${docker-maven-plugin} + false + + + generate-policy-agent-image + package + + build + + + ${env.CONTAINER_PULL_REGISTRY} + + + o-ran-sc/nonrtric-policy-agent:${project.version} + + try + ${basedir} + Dockerfile + + ${project.build.finalName}.jar + + + ${project.version} + + + + + + + + push-policy-agent-image + + build + push + + + ${env.CONTAINER_PULL_REGISTRY} + ${env.CONTAINER_PUSH_REGISTRY} + + + o-ran-sc/nonrtric-policy-agent:${project.version} + + ${basedir} + Dockerfile + + ${project.build.finalName}.jar + + + ${project.version} + latest + + + + + + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + org.codehaus.mojo + exec-maven-plugin + + + run-test-script + verify + + exec + + + + + bash + + run_test.sh + + ../test/jenkins/ + + + + + + JIRA + https://jira.o-ran-sc.org/ + + diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/Application.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/Application.java new file mode 100644 index 00000000..3c0156cb --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/Application.java @@ -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.enrichment; + +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/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java new file mode 100644 index 00000000..7b868f52 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/BeanFactory.java @@ -0,0 +1,80 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.catalina.connector.Connector; +import org.oransc.enrichment.configuration.ApplicationConfig; +import org.oransc.enrichment.repository.EiJobs; +import org.oransc.enrichment.repository.EiTypes; +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(); + + @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 EiJobs eiJobs() { + return new EiJobs(); + } + + @Bean + public EiTypes eiTypes() { + return new EiTypes(); + } + + @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/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java new file mode 100644 index 00000000..07dbefdf --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/SwaggerConfig.java @@ -0,0 +1,95 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment; + +import com.fasterxml.classmate.TypeResolver; +import com.google.common.base.Predicates; + +import org.oransc.enrichment.controllers.consumer.ConsumerEiJobInfo; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + * Swagger configuration class that uses swagger2 documentation type and scans + * all the controllers. To access the swagger gui go to + * http://ip:port/swagger-ui.html + */ +@Configuration +@EnableSwagger2 +public class SwaggerConfig extends WebMvcConfigurationSupport { + + static final String API_TITLE = "Enrichment Data service"; + static final String DESCRIPTION = "This page lists all the rest apis for the service."; + static final String VERSION = "1.0"; + @SuppressWarnings("squid:S1075") // Refactor your code to get this URI from a customizable parameter. + static final String RESOURCES_PATH = "classpath:/META-INF/resources/"; + static final String WEBJARS_PATH = RESOURCES_PATH + "webjars/"; + static final String SWAGGER_UI = "swagger-ui.html"; + static final String WEBJARS = "/webjars/**"; + + /** + * Gets the API info. + * + * @return the API info. + */ + @Bean + public Docket api(TypeResolver resolver) { + return new Docket(DocumentationType.SWAGGER_2) // + .apiInfo(apiInfo()) // + .additionalModels(resolver.resolve(ConsumerEiJobInfo.class)) // + .select() // + .apis(RequestHandlerSelectors.any()) // + .paths(PathSelectors.any()) // + .paths(Predicates.not(PathSelectors.regex("/error"))) // + // this endpoint is not implemented, but was visible for Swagger + .paths(Predicates.not(PathSelectors.regex("/actuator.*"))) // + // this endpoint is implemented by spring framework, exclude for now + .build(); + } + + private static ApiInfo apiInfo() { + return new ApiInfoBuilder() // + .title(API_TITLE) // + .description(DESCRIPTION) // + .version(VERSION) // + .build(); + } + + @Override + protected void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler(SWAGGER_UI) // + .addResourceLocations(RESOURCES_PATH); + + registry.addResourceHandler(WEBJARS) // + .addResourceLocations(WEBJARS_PATH); + } + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/AsyncRestClient.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/AsyncRestClient.java new file mode 100644 index 00000000..2782f94a --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/AsyncRestClient.java @@ -0,0 +1,334 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.clients; + +import io.netty.channel.ChannelOption; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; + +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.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import javax.net.ssl.KeyManagerFactory; + +import org.oransc.enrichment.configuration.WebClientConfig; +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.util.ResourceUtils; +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 org.springframework.web.reactive.function.client.WebClientResponseException; + +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.tcp.TcpClient; + +/** + * 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 WebClientConfig clientConfig; + static KeyStore clientTrustStore = null; + private boolean sslEnabled = true; + + public AsyncRestClient(String baseUrl) { + this(baseUrl, null); + this.sslEnabled = false; + } + + public AsyncRestClient(String baseUrl, WebClientConfig config) { + this.baseUrl = baseUrl; + this.clientConfig = config; + } + + public Mono> postForEntity(String uri, @Nullable String body) { + Object traceTag = createTraceTag(); + logger.debug("{} POST uri = '{}{}''", traceTag, baseUrl, uri); + logger.trace("{} POST body: {}", traceTag, body); + Mono bodyProducer = body != null ? Mono.just(body) : Mono.empty(); + return getWebClient() // + .flatMap(client -> { + RequestHeadersSpec request = client.post() // + .uri(uri) // + .contentType(MediaType.APPLICATION_JSON) // + .body(bodyProducer, String.class); + return retrieve(traceTag, request); + }); + } + + public Mono post(String uri, @Nullable String body) { + return postForEntity(uri, body) // + .flatMap(this::toBody); + } + + public Mono postWithAuthHeader(String uri, String body, String username, String password) { + Object traceTag = createTraceTag(); + logger.debug("{} POST (auth) uri = '{}{}''", traceTag, baseUrl, uri); + logger.trace("{} POST body: {}", traceTag, body); + return getWebClient() // + .flatMap(client -> { + RequestHeadersSpec request = client.post() // + .uri(uri) // + .headers(headers -> headers.setBasicAuth(username, password)) // + .contentType(MediaType.APPLICATION_JSON) // + .bodyValue(body); + return retrieve(traceTag, request) // + .flatMap(this::toBody); + }); + } + + public Mono> putForEntity(String uri, String body) { + Object traceTag = createTraceTag(); + logger.debug("{} PUT uri = '{}{}''", traceTag, baseUrl, uri); + logger.trace("{} PUT body: {}", traceTag, body); + return getWebClient() // + .flatMap(client -> { + RequestHeadersSpec request = client.put() // + .uri(uri) // + .contentType(MediaType.APPLICATION_JSON) // + .bodyValue(body); + return retrieve(traceTag, request); + }); + } + + public Mono> putForEntity(String uri) { + Object traceTag = createTraceTag(); + logger.debug("{} PUT uri = '{}{}''", traceTag, baseUrl, uri); + logger.trace("{} PUT body: ", traceTag); + return getWebClient() // + .flatMap(client -> { + RequestHeadersSpec request = client.put() // + .uri(uri); + return retrieve(traceTag, request); + }); + } + + public Mono put(String uri, String body) { + return putForEntity(uri, body) // + .flatMap(this::toBody); + } + + public Mono> getForEntity(String uri) { + Object traceTag = createTraceTag(); + logger.debug("{} GET uri = '{}{}''", traceTag, baseUrl, uri); + return getWebClient() // + .flatMap(client -> { + RequestHeadersSpec request = client.get().uri(uri); + return retrieve(traceTag, request); + }); + } + + public Mono get(String uri) { + return getForEntity(uri) // + .flatMap(this::toBody); + } + + public Mono> deleteForEntity(String uri) { + Object traceTag = createTraceTag(); + logger.debug("{} DELETE uri = '{}{}''", traceTag, baseUrl, uri); + return getWebClient() // + .flatMap(client -> { + RequestHeadersSpec request = client.delete().uri(uri); + return retrieve(traceTag, request); + }); + } + + public Mono delete(String uri) { + return deleteForEntity(uri) // + .flatMap(this::toBody); + } + + private Mono> retrieve(Object traceTag, RequestHeadersSpec request) { + final Class clazz = String.class; + return request.retrieve() // + .toEntity(clazz) // + .doOnNext(entity -> logger.trace("{} Received: {}", traceTag, entity.getBody())) // + .doOnError(throwable -> onHttpError(traceTag, throwable)); + } + + private static Object createTraceTag() { + return sequenceNumber.incrementAndGet(); + } + + private void onHttpError(Object traceTag, Throwable t) { + if (t instanceof WebClientResponseException) { + WebClientResponseException exception = (WebClientResponseException) t; + logger.debug("{} HTTP error status = '{}', body '{}'", traceTag, exception.getStatusCode(), + exception.getResponseBodyAsString()); + } else { + logger.debug("{} HTTP error", traceTag, t); + } + } + + private Mono toBody(ResponseEntity entity) { + if (entity.getBody() == null) { + return Mono.just(""); + } else { + return Mono.just(entity.getBody()); + } + } + + 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 static synchronized KeyStore getTrustStore(String trustStorePath, String trustStorePass) + throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException { + if (clientTrustStore == null) { + KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); + store.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray()); + clientTrustStore = store; + } + return clientTrustStore; + } + + private SslContext createSslContextRejectingUntrustedPeers(String trustStorePath, String trustStorePass, + KeyManagerFactory keyManager) + throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException { + + final KeyStore trustStore = getTrustStore(trustStorePath, trustStorePass); + List 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 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 TcpClient createTcpClientSecure(SslContext sslContext) { + return TcpClient.create(ConnectionProvider.newConnection()) // + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000) // + .secure(c -> c.sslContext(sslContext)) // + .doOnConnected(connection -> { + connection.addHandlerLast(new ReadTimeoutHandler(30)); + connection.addHandlerLast(new WriteTimeoutHandler(30)); + }); + } + + private TcpClient createTcpClientInsecure() { + return TcpClient.create(ConnectionProvider.newConnection()) // + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10_000) // + .doOnConnected(connection -> { + connection.addHandlerLast(new ReadTimeoutHandler(30)); + connection.addHandlerLast(new WriteTimeoutHandler(30)); + }); + } + + private WebClient createWebClient(String baseUrl, TcpClient tcpClient) { + HttpClient httpClient = HttpClient.from(tcpClient); + ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); + ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder() // + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)) // + .build(); + return WebClient.builder() // + .clientConnector(connector) // + .baseUrl(baseUrl) // + .exchangeStrategies(exchangeStrategies) // + .build(); + } + + private Mono getWebClient() { + if (this.webClient == null) { + try { + if (this.sslEnabled) { + 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()); + SslContext sslContext = createSslContext(keyManager); + TcpClient tcpClient = createTcpClientSecure(sslContext); + this.webClient = createWebClient(this.baseUrl, tcpClient); + } else { + TcpClient tcpClient = createTcpClientInsecure(); + this.webClient = createWebClient(this.baseUrl, tcpClient); + } + } catch (Exception e) { + logger.error("Could not create WebClient {}", e.getMessage()); + return Mono.error(e); + } + } + return Mono.just(this.webClient); + } + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/ApplicationConfig.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/ApplicationConfig.java new file mode 100644 index 00000000..225b83a5 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/ApplicationConfig.java @@ -0,0 +1,72 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2019-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. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.configuration; + +import javax.validation.constraints.NotEmpty; + +import lombok.Getter; + +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 { + @NotEmpty + @Getter + @Value("${app.filepath}") + private String localConfigurationFilePath; + + @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 = ""; + + public WebClientConfig getWebClientConfig() { + return ImmutableWebClientConfig.builder() // + .keyStoreType(this.sslKeyStoreType) // + .keyStorePassword(this.sslKeyStorePassword) // + .keyStore(this.sslKeyStore) // + .keyPassword(this.sslKeyPassword) // + .isTrustStoreUsed(this.sslTrustStoreUsed) // + .trustStore(this.sslTrustStore) // + .trustStorePassword(this.sslTrustStorePassword) // + .build(); + } + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/WebClientConfig.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/WebClientConfig.java new file mode 100644 index 00000000..61d0f5ad --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/configuration/WebClientConfig.java @@ -0,0 +1,45 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2020 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.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(); + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java new file mode 100644 index 00000000..1df2df73 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/ErrorResponse.java @@ -0,0 +1,106 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * 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. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.controllers; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +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 + @ApiModel(value = "error_information", description = "Problem as defined in https://tools.ietf.org/html/rfc7807") + 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; + } + + @ApiModelProperty( + example = "503", + value = "The HTTP status code generated by the origin server for this occurrence of the problem.") + public Integer getStatus() { + return status; + } + + @ApiModelProperty( + example = "EI job type not found", + value = "A human-readable explanation specific to this occurrence of the problem.") + public String getDetail() { + return this.detail; + } + + } + + @ApiModelProperty(value = "message") + public final String message; + + ErrorResponse(String message) { + this.message = message; + } + + public static Mono> createMono(String text, HttpStatus code) { + return Mono.just(create(text, code)); + } + + public static Mono> createMono(Exception e, HttpStatus code) { + return createMono(e.toString(), code); + } + + public static ResponseEntity create(String text, HttpStatus code) { + ErrorInfo p = new ErrorInfo(text, code.value()); + String json = gson.toJson(p); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PROBLEM_JSON); + return new ResponseEntity<>(json, headers, code); + } + + public static ResponseEntity create(Exception e, HttpStatus code) { + return create(e.toString(), code); + } + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java new file mode 100644 index 00000000..d38d6dca --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerConsts.java @@ -0,0 +1,32 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * 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. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.controllers.consumer; + +public class ConsumerConsts { + + public static final String A1E_API_ROOT = "/A1-EI/v1"; + public static final String CONSUMER_API_NAME = "A1-E Enrichment Data Consumer API"; + public static final String OWNER_PARAM = "owner"; + public static final String OWNER_PARAM_DESCRIPTION = "identifies the owner of the job"; + + private ConsumerConsts() { + } +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java new file mode 100644 index 00000000..7dfaecae --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java @@ -0,0 +1,258 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2019-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. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.controllers.consumer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; + +import java.util.ArrayList; +import java.util.List; + +import org.oransc.enrichment.controllers.ErrorResponse; +import org.oransc.enrichment.repository.EiJob; +import org.oransc.enrichment.repository.EiJobs; +import org.oransc.enrichment.repository.EiType; +import org.oransc.enrichment.repository.EiTypes; +import org.oransc.enrichment.repository.ImmutableEiJob; +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.RestController; + +@RestController("ConsumerController") +@Api(tags = {ConsumerConsts.CONSUMER_API_NAME}) +public class ConsumerController { + + @Autowired + private EiJobs eiJobs; + + @Autowired + private EiTypes eiTypes; + + private static Gson gson = new GsonBuilder() // + .serializeNulls() // + .create(); // + + @GetMapping(path = ConsumerConsts.A1E_API_ROOT + "/eitypes", produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Query EI type identifiers", notes = "DETAILS TBD") + @ApiResponses( + value = { // + @ApiResponse( + code = 200, + message = "EI type identifiers", + response = String.class, + responseContainer = "List"), // + }) + public ResponseEntity getEiTypeIdentifiers( // + ) { + List result = new ArrayList<>(); + for (EiType eiType : this.eiTypes.getAllEiTypes()) { + result.add(eiType.id()); + } + + return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); + } + + @GetMapping(path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}", produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Definitions for an individual EI Type", notes = "Query EI type") + @ApiResponses( + value = { // + @ApiResponse(code = 200, message = "EI type", response = ConsumerEiTypeInfo.class), // + @ApiResponse( + code = 404, + message = "Enrichment Information type is not found", + response = ErrorResponse.ErrorInfo.class)}) + public ResponseEntity getEiType( // + @PathVariable("eiTypeId") String eiTypeId) { + try { + EiType t = this.eiTypes.getType(eiTypeId); + ConsumerEiTypeInfo info = toEiTypeInfo(t); + return new ResponseEntity<>(gson.toJson(info), HttpStatus.OK); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + @GetMapping( + path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs", + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Query EI job identifiers", notes = "Returns the EI Job identifiers for an EI Type") + @ApiResponses( + value = { // + @ApiResponse( + code = 200, + message = "EI job identifiers", + response = String.class, + responseContainer = "List"), // + @ApiResponse( + code = 404, + message = "Enrichment Information type is not found", + response = ErrorResponse.ErrorInfo.class)}) + public ResponseEntity getEiJobIds( // + @PathVariable("eiTypeId") String eiTypeId, // + @ApiParam( + name = ConsumerConsts.OWNER_PARAM, + required = false, // + value = ConsumerConsts.OWNER_PARAM_DESCRIPTION) // + String owner) { + try { + this.eiTypes.getType(eiTypeId); // Just to check that the type exists + List result = new ArrayList<>(); + for (EiJob job : this.eiJobs.getJobsForType(eiTypeId)) { + result.add(job.id()); + } + return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + @GetMapping( + path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}", + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Individual EI Job", notes = "") + @ApiResponses( + value = { // + @ApiResponse(code = 200, message = "EI Job", response = ConsumerEiJobInfo.class), // + @ApiResponse( + code = 404, + message = "Enrichment Information type or job is not found", + response = ErrorResponse.ErrorInfo.class)}) + public ResponseEntity getIndividualEiJob( // + @PathVariable("eiTypeId") String eiTypeId, // + @PathVariable("eiJobId") String eiJobId) { + try { + this.eiTypes.getType(eiTypeId); // Just to check that the type exists + EiJob 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 = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}/status", + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "EI Job status", notes = "") + @ApiResponses( + value = { // + @ApiResponse(code = 200, message = "EI Job status", response = ConsumerEiJobStatus.class), // + @ApiResponse( + code = 404, + message = "Enrichment Information type or job is not found", + response = ErrorResponse.ErrorInfo.class)}) + public ResponseEntity getEiJobStatus( // + @PathVariable("eiTypeId") String eiTypeId, // + @PathVariable("eiJobId") String eiJobId) { + try { + this.eiTypes.getType(eiTypeId); // Just to check that the type exists + EiJob 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 ConsumerEiJobStatus toEiJobStatus(EiJob job) { + return new ConsumerEiJobStatus(ConsumerEiJobStatus.OperationalState.ENABLED); + } + + @DeleteMapping( + path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}", + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Individual EI Job", notes = "Delete EI job") + @ApiResponses( + value = { // + @ApiResponse(code = 200, message = "Not used", response = void.class), + @ApiResponse(code = 204, message = "Job deleted", response = void.class), + @ApiResponse( + code = 404, + message = "Enrichment Information type or job is not found", + response = ErrorResponse.ErrorInfo.class)}) + public ResponseEntity deleteIndividualEiJob( // + @PathVariable("eiTypeId") String eiTypeId, // + @PathVariable("eiJobId") String eiJobId) { + try { + this.eiJobs.remove(eiJobId); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + @PutMapping( + path = ConsumerConsts.A1E_API_ROOT + "/eitypes/{eiTypeId}/eijobs/{eiJobId}", // + produces = MediaType.APPLICATION_JSON_VALUE, // + consumes = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Individual EI Job", notes = "Create or update an EI Job") + @ApiResponses( + value = { // + @ApiResponse(code = 201, message = "Job created", response = void.class), // + @ApiResponse(code = 200, message = "Job updated", response = void.class), // , + @ApiResponse( + code = 404, + message = "Enrichment Information type is not found", + response = ErrorResponse.ErrorInfo.class)}) + public ResponseEntity putIndividualEiJob( // + @PathVariable("eiTypeId") String eiTypeId, // + @PathVariable("eiJobId") String eiJobId, // + @RequestBody ConsumerEiJobInfo eiJobInfo) { + try { + this.eiTypes.getType(eiTypeId); // Just to check that the type exists + final boolean newJob = this.eiJobs.get(eiJobId) == null; + this.eiJobs.put(toEiJob(eiJobInfo, eiJobId, eiTypeId)); + return new ResponseEntity<>(newJob ? HttpStatus.CREATED : HttpStatus.OK); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + // Status TBD + + private EiJob toEiJob(ConsumerEiJobInfo info, String id, String typeId) { + return ImmutableEiJob.builder() // + .id(id) // + .typeId(typeId) // + .owner(info.owner) // + .jobData(info.jobData) // + .build(); + } + + private ConsumerEiTypeInfo toEiTypeInfo(EiType t) { + return new ConsumerEiTypeInfo(t.jobDataSchema()); + } + + private ConsumerEiJobInfo toEiJobInfo(EiJob s) { + return new ConsumerEiJobInfo(s.jobData(), s.owner()); + } +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java new file mode 100644 index 00000000..08d3caea --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobInfo.java @@ -0,0 +1,52 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * 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. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.controllers.consumer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import org.immutables.gson.Gson; + +@Gson.TypeAdapters +@ApiModel(value = "ei_job_info", description = "Information for a Enrichment Information Job") +public class ConsumerEiJobInfo { + + @ApiModelProperty(value = "Identity of the owner of the job") + @SerializedName("owner") + @JsonProperty("owner") + public String owner; + + @ApiModelProperty(value = "EI Type specific job data") + @SerializedName("job_data") + @JsonProperty("job_data") + public Object jobData; + + public ConsumerEiJobInfo() { + } + + public ConsumerEiJobInfo(Object jobData, String owner) { + this.jobData = jobData; + this.owner = owner; + } +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java new file mode 100644 index 00000000..dbdd1a34 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiJobStatus.java @@ -0,0 +1,54 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.controllers.consumer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import org.immutables.gson.Gson; + +@Gson.TypeAdapters +@ApiModel(value = "ei_job_status", description = "Status for an EI Job") +public class ConsumerEiJobStatus { + + @Gson.TypeAdapters + @ApiModel(value = "operational_state", description = "Represents the operational states for a EI Job") + public enum OperationalState { + ENABLED, DISABLED + } + + private static final String OPERATIONAL_STATE_DESCRIPTION = "Operational state, values:\n" // + + "ENABLED: TBD\n" // + + "DISABLED: TBD."; + + @ApiModelProperty(value = OPERATIONAL_STATE_DESCRIPTION, name = "operational_state") + @SerializedName("operational_state") + @JsonProperty("operational_state") + public final OperationalState state; + + public ConsumerEiJobStatus(OperationalState state) { + this.state = state; + } + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiTypeInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiTypeInfo.java new file mode 100644 index 00000000..05d23262 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerEiTypeInfo.java @@ -0,0 +1,43 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * 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. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.controllers.consumer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import org.immutables.gson.Gson; + +@Gson.TypeAdapters +@ApiModel(value = "ei_type_info", description = "Information for an EI type") +public class ConsumerEiTypeInfo { + + @ApiModelProperty(value = "Json schema for the job data") + @SerializedName("job_data_schema") + @JsonProperty("job_data_schema") + public final Object jobDataSchema; + + ConsumerEiTypeInfo(Object jobDataSchema) { + this.jobDataSchema = jobDataSchema; + } +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/exceptions/ServiceException.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/exceptions/ServiceException.java new file mode 100644 index 00000000..a14e8dec --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/exceptions/ServiceException.java @@ -0,0 +1,32 @@ +/*- + * ============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.enrichment.exceptions; + +public class ServiceException extends Exception { + + private static final long serialVersionUID = 1L; + + public ServiceException(String message) { + super(message); + } + + public ServiceException(String message, Exception originalException) { + super(message, originalException); + } +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java new file mode 100644 index 00000000..79f62f86 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJob.java @@ -0,0 +1,40 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.repository; + +import org.immutables.gson.Gson; +import org.immutables.value.Value; + +/** + * Represents the dynamic information about a Near-RT RIC. + */ +@Value.Immutable +@Gson.TypeAdapters +public interface EiJob { + + String id(); + + String typeId(); + + String owner(); + + Object jobData(); +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java new file mode 100644 index 00000000..bb2e40fd --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiJobs.java @@ -0,0 +1,106 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.repository; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; + +import org.oransc.enrichment.exceptions.ServiceException; + +/** + * Dynamic representation of all EI Jobs in the system. + */ +public class EiJobs { + private Map allEiJobs = new HashMap<>(); + private Map> jobsByType = new HashMap<>(); + + public synchronized void put(EiJob job) { + allEiJobs.put(job.id(), job); + multiMapPut(this.jobsByType, job.typeId(), job); + } + + public synchronized Collection getJobs() { + return new Vector<>(allEiJobs.values()); + } + + public synchronized EiJob getJob(String id) throws ServiceException { + EiJob ric = allEiJobs.get(id); + if (ric == null) { + throw new ServiceException("Could not find EI Job: " + id); + } + return ric; + } + + public synchronized Collection getJobsForType(String typeId) { + return multiMapGet(this.jobsByType, typeId); + } + + public synchronized EiJob get(String id) { + return allEiJobs.get(id); + } + + public synchronized EiJob remove(String id) { + EiJob job = allEiJobs.get(id); + if (job != null) { + remove(job); + } + return job; + } + + public synchronized void remove(EiJob job) { + this.allEiJobs.remove(job.id()); + multiMapRemove(this.jobsByType, job.typeId(), job); + } + + public synchronized int size() { + return allEiJobs.size(); + } + + public synchronized void clear() { + this.allEiJobs.clear(); + } + + private void multiMapPut(Map> multiMap, String key, EiJob value) { + multiMap.computeIfAbsent(key, k -> new HashMap<>()).put(value.id(), value); + } + + private void multiMapRemove(Map> multiMap, String key, EiJob value) { + Map map = multiMap.get(key); + if (map != null) { + map.remove(value.id()); + if (map.isEmpty()) { + multiMap.remove(key); + } + } + } + + private Collection multiMapGet(Map> multiMap, String key) { + Map map = multiMap.get(key); + if (map == null) { + return Collections.emptyList(); + } + return new Vector<>(map.values()); + } + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java new file mode 100644 index 00000000..997484d8 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java @@ -0,0 +1,32 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.repository; + +import org.immutables.gson.Gson; +import org.immutables.value.Value; + +@Value.Immutable +@Gson.TypeAdapters +public interface EiType { + public String id(); + + public Object jobDataSchema(); +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java new file mode 100644 index 00000000..7668ff16 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java @@ -0,0 +1,68 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.repository; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; + +import org.oransc.enrichment.exceptions.ServiceException; + +/** + * Dynamic representation of all EI Types in the system. + */ +public class EiTypes { + Map allEiTypes = new HashMap<>(); + + public synchronized void put(EiType type) { + allEiTypes.put(type.id(), type); + } + + public synchronized Collection getAllEiTypes() { + return new Vector<>(allEiTypes.values()); + } + + public synchronized EiType getType(String id) throws ServiceException { + EiType type = allEiTypes.get(id); + if (type == null) { + throw new ServiceException("Could not find EI Job: " + id); + } + return type; + } + + public synchronized EiType get(String id) { + return allEiTypes.get(id); + } + + public synchronized void remove(String id) { + allEiTypes.remove(id); + } + + public synchronized int size() { + return allEiTypes.size(); + } + + public synchronized void clear() { + this.allEiTypes.clear(); + } + +} diff --git a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java new file mode 100644 index 00000000..d8714277 --- /dev/null +++ b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java @@ -0,0 +1,275 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2019-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. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.oransc.enrichment.clients.AsyncRestClient; +import org.oransc.enrichment.configuration.ApplicationConfig; +import org.oransc.enrichment.configuration.ImmutableWebClientConfig; +import org.oransc.enrichment.configuration.WebClientConfig; +import org.oransc.enrichment.controllers.consumer.ConsumerConsts; +import org.oransc.enrichment.controllers.consumer.ConsumerEiJobInfo; +import org.oransc.enrichment.repository.EiJob; +import org.oransc.enrichment.repository.EiJobs; +import org.oransc.enrichment.repository.EiType; +import org.oransc.enrichment.repository.EiTypes; +import org.oransc.enrichment.repository.ImmutableEiJob; +import org.oransc.enrichment.repository.ImmutableEiType; +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.server.LocalServerPort; +import org.springframework.context.ApplicationContext; +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"}) +class ApplicationTest { + private static final Logger logger = LoggerFactory.getLogger(ApplicationTest.class); + + @Autowired + ApplicationContext context; + + @Autowired + EiJobs eiJobs; + + @Autowired + EiTypes eiTypes; + + @Autowired + ApplicationConfig applicationConfig; + + private static Gson gson = new GsonBuilder() // + .serializeNulls() // + .create(); // + + /** + * Overrides the BeanFactory. + */ + @TestConfiguration + static class TestBeanFactory { + + } + + @LocalServerPort + private int port; + + @BeforeEach + void reset() { + this.eiJobs.clear(); + this.eiTypes.clear(); + } + + @Test + void getEiTypes() throws Exception { + addEiType("test"); + String url = "/eitypes"; + String rsp = restClient().get(url).block(); + assertThat(rsp).isEqualTo("[\"test\"]"); + } + + @Test + void getEiType() throws Exception { + addEiType("test"); + String url = "/eitypes/test"; + String rsp = restClient().get(url).block(); + assertThat(rsp).contains("job_data_schema"); + } + + @Test + void getEiTypeNotFound() throws Exception { + String url = "/eitypes/junk"; + testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find EI Job: junk"); + } + + @Test + void getEiJobsIds() throws Exception { + addEiJob("typeId", "jobId"); + String url = "/eitypes/typeId/eijobs"; + String rsp = restClient().get(url).block(); + assertThat(rsp).isEqualTo("[\"jobId\"]"); + } + + @Test + void getEiJobTypeNotFound() throws Exception { + String url = "/eitypes/junk/eijobs"; + testErrorCode(restClient().get(url), HttpStatus.NOT_FOUND, "Could not find EI Job: junk"); + } + + @Test + void getEiJob() throws Exception { + addEiJob("typeId", "jobId"); + String url = "/eitypes/typeId/eijobs/jobId"; + String rsp = restClient().get(url).block(); + assertThat(rsp).contains("job_data"); + } + + @Test + void getEiJobStatus() throws Exception { + addEiJob("typeId", "jobId"); + String url = "/eitypes/typeId/eijobs/jobId/status"; + String rsp = restClient().get(url).block(); + assertThat(rsp).contains("ENABLED"); + } + + // Status TBD + + @Test + void deleteEiJob() throws Exception { + addEiJob("typeId", "jobId"); + assertThat(this.eiJobs.size()).isEqualTo(1); + String url = "/eitypes/typeId/eijobs/jobId"; + restClient().delete(url).block(); + assertThat(this.eiJobs.size()).isEqualTo(0); + } + + @Test + void putEiJob() throws Exception { + addEiType("typeId"); + + String url = "/eitypes/typeId/eijobs/jobId"; + String body = gson.toJson(eiJobInfo()); + ResponseEntity resp = restClient().putForEntity(url, body).block(); + assertThat(this.eiJobs.size()).isEqualTo(1); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + resp = restClient().putForEntity(url, body).block(); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); + EiJob job = this.eiJobs.getJob("jobId"); + assertThat(job.owner()).isEqualTo("owner"); + } + + ConsumerEiJobInfo eiJobInfo() { + return new ConsumerEiJobInfo(jsonObject(), "owner"); + } + + // @Test + @SuppressWarnings("squid:S2699") + void runMock() throws Exception { + logger.info("Keeping server alive! " + this.port); + synchronized (this) { + this.wait(); + } + } + + JsonObject jsonObject() { + JsonObject jsonObj = new JsonObject(); + JsonElement e = new JsonPrimitive(111); + jsonObj.add("param", e); + return jsonObj; + } + + private EiJob addEiJob(String typeId, String jobId) { + addEiType(typeId); + EiJob job = ImmutableEiJob.builder() // + .id(jobId) // + .typeId(typeId) // + .owner("owner") // + .jobData(jsonObject()) // + .build(); + this.eiJobs.put(job); + return job; + } + + private EiType addEiType(String typeId) { + EiType t = ImmutableEiType.builder() // + .id(typeId) // + .jobDataSchema(jsonObject()) // + .build(); + this.eiTypes.put(t); + return t; + } + + private String baseUrl() { + return "https://localhost:" + this.port + ConsumerConsts.A1E_API_ROOT; + } + + private AsyncRestClient restClient(boolean useTrustValidation) { + WebClientConfig config = this.applicationConfig.getWebClientConfig(); + config = ImmutableWebClientConfig.builder() // + .keyStoreType(config.keyStoreType()) // + .keyStorePassword(config.keyStorePassword()) // + .keyStore(config.keyStore()) // + .keyPassword(config.keyPassword()) // + .isTrustStoreUsed(useTrustValidation) // + .trustStore(config.trustStore()) // + .trustStorePassword(config.trustStorePassword()) // + .build(); + + return new AsyncRestClient(baseUrl(), config); + } + + 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; + } + +} -- 2.16.6