From: PatrikBuhr Date: Fri, 11 Sep 2020 14:50:37 +0000 (+0200) Subject: Enrichment Coordination Service X-Git-Tag: 2.1.0~48 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=d1d085456c485599f6b8aba87b6d761b29c2ecd4;p=nonrtric.git Enrichment Coordination Service First part. Consumer API defined. Change-Id: I0841cba636c2e79ed0e54978641434e83aff77e5 Issue-ID: NONRTRIC-173 Signed-off-by: PatrikBuhr --- 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 00000000..122997ac Binary files /dev/null and b/enrichment-coordinator-service/config/keystore.jks differ diff --git a/enrichment-coordinator-service/config/truststore.jks b/enrichment-coordinator-service/config/truststore.jks new file mode 100644 index 00000000..60d62889 Binary files /dev/null and b/enrichment-coordinator-service/config/truststore.jks differ 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; + } + +}