Soutbound PM Rest Client 02/6002/6
authorClaudio D. Gasparini <claudio.gasparini@intl.att.com>
Mon, 3 May 2021 15:42:22 +0000 (17:42 +0200)
committerClaudio David Gasparini <claudio.gasparini@intl.att.com>
Tue, 18 May 2021 14:28:46 +0000 (14:28 +0000)
Issue-ID: OAM-207
Signed-off-by: Claudio D. Gasparini <claudio.gasparini@intl.att.com>
Change-Id: I77363efda6f4d228409d64d330b2b640c67efb69

18 files changed:
ves-nf-oam-adopter/pom.xml
ves-nf-oam-adopter/ves-nf-oam-adopter-artifacts/pom.xml
ves-nf-oam-adopter/ves-nf-oam-adopter-parent/pom.xml
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-manager/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/rest/manager/VesEventNotifierMock.java
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/pom.xml [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClient.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/DownloadPerformanceManagementFilesHandler.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/OffSetTimeZoneHandler.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/TokenHandler.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TimeZoneResponse.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TokenResponse.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/properties/PmEndpointsUrlsProperties.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClientTest.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/JsonUtils.java [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/application.yaml [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/timeZoneOffsetResponse.json [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/tokenResponse.json [new file with mode: 0644]
ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker [new file with mode: 0644]

index c116ba1..7b455cb 100644 (file)
@@ -51,5 +51,6 @@
         <module>ves-nf-oam-adopter-event-notifier</module>
         <module>ves-nf-oam-adopter-snmp-manager</module>
         <module>ves-nf-oam-adopter-pm-manager</module>
+        <module>ves-nf-oam-adopter-pm-sb-rest-client</module>
     </modules>
 </project>
\ No newline at end of file
index eac13ad..7269b55 100644 (file)
                 <artifactId>ves-nf-oam-adopter-pm-manager</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>${project.groupId}</groupId>
+                <artifactId>ves-nf-oam-adopter-pm-sb-rest-client</artifactId>
+                <version>${project.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 </project>
\ No newline at end of file
index aa697e4..393a11b 100644 (file)
@@ -46,7 +46,9 @@
             ../ves-nf-oam-adopter-snmp-manager/target/site/jacoco-ut/jacoco.xml,
             ../ves-nf-oam-adopter-snmp-manager/target/site/jacoco-aggregate/jacoco.xml,
             ../ves-nf-oam-adopter-pm-manager/target/site/jacoco-ut/jacoco.xml,
-            ../ves-nf-oam-adopter-pm-manager/target/site/jacoco-aggregate/jacoco.xml
+            ../ves-nf-oam-adopter-pm-manager/target/site/jacoco-aggregate/jacoco.xml,
+            ../ves-nf-oam-adopter-pm-sb-rest-client/target/site/jacoco-ut/jacoco.xml,
+            ../ves-nf-oam-adopter-pm-sb-rest-client/target/site/jacoco-aggregate/jacoco.xml
         </sonar.coverage.jacoco.xmlReportPaths>
         <sonar.scanner.version>3.8.0.2131</sonar.scanner.version>
         <!--Dependency Versions-->
index e6249ff..ff79438 100644 (file)
@@ -35,7 +35,7 @@ final class VesEventNotifierMock implements VesEventNotifier {
     private final List<CommonEventFormat302ONAP> event = new ArrayList<>();
 
     @Override
-    public Completable notifyEvents(final CommonEventFormat302ONAP event) {
+    public synchronized Completable notifyEvents(final CommonEventFormat302ONAP event) {
         this.event.add(event);
         return Completable.complete();
     }
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/pom.xml b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/pom.xml
new file mode 100644 (file)
index 0000000..3dcd893
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+* ============LICENSE_START=======================================================
+* O-RAN-SC
+* ================================================================================
+* Copyright (C) 2021 AT&T Intellectual Property. 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============================================
+*
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.o-ran-sc.oam</groupId>
+        <artifactId>ves-nf-oam-adopter-parent</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../ves-nf-oam-adopter-parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>ves-nf-oam-adopter-pm-sb-rest-client</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>ves-nf-oam-adopter-pm-manager</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents.client5</groupId>
+            <artifactId>httpclient5</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.reactivex.rxjava3</groupId>
+            <artifactId>rxjava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClient.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClient.java
new file mode 100644 (file)
index 0000000..b1d6008
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  O-RAN-SC
+ *  ================================================================================
+ *  Copyright © 2021 AT&T Intellectual Property. 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client;
+
+import static org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http.DownloadPerformanceManagementFilesHandler.readPerformanceManagementFiles;
+import static org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http.OffSetTimeZoneHandler.readTimeZone;
+import static org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http.TokenHandler.returnToken;
+import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import io.reactivex.rxjava3.core.Single;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.ZipInputStream;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequests;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.core5.http.ConnectionClosedException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.checkerframework.checker.lock.qual.GuardedBy;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.api.HttpRestClient;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementException;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.TokenGenerationException;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.ZoneIdException;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter;
+import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.properties.PmEndpointsUrlsProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public final class DefaultHttpRestClient implements HttpRestClient {
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpRestClient.class);
+
+    public static final String HTTPS = "https://";
+    public static final String BEARER = "Bearer ";
+    private static final DateTimeFormatter OFFSET_FORMATTER = DateTimeFormatter.ofPattern("xxx");
+    private final CloseableHttpAsyncClient client;
+    @GuardedBy("this")
+    private final LoadingCache<Adapter, String> sessionCache =
+            CacheBuilder.newBuilder().refreshAfterWrite(59, TimeUnit.MINUTES).build(new CacheLoader<>() {
+                @Override
+                public String load(final Adapter adapter) throws ExecutionException, InterruptedException {
+                    try {
+                        return returnToken(DefaultHttpRestClient.this.client, DefaultHttpRestClient.this.tokenEndpoint,
+                                adapter);
+                    } catch (final Exception error) {
+                        LOG.error("Failed to read time zone", error);
+                        throw error;
+                    }
+                }
+            });
+
+    @GuardedBy("this")
+    private final LoadingCache<Adapter, ZoneId> zoneIdCache =
+            CacheBuilder.newBuilder().build(new CacheLoader<>() {
+                @Override
+                public ZoneId load(final Adapter adapter) {
+                    return readTimeZone(DefaultHttpRestClient.this, timeZoneEndpoint,  adapter)
+                                   .doOnError(error -> LOG.error("Failed to read time zone", error))
+                                   .blockingGet();
+                }
+            });
+    private final String pmFilesEndpoint;
+    private final String timeZoneEndpoint;
+    private final String tokenEndpoint;
+
+    /**
+     * Default constructor.
+     */
+    @Autowired
+    public DefaultHttpRestClient(final CloseableHttpAsyncClient httpAsyncClient,
+            final PmEndpointsUrlsProperties properties) {
+        this.client = httpAsyncClient;
+        this.pmFilesEndpoint = properties.getRanPmEndpoint();
+        this.timeZoneEndpoint = properties.getRanTimeZoneOffsetEndpoint();
+        this.tokenEndpoint = properties.getRanTokenEndpoint();
+    }
+
+
+    @Override
+    public synchronized Single<ZipInputStream> readFiles(final Adapter adapter) {
+        return readPerformanceManagementFiles(this, pmFilesEndpoint, adapter);
+    }
+
+    @Override
+    public Single<ZoneId> getTimeZone(final Adapter adapter) {
+        try {
+            final ZoneId zoneId = zoneIdCache.get(adapter);
+            LOG.info("Adapter {} has offset {}", adapter.getHostIpAddress(),
+                    OFFSET_FORMATTER.format(zoneId.getRules().getOffset(Instant.now())));
+            return Single.just(zoneId);
+        } catch (final Exception e) {
+            final Throwable cause = e.getCause();
+            if (cause instanceof PerformanceManagementException) {
+                return Single.error(cause);
+            }
+            return Single.error(new ZoneIdException("Failed to get Zone ID for " + adapter.getHostIpAddress(), cause));
+        }
+    }
+
+    /**
+     * Execute GET request on adapter endpoint.
+     * @param adapter destiny
+     * @param url endpoint
+     * @return response
+     */
+    public Single<SimpleHttpResponse> get(final Adapter adapter, final String url) {
+        return getToken(adapter).flatMap(token -> {
+            final SimpleHttpRequest request =
+                SimpleHttpRequests.get(HTTPS + adapter.getHostIpAddress() + url);
+            request.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE);
+            request.addHeader(HttpHeaders.AUTHORIZATION, BEARER + token);
+            return Single.fromFuture(client.execute(request, null))
+                    .doOnSubscribe(result -> LOG.trace("GET Request started {} ...", request.toString()))
+                    .doOnSuccess(result -> LOG.trace("GET Request finished {}", request));
+        });
+    }
+
+    private Single<String> getToken(final Adapter adapter) {
+        try {
+            final String token = sessionCache.get(adapter);
+            return Single.just(token);
+        } catch (final Exception e) {
+            if (e.getCause() instanceof TokenGenerationException) {
+                return Single.error(e.getCause());
+            } else if (e.getCause() instanceof ConnectionClosedException) {
+                return Single.error(e.getCause());
+            }
+            return Single.error(e);
+        }
+    }
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/DownloadPerformanceManagementFilesHandler.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/DownloadPerformanceManagementFilesHandler.java
new file mode 100644 (file)
index 0000000..77957cb
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  O-RAN-SC
+ *  ================================================================================
+ *  Copyright © 2021 AT&T Intellectual Property. 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http;
+
+import io.reactivex.rxjava3.core.Single;
+import java.io.ByteArrayInputStream;
+import java.util.zip.ZipInputStream;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.hc.client5.http.async.methods.SimpleBody;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.message.StatusLine;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementException;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter;
+import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.DefaultHttpRestClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class DownloadPerformanceManagementFilesHandler {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DownloadPerformanceManagementFilesHandler.class);
+
+    /**
+     * Reads and return ZIP CSV PM Files from device.
+     */
+    public static Single<ZipInputStream> readPerformanceManagementFiles(final DefaultHttpRestClient httpSession,
+            final String pmFilesEndpoint, final Adapter adapter) {
+        LOG.info("Download PM files from RAN {}", adapter.getHostIpAddress());
+        return httpSession.get(adapter, pmFilesEndpoint)
+                       .flatMap(response -> DownloadPerformanceManagementFilesHandler
+                                                    .validateGetZipFile(adapter, response))
+                       .map(entity -> new ZipInputStream(new ByteArrayInputStream(entity.getBodyBytes())));
+    }
+
+    private static Single<SimpleBody> validateGetZipFile(final Adapter adapter, final SimpleHttpResponse response) {
+        final String statusLine = new StatusLine(response).toString();
+        final ContentType contentType = response.getContentType();
+        final SimpleBody entity = response.getBody();
+        if (response.getCode() == HttpStatus.SC_OK && entity != null) {
+            if (ContentType.APPLICATION_OCTET_STREAM.getMimeType().equals(contentType.getMimeType())) {
+                return Single.just(entity);
+            }
+        }
+        return Single.error(new PerformanceManagementException(
+                "Download files from " + adapter.getHostIpAddress() + " failed: " + statusLine));
+    }
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/OffSetTimeZoneHandler.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/OffSetTimeZoneHandler.java
new file mode 100644 (file)
index 0000000..2bf8aeb
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  O-RAN-SC
+ *  ================================================================================
+ *  Copyright © 2021 AT&T Intellectual Property. 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http;
+
+import com.google.gson.Gson;
+import io.reactivex.rxjava3.core.Single;
+import java.time.ZoneId;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementEmptyOutputException;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementException;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter;
+import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.DefaultHttpRestClient;
+import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos.TimeZoneResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class OffSetTimeZoneHandler {
+
+    private static final Logger LOG = LoggerFactory.getLogger(OffSetTimeZoneHandler.class);
+
+    private static final Gson GSON = new Gson();
+
+    /**
+     * Returns time zone of the device.
+     */
+    public static Single<ZoneId> readTimeZone(final DefaultHttpRestClient httpRestClient, final String offsetEndpoint,
+            final Adapter adapter) {
+        LOG.debug("Read Time Zone from adapter {}", adapter.getHostIpAddress());
+        return httpRestClient.get(adapter, offsetEndpoint)
+                       .map(response -> validateGet(response, adapter))
+                       .map(ZoneId::of);
+    }
+
+    private static String validateGet(final SimpleHttpResponse response, final Adapter adapter) {
+        if (response.getCode() != HttpStatus.SC_OK) {
+            throw new PerformanceManagementException(
+                    "Get Zone offset failed for " + adapter.getHostIpAddress() + " Code: " + response.getCode());
+        }
+        final String output = response.getBody().getBodyText();
+        if (output.isEmpty()) {
+            throw new PerformanceManagementEmptyOutputException(
+                    "Get Zone offset failed for " + adapter.getHostIpAddress() + " . Empty output received");
+        }
+        return GSON.fromJson(output, TimeZoneResponse.class).getOffset();
+    }
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/TokenHandler.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/TokenHandler.java
new file mode 100644 (file)
index 0000000..26c62b5
--- /dev/null
@@ -0,0 +1,53 @@
+package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http;
+
+import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
+
+import com.google.gson.Gson;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.concurrent.ExecutionException;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequests;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.message.StatusLine;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.TokenGenerationException;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter;
+import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos.TokenResponse;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class TokenHandler {
+
+    public static final Gson GSON = new Gson();
+    public static final String HTTPS = "https://";
+
+    /**
+     * Request Token under defined endpoint.
+     */
+    public static synchronized String returnToken(final CloseableHttpAsyncClient client, final String tokenEndpoint,
+            final Adapter adapter) throws ExecutionException, InterruptedException {
+        final String host = HTTPS + adapter.getHostIpAddress();
+        final SimpleHttpRequest request = SimpleHttpRequests.post(host + tokenEndpoint);
+        final String basicAuth = Base64.getEncoder().encodeToString(
+                (adapter.getUsername() + ":" + adapter.getPassword()).getBytes(StandardCharsets.UTF_8));
+        request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth);
+        request.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE);
+        request.addHeader(HttpHeaders.ACCEPT, APPLICATION_JSON_VALUE);
+
+        final SimpleHttpResponse response = client.execute(request, null).get();
+        final String statusLine = new StatusLine(response).toString();
+        if (response.getCode() != HttpStatus.SC_OK) {
+            throw new TokenGenerationException("Failed to obtain a token for host " + host + ": " + statusLine);
+        }
+        final String output = response.getBody().getBodyText();
+        if (output.isEmpty()) {
+            throw new TokenGenerationException(
+                    "Failed to obtain a token for host " + host + ", empty output: " + statusLine);
+        }
+        return GSON.fromJson(output, TokenResponse.class).getToken();
+    }
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TimeZoneResponse.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TimeZoneResponse.java
new file mode 100644 (file)
index 0000000..853951d
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  O-RAN-SC
+ *  ================================================================================
+ *  Copyright © 2021 AT&T Intellectual Property. 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+@RequiredArgsConstructor
+@Getter
+@Setter
+public class TimeZoneResponse {
+    @SerializedName(value = "offset")
+    private @NonNull final String offset;
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TokenResponse.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TokenResponse.java
new file mode 100644 (file)
index 0000000..195922f
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  O-RAN-SC
+ *  ================================================================================
+ *  Copyright © 2021 AT&T Intellectual Property. 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class TokenResponse {
+    private String token;
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/properties/PmEndpointsUrlsProperties.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/properties/PmEndpointsUrlsProperties.java
new file mode 100644 (file)
index 0000000..65a6826
--- /dev/null
@@ -0,0 +1,23 @@
+package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.properties;
+
+import javax.validation.constraints.NotEmpty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.annotation.Validated;
+
+@Component
+@ConfigurationProperties(prefix = "pm-rest-manager")
+@Data
+@NoArgsConstructor
+@Validated
+public class PmEndpointsUrlsProperties {
+
+    @NotEmpty
+    private String ranTokenEndpoint;
+    @NotEmpty
+    private String ranPmEndpoint;
+    @NotEmpty
+    private String ranTimeZoneOffsetEndpoint;
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClientTest.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClientTest.java
new file mode 100644 (file)
index 0000000..f9c115a
--- /dev/null
@@ -0,0 +1,180 @@
+package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.when;
+
+import io.reactivex.rxjava3.observers.TestObserver;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.zip.ZipInputStream;
+import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpStatus;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.mockito.stubbing.Answer;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.api.HttpRestClient;
+import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter;
+import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.properties.PmEndpointsUrlsProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.MethodMode;
+
+@SpringBootTest(classes = {DefaultHttpRestClient.class, PmEndpointsUrlsProperties.class})
+@EnableConfigurationProperties
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+public class DefaultHttpRestClientTest {
+
+    private static final Adapter ADAPTER =
+            Adapter.builder().hostIpAddress("150.62.25.26").username("admin").password("secretPassword").build();
+    @Autowired
+    public HttpRestClient restClient;
+
+    @MockBean
+    CloseableHttpAsyncClient client;
+
+    @Test
+    @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD)
+    public void testGetFailedToken() {
+        final SimpleHttpResponse response =
+                SimpleHttpResponse.create(HttpStatus.SC_UNAUTHORIZED, "error", ContentType.APPLICATION_JSON);
+
+        when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class)))
+                .thenAnswer((Answer<Future<SimpleHttpResponse>>) invocation -> {
+                    final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0];
+                    if ("/auth/token".equals(req.getPath())) {
+                        return CompletableFuture.completedFuture(response);
+                    }
+                    throw new IllegalStateException("Unexpected value: " + req.getPath());
+                });
+
+
+        final TestObserver<ZoneId> observer = restClient.getTimeZone(ADAPTER).test();
+        observer.assertError(throwable -> throwable.getMessage()
+            .equals("Failed to obtain a token for host https://150.62.25.26: HTTP/1.1 401 Unauthorized"));
+    }
+
+    @Test
+    public void testReadFiles() throws IOException {
+        final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json");
+        final SimpleHttpResponse response =
+                SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON);
+
+        final SimpleHttpResponse zipResponse =
+                SimpleHttpResponse.create(HttpStatus.SC_OK, "mockZip", ContentType.APPLICATION_OCTET_STREAM);
+
+        when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class)))
+                .thenAnswer((Answer<Future<SimpleHttpResponse>>) invocation -> {
+                    final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0];
+                    switch (req.getPath()) {
+                        case "/auth/token":
+                            return CompletableFuture.completedFuture(response);
+                        case "/pm/files":
+                            return CompletableFuture.completedFuture(zipResponse);
+                        default:
+                            throw new IllegalStateException("Unexpected value: " + req.getPath());
+                    }
+                });
+
+
+        final TestObserver<ZipInputStream> observer = restClient.readFiles(ADAPTER).test();
+        final ZipInputStream expected = new ZipInputStream(new ByteArrayInputStream("mockZip".getBytes()));
+        observer.assertValue(zip -> Arrays.equals(zip.readAllBytes(), expected.readAllBytes()));
+    }
+
+    @Test
+    public void testReadFilesResponseFail() throws IOException {
+        final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json");
+        final SimpleHttpResponse response =
+                SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON);
+
+        final SimpleHttpResponse zipResponse = SimpleHttpResponse.create(HttpStatus.SC_BAD_REQUEST, "mockZip",
+                ContentType.APPLICATION_OCTET_STREAM);
+
+        when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class)))
+                .thenAnswer((Answer<Future<SimpleHttpResponse>>) invocation -> {
+                    final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0];
+                    switch (req.getPath()) {
+                        case "/auth/token":
+                            return CompletableFuture.completedFuture(response);
+                        case "/pm/files":
+                            return CompletableFuture.completedFuture(zipResponse);
+                        default:
+                            throw new IllegalStateException("Unexpected value: " + req.getPath());
+                    }
+                });
+
+
+        final TestObserver<ZipInputStream> observer = restClient.readFiles(ADAPTER).test();
+        observer.assertError(throwable -> throwable.getMessage()
+            .equals("Download files from 150.62.25.26 failed: HTTP/1.1 400 Bad Request"));
+    }
+
+    @Test
+    public void testGetTimeOffset() throws IOException {
+        final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json");
+        final SimpleHttpResponse response =
+                SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON);
+
+        final String timeZoneOFfsetResponseJson = JsonUtils.readJson("/json/timeZoneOffsetResponse.json");
+        final SimpleHttpResponse timeOffsetResponse =
+                SimpleHttpResponse.create(HttpStatus.SC_OK, timeZoneOFfsetResponseJson, ContentType.APPLICATION_JSON);
+
+        when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class)))
+                .thenAnswer((Answer<Future<SimpleHttpResponse>>) invocation -> {
+                    final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0];
+                    switch (req.getPath()) {
+                        case "/auth/token":
+                            return CompletableFuture.completedFuture(response);
+                        case "/system/timeZone":
+                            return CompletableFuture.completedFuture(timeOffsetResponse);
+                        default:
+                            throw new IllegalStateException("Unexpected value: " + req.getPath());
+                    }
+                });
+
+
+        final TestObserver<ZoneId> observer = restClient.getTimeZone(ADAPTER).test();
+        observer.assertValues(ZoneId.of("+02:00"));
+    }
+
+    @Test
+    public void testGetTimeOffsetFail() throws IOException {
+        final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json");
+        final SimpleHttpResponse response =
+                SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON);
+
+        final SimpleHttpResponse timeOffsetResponse =
+                SimpleHttpResponse.create(HttpStatus.SC_OK, "", ContentType.APPLICATION_JSON);
+
+        when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class)))
+                .thenAnswer((Answer<Future<SimpleHttpResponse>>) invocation -> {
+                    final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0];
+                    switch (req.getPath()) {
+                        case "/auth/token":
+                            return CompletableFuture.completedFuture(response);
+                        case "/system/timeZone":
+                            return CompletableFuture.completedFuture(timeOffsetResponse);
+                        default:
+                            throw new IllegalStateException("Unexpected value: " + req.getPath());
+                    }
+                });
+
+
+        final TestObserver<ZoneId> observer = restClient.getTimeZone(ADAPTER).test();
+        observer.assertError(throwable -> throwable.getMessage()
+            .equals("Get Zone offset failed for 150.62.25.26 . Empty output received"));
+    }
+}
\ No newline at end of file
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/JsonUtils.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/JsonUtils.java
new file mode 100644 (file)
index 0000000..60bf0ef
--- /dev/null
@@ -0,0 +1,13 @@
+package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import lombok.experimental.UtilityClass;
+import org.apache.commons.io.IOUtils;
+
+@UtilityClass
+final class JsonUtils {
+    static String readJson(final String url) throws IOException {
+        return IOUtils.toString(JsonUtils.class.getResourceAsStream(url), StandardCharsets.UTF_8);
+    }
+}
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/application.yaml b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/application.yaml
new file mode 100644 (file)
index 0000000..f81c8ff
--- /dev/null
@@ -0,0 +1,4 @@
+pm-rest-manager:
+  ran-token-endpoint: /auth/token
+  ran-pm-endpoint: /pm/files
+  ran-time-zone-offset-endpoint: /system/timeZone
\ No newline at end of file
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/timeZoneOffsetResponse.json b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/timeZoneOffsetResponse.json
new file mode 100644 (file)
index 0000000..c80769c
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "offset": "+02:00"
+}
\ No newline at end of file
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/tokenResponse.json b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/tokenResponse.json
new file mode 100644 (file)
index 0000000..2269aa0
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "token": "someRandomToken"
+}
\ No newline at end of file
diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644 (file)
index 0000000..ca6ee9c
--- /dev/null
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file