<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
<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
../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-->
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();
}
--- /dev/null
+<?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
--- /dev/null
+/*
+ * ============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);
+ }
+ }
+}
--- /dev/null
+/*
+ * ============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));
+ }
+}
--- /dev/null
+/*
+ * ============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();
+ }
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+/*
+ * ============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;
+}
--- /dev/null
+/*
+ * ============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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+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
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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
--- /dev/null
+{
+ "offset": "+02:00"
+}
\ No newline at end of file
--- /dev/null
+{
+ "token": "someRandomToken"
+}
\ No newline at end of file
--- /dev/null
+mock-maker-inline
\ No newline at end of file