K8sPlugin: support UAT functionality
[ccsdk/cds.git] / ms / blueprintsprocessor / modules / commons / rest-lib / src / main / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / rest / service / BaseBlueprintWebClientService.kt
1 /*
2  * Copyright © 2017-2019 AT&T, Bell Canada, Nordix Foundation
3  * Modifications Copyright © 2018-2019 IBM.
4  * Modifications Copyright © 2019 Huawei.
5  * Modifications Copyright © 2022 Deutsche Telekom AG.
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *     http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19
20 package org.onap.ccsdk.cds.blueprintsprocessor.rest.service
21
22 import com.fasterxml.jackson.databind.JsonNode
23 import kotlinx.coroutines.Dispatchers
24 import kotlinx.coroutines.withContext
25 import org.apache.commons.io.IOUtils
26 import org.apache.http.HttpEntity
27 import org.apache.http.HttpResponse
28 import org.apache.http.HttpStatus
29 import org.apache.http.client.ClientProtocolException
30 import org.apache.http.client.config.RequestConfig
31 import org.apache.http.client.entity.EntityBuilder
32 import org.apache.http.client.methods.HttpDelete
33 import org.apache.http.client.methods.HttpGet
34 import org.apache.http.client.methods.HttpPatch
35 import org.apache.http.client.methods.HttpPost
36 import org.apache.http.client.methods.HttpPut
37 import org.apache.http.client.methods.HttpUriRequest
38 import org.apache.http.entity.StringEntity
39 import org.apache.http.impl.client.CloseableHttpClient
40 import org.apache.http.impl.client.HttpClients
41 import org.apache.http.message.BasicHeader
42 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestClientProperties
43 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestLibConstants
44 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
45 import org.onap.ccsdk.cds.blueprintsprocessor.rest.utils.WebClientUtils
46 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
47 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
48 import org.springframework.http.HttpHeaders
49 import org.springframework.http.HttpMethod
50 import java.io.IOException
51 import java.io.InputStream
52 import java.net.URI
53 import java.nio.charset.Charset
54 import java.nio.file.Files
55 import java.nio.file.Path
56
57 abstract class BaseBlueprintWebClientService<out E : RestClientProperties> : BlueprintWebClientService {
58
59     open fun host(uri: String): String {
60         val uri: URI = URI.create(getRestClientProperties().url + uri)
61         return uri.resolve(uri).toString()
62     }
63
64     abstract fun getRestClientProperties(): E
65
66     open fun getRequestConfig(): RequestConfig {
67         val requestConfigBuilder = RequestConfig.custom()
68         if (getRestClientProperties().connectionRequestTimeout > 0)
69             requestConfigBuilder.setConnectionRequestTimeout(getRestClientProperties().connectionRequestTimeout)
70         if (getRestClientProperties().connectTimeout > 0)
71             requestConfigBuilder.setConnectTimeout(getRestClientProperties().connectTimeout)
72         if (getRestClientProperties().socketTimeout > 0)
73             requestConfigBuilder.setSocketTimeout(getRestClientProperties().socketTimeout)
74         return requestConfigBuilder.build()
75     }
76
77     open fun httpClient(): CloseableHttpClient {
78         return HttpClients.custom()
79             .addInterceptorFirst(WebClientUtils.logRequest())
80             .addInterceptorLast(WebClientUtils.logResponse())
81             .setDefaultRequestConfig(getRequestConfig())
82             .build()
83     }
84
85     override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> {
86         return this.exchangeResource(methodType, path, request, defaultHeaders())
87     }
88
89     override fun exchangeResource(
90         methodType: String,
91         path: String,
92         request: String,
93         headers: Map<String, String>
94     ): WebClientResponse<String> {
95         /**
96          * TODO: Basic headers in the implementations of this client do not get added
97          * in blocking version, whereas in NB version defaultHeaders get added.
98          * the difference is in convertToBasicHeaders vs basicHeaders
99          */
100         val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(headers)
101         return when (HttpMethod.resolve(methodType)) {
102             HttpMethod.DELETE -> delete(path, convertedHeaders, String::class.java)
103             HttpMethod.GET -> get(path, convertedHeaders, String::class.java)
104             HttpMethod.POST -> post(path, request, convertedHeaders, String::class.java)
105             HttpMethod.PUT -> put(path, request, convertedHeaders, String::class.java)
106             HttpMethod.PATCH -> patch(path, request, convertedHeaders, String::class.java)
107             else -> throw BluePrintProcessorException(
108                 "Unsupported methodType($methodType) attempted on path($path)"
109             )
110         }
111     }
112
113     @Throws(IOException::class, ClientProtocolException::class)
114     protected fun performHttpCall(httpUriRequest: HttpUriRequest): WebClientResponse<String> {
115         val httpResponse = httpClient().execute(httpUriRequest)
116         val statusCode = httpResponse.statusLine.statusCode
117         httpResponse.entity.content.use {
118             val body = IOUtils.toString(it, Charset.defaultCharset())
119             return WebClientResponse(statusCode, body)
120         }
121     }
122
123     open override fun uploadBinaryFile(path: String, filePath: Path): WebClientResponse<String> {
124         val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(defaultHeaders())
125         val httpPost = HttpPost(host(path))
126         val entity = EntityBuilder.create().setBinary(Files.readAllBytes(filePath)).build()
127         httpPost.setEntity(entity)
128         RestLoggerService.httpInvoking(convertedHeaders)
129         httpPost.setHeaders(convertedHeaders)
130         return performHttpCall(httpPost)
131     }
132
133     // TODO: convert to multi-map
134     override fun convertToBasicHeaders(headers: Map<String, String>): Array<BasicHeader> {
135         return headers.map { BasicHeader(it.key, it.value) }.toTypedArray()
136     }
137
138     open fun <T> delete(path: String, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
139         val httpDelete = HttpDelete(host(path))
140         RestLoggerService.httpInvoking(headers)
141         httpDelete.setHeaders(headers)
142         return performCallAndExtractTypedWebClientResponse(httpDelete, responseType)
143     }
144
145     open fun <T> get(path: String, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
146         val httpGet = HttpGet(host(path))
147         RestLoggerService.httpInvoking(headers)
148         httpGet.setHeaders(headers)
149         return performCallAndExtractTypedWebClientResponse(httpGet, responseType)
150     }
151
152     open fun <T> post(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
153         val httpPost = HttpPost(host(path))
154         val entity = StringEntity(strRequest(request))
155         httpPost.entity = entity
156         RestLoggerService.httpInvoking(headers)
157         httpPost.setHeaders(headers)
158         return performCallAndExtractTypedWebClientResponse(httpPost, responseType)
159     }
160
161     open fun <T> put(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
162         val httpPut = HttpPut(host(path))
163         val entity = StringEntity(strRequest(request))
164         httpPut.entity = entity
165         RestLoggerService.httpInvoking(headers)
166         httpPut.setHeaders(headers)
167         return performCallAndExtractTypedWebClientResponse(httpPut, responseType)
168     }
169
170     open fun <T> patch(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
171         val httpPatch = HttpPatch(host(path))
172         val entity = StringEntity(strRequest(request))
173         httpPatch.entity = entity
174         RestLoggerService.httpInvoking(headers)
175         httpPatch.setHeaders(headers)
176         return performCallAndExtractTypedWebClientResponse(httpPatch, responseType)
177     }
178
179     /**
180      * Perform the HTTP call and return HTTP status code and body.
181      * @param httpUriRequest {@link HttpUriRequest} object
182      * @return {@link WebClientResponse} object
183      * http client may throw IOException and ClientProtocolException on error
184      */
185
186     @Throws(IOException::class, ClientProtocolException::class)
187     protected fun <T> performCallAndExtractTypedWebClientResponse(
188         httpUriRequest: HttpUriRequest,
189         responseType: Class<T>
190     ):
191         WebClientResponse<T> {
192             val httpResponse = httpClient().execute(httpUriRequest)
193             val statusCode = httpResponse.statusLine.statusCode
194             val entity: HttpEntity? = httpResponse.entity
195             if (canResponseHaveBody(httpResponse)) {
196                 entity!!.content.use {
197                     val body = getResponse(it, responseType)
198                     return WebClientResponse(statusCode, body)
199                 }
200             } else {
201                 val constructor = responseType.getConstructor()
202                 val body = constructor.newInstance()
203                 return WebClientResponse(statusCode, body)
204             }
205         }
206     fun canResponseHaveBody(response: HttpResponse): Boolean {
207         val status = response.statusLine.statusCode
208         return response.entity !== null &&
209             status != HttpStatus.SC_NO_CONTENT &&
210             status != HttpStatus.SC_NOT_MODIFIED &&
211             status != HttpStatus.SC_RESET_CONTENT
212     }
213
214     open suspend fun getNB(path: String): WebClientResponse<String> {
215         return getNB(path, null, String::class.java)
216     }
217
218     open suspend fun getNB(path: String, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
219         return getNB(path, additionalHeaders, String::class.java)
220     }
221
222     open suspend fun <T> getNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
223         WebClientResponse<T> = withContext(Dispatchers.IO) {
224             get(path, additionalHeaders!!, responseType)
225         }
226
227     open suspend fun postNB(path: String, request: Any): WebClientResponse<String> {
228         return postNB(path, request, null, String::class.java)
229     }
230
231     open suspend fun postNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
232         return postNB(path, request, additionalHeaders, String::class.java)
233     }
234
235     open suspend fun <T> postNB(
236         path: String,
237         request: Any,
238         additionalHeaders: Array<BasicHeader>?,
239         responseType: Class<T>
240     ): WebClientResponse<T> = withContext(Dispatchers.IO) {
241         post(path, request, additionalHeaders!!, responseType)
242     }
243
244     open suspend fun putNB(path: String, request: Any): WebClientResponse<String> {
245         return putNB(path, request, null, String::class.java)
246     }
247
248     open suspend fun putNB(
249         path: String,
250         request: Any,
251         additionalHeaders: Array<BasicHeader>?
252     ): WebClientResponse<String> {
253         return putNB(path, request, additionalHeaders, String::class.java)
254     }
255
256     open suspend fun <T> putNB(
257         path: String,
258         request: Any,
259         additionalHeaders: Array<BasicHeader>?,
260         responseType: Class<T>
261     ): WebClientResponse<T> = withContext(Dispatchers.IO) {
262         put(path, request, additionalHeaders!!, responseType)
263     }
264
265     open suspend fun <T> deleteNB(path: String): WebClientResponse<String> {
266         return deleteNB(path, null, String::class.java)
267     }
268
269     open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?):
270         WebClientResponse<String> {
271             return deleteNB(path, additionalHeaders, String::class.java)
272         }
273
274     open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
275         WebClientResponse<T> = withContext(Dispatchers.IO) {
276             delete(path, additionalHeaders!!, responseType)
277         }
278
279     open suspend fun <T> patchNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
280         WebClientResponse<T> = withContext(Dispatchers.IO) {
281             patch(path, request, additionalHeaders!!, responseType)
282         }
283
284     override suspend fun exchangeNB(methodType: String, path: String, request: Any): WebClientResponse<String> {
285         return exchangeNB(
286             methodType, path, request, hashMapOf(),
287             String::class.java
288         )
289     }
290
291     override suspend fun exchangeNB(methodType: String, path: String, request: Any, additionalHeaders: Map<String, String>?):
292         WebClientResponse<String> {
293             return exchangeNB(methodType, path, request, additionalHeaders, String::class.java)
294         }
295
296     override suspend fun <T> exchangeNB(
297         methodType: String,
298         path: String,
299         request: Any,
300         additionalHeaders: Map<String, String>?,
301         responseType: Class<T>
302     ): WebClientResponse<T> {
303
304         // TODO: possible inconsistency
305         // NOTE: this basic headers function is different from non-blocking
306         val convertedHeaders: Array<BasicHeader> = basicHeaders(additionalHeaders!!)
307         return when (HttpMethod.resolve(methodType)) {
308             HttpMethod.GET -> getNB(path, convertedHeaders, responseType)
309             HttpMethod.POST -> postNB(path, request, convertedHeaders, responseType)
310             HttpMethod.DELETE -> deleteNB(path, convertedHeaders, responseType)
311             HttpMethod.PUT -> putNB(path, request, convertedHeaders, responseType)
312             HttpMethod.PATCH -> patchNB(path, request, convertedHeaders, responseType)
313             else -> throw BluePrintProcessorException("Unsupported methodType($methodType)")
314         }
315     }
316
317     protected fun strRequest(request: Any): String {
318         return when (request) {
319             is String -> request.toString()
320             is JsonNode -> request.toString()
321             else -> JacksonUtils.getJson(request)
322         }
323     }
324
325     protected fun <T> getResponse(it: InputStream, responseType: Class<T>): T {
326         return if (responseType == String::class.java) {
327             IOUtils.toString(it, Charset.defaultCharset()) as T
328         } else {
329             JacksonUtils.readValue(it, responseType)!!
330         }
331     }
332
333     protected fun basicHeaders(headers: Map<String, String>?):
334         Array<BasicHeader> {
335             val basicHeaders = mutableListOf<BasicHeader>()
336             defaultHeaders().forEach { (name, value) ->
337                 basicHeaders.add(BasicHeader(name, value))
338             }
339             headers?.forEach { name, value ->
340                 basicHeaders.add(BasicHeader(name, value))
341             }
342             return basicHeaders.toTypedArray()
343         }
344
345     // Non Blocking Rest Implementation
346     suspend fun httpClientNB(): CloseableHttpClient {
347         return httpClient()
348     }
349
350     open fun verifyAdditionalHeaders(): Map<String, String> {
351         return verifyAdditionalHeaders(getRestClientProperties())
352     }
353
354     open fun verifyAdditionalHeaders(restClientProperties: RestClientProperties): Map<String, String> {
355         val customHeaders: MutableMap<String, String> = mutableMapOf()
356         // Extract additionalHeaders from the requestProperties and
357         // throw an error if HttpHeaders.AUTHORIZATION key (headers are case-insensitive)
358         restClientProperties.additionalHeaders?.let {
359             if (it.keys.map { k -> k.toLowerCase().trim() }.contains(HttpHeaders.AUTHORIZATION.toLowerCase())) {
360                 val errMsg = "Error in definition of endpoint ${restClientProperties.url}." +
361                     " User-supplied \"additionalHeaders\" cannot contain AUTHORIZATION header with" +
362                     " auth-type \"${RestLibConstants.TYPE_BASIC_AUTH}\""
363                 WebClientUtils.log.error(errMsg)
364                 throw BluePrintProcessorException(errMsg)
365             } else {
366                 customHeaders.putAll(it)
367             }
368         }
369         return customHeaders
370     }
371 }