Adding some minor features
[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.HttpHost
28 import org.apache.http.HttpResponse
29 import org.apache.http.HttpStatus
30 import org.apache.http.client.ClientProtocolException
31 import org.apache.http.client.config.RequestConfig
32 import org.apache.http.client.entity.EntityBuilder
33 import org.apache.http.client.methods.HttpDelete
34 import org.apache.http.client.methods.HttpGet
35 import org.apache.http.client.methods.HttpPatch
36 import org.apache.http.client.methods.HttpPost
37 import org.apache.http.client.methods.HttpPut
38 import org.apache.http.client.methods.HttpUriRequest
39 import org.apache.http.conn.ssl.NoopHostnameVerifier
40 import org.apache.http.conn.ssl.SSLContextBuilder
41 import org.apache.http.conn.ssl.TrustAllStrategy
42 import org.apache.http.entity.StringEntity
43 import org.apache.http.impl.client.CloseableHttpClient
44 import org.apache.http.impl.client.HttpClients
45 import org.apache.http.message.BasicHeader
46 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestClientProperties
47 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestLibConstants
48 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
49 import org.onap.ccsdk.cds.blueprintsprocessor.rest.utils.WebClientUtils
50 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
51 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
52 import org.springframework.http.HttpHeaders
53 import org.springframework.http.HttpMethod
54 import java.io.IOException
55 import java.io.InputStream
56 import java.net.URI
57 import java.nio.charset.Charset
58 import java.nio.file.Files
59 import java.nio.file.Path
60
61 abstract class BaseBlueprintWebClientService<out E : RestClientProperties> : BlueprintWebClientService {
62
63     open fun host(uri: String): String {
64         val uri: URI = URI.create(getRestClientProperties().url + uri)
65         return uri.resolve(uri).toString()
66     }
67
68     abstract fun getRestClientProperties(): E
69
70     open fun getRequestConfig(): RequestConfig {
71         val requestConfigBuilder = RequestConfig.custom()
72         if (getRestClientProperties().connectionRequestTimeout > 0)
73             requestConfigBuilder.setConnectionRequestTimeout(getRestClientProperties().connectionRequestTimeout)
74         if (getRestClientProperties().connectTimeout > 0)
75             requestConfigBuilder.setConnectTimeout(getRestClientProperties().connectTimeout)
76         if (getRestClientProperties().socketTimeout > 0)
77             requestConfigBuilder.setSocketTimeout(getRestClientProperties().socketTimeout)
78         return requestConfigBuilder.build()
79     }
80
81     open fun https_proxy(): String? {
82         return getRestClientProperties().proxy
83     }
84
85     open fun httpClient(): CloseableHttpClient {
86         var httpClients = HttpClients.custom()
87         if (https_proxy() != null && https_proxy() != "") {
88             val proxyProtocol = https_proxy()?.split(':')?.get(0) ?: "http"
89             val proxyUri = https_proxy()?.split(':')?.get(1)?.replace("/", "") ?: ""
90             val proxyPort = https_proxy()?.split(':')?.get(2)?.toInt() ?: 0
91             if (proxyUri != "" && proxyPort != 0) {
92                 val proxy = HttpHost(proxyUri, proxyPort, proxyProtocol)
93                 httpClients = httpClients.setProxy(proxy)
94                     .setSSLContext(SSLContextBuilder().loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build())
95                     .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
96             }
97         }
98         return httpClients
99             .addInterceptorFirst(WebClientUtils.logRequest())
100             .addInterceptorLast(WebClientUtils.logResponse())
101             .setDefaultRequestConfig(getRequestConfig())
102             .build()
103     }
104
105     override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> {
106         return this.exchangeResource(methodType, path, request, defaultHeaders())
107     }
108
109     override fun exchangeResource(
110         methodType: String,
111         path: String,
112         request: String,
113         headers: Map<String, String>
114     ): WebClientResponse<String> {
115         /**
116          * TODO: Basic headers in the implementations of this client do not get added
117          * in blocking version, whereas in NB version defaultHeaders get added.
118          * the difference is in convertToBasicHeaders vs basicHeaders
119          */
120         val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(headers)
121         return when (HttpMethod.resolve(methodType)) {
122             HttpMethod.DELETE -> delete(path, convertedHeaders, String::class.java)
123             HttpMethod.GET -> get(path, convertedHeaders, String::class.java)
124             HttpMethod.POST -> post(path, request, convertedHeaders, String::class.java)
125             HttpMethod.PUT -> put(path, request, convertedHeaders, String::class.java)
126             HttpMethod.PATCH -> patch(path, request, convertedHeaders, String::class.java)
127             else -> throw BluePrintProcessorException(
128                 "Unsupported methodType($methodType) attempted on path($path)"
129             )
130         }
131     }
132
133     @Throws(IOException::class, ClientProtocolException::class)
134     protected fun performHttpCall(httpUriRequest: HttpUriRequest): WebClientResponse<String> {
135         val httpResponse = httpClient().execute(httpUriRequest)
136         val statusCode = httpResponse.statusLine.statusCode
137         httpResponse.entity.content.use {
138             val body = IOUtils.toString(it, Charset.defaultCharset())
139             return WebClientResponse(statusCode, body)
140         }
141     }
142
143     open override fun uploadBinaryFile(path: String, filePath: Path): WebClientResponse<String> {
144         val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(defaultHeaders())
145         val httpPost = HttpPost(host(path))
146         val entity = EntityBuilder.create().setBinary(Files.readAllBytes(filePath)).build()
147         httpPost.setEntity(entity)
148         RestLoggerService.httpInvoking(convertedHeaders)
149         httpPost.setHeaders(convertedHeaders)
150         return performHttpCall(httpPost)
151     }
152
153     // TODO: convert to multi-map
154     override fun convertToBasicHeaders(headers: Map<String, String>): Array<BasicHeader> {
155         return headers.map { BasicHeader(it.key, it.value) }.toTypedArray()
156     }
157
158     open fun <T> delete(path: String, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
159         val httpDelete = HttpDelete(host(path))
160         RestLoggerService.httpInvoking(headers)
161         httpDelete.setHeaders(headers)
162         return performCallAndExtractTypedWebClientResponse(httpDelete, responseType)
163     }
164
165     open fun <T> get(path: String, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
166         val httpGet = HttpGet(host(path))
167         RestLoggerService.httpInvoking(headers)
168         httpGet.setHeaders(headers)
169         return performCallAndExtractTypedWebClientResponse(httpGet, responseType)
170     }
171
172     open fun <T> post(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
173         val httpPost = HttpPost(host(path))
174         val entity = StringEntity(strRequest(request))
175         httpPost.entity = entity
176         RestLoggerService.httpInvoking(headers)
177         httpPost.setHeaders(headers)
178         return performCallAndExtractTypedWebClientResponse(httpPost, responseType)
179     }
180
181     open fun <T> put(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
182         val httpPut = HttpPut(host(path))
183         val entity = StringEntity(strRequest(request))
184         httpPut.entity = entity
185         RestLoggerService.httpInvoking(headers)
186         httpPut.setHeaders(headers)
187         return performCallAndExtractTypedWebClientResponse(httpPut, responseType)
188     }
189
190     open fun <T> patch(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
191         val httpPatch = HttpPatch(host(path))
192         val entity = StringEntity(strRequest(request))
193         httpPatch.entity = entity
194         RestLoggerService.httpInvoking(headers)
195         httpPatch.setHeaders(headers)
196         return performCallAndExtractTypedWebClientResponse(httpPatch, responseType)
197     }
198
199     /**
200      * Perform the HTTP call and return HTTP status code and body.
201      * @param httpUriRequest {@link HttpUriRequest} object
202      * @return {@link WebClientResponse} object
203      * http client may throw IOException and ClientProtocolException on error
204      */
205
206     @Throws(IOException::class, ClientProtocolException::class)
207     protected fun <T> performCallAndExtractTypedWebClientResponse(
208         httpUriRequest: HttpUriRequest,
209         responseType: Class<T>
210     ):
211         WebClientResponse<T> {
212             val httpResponse = httpClient().execute(httpUriRequest)
213             val statusCode = httpResponse.statusLine.statusCode
214             val entity: HttpEntity? = httpResponse.entity
215             if (canResponseHaveBody(httpResponse)) {
216                 entity!!.content.use {
217                     val body = getResponse(it, responseType)
218                     return WebClientResponse(statusCode, body)
219                 }
220             } else {
221                 val constructor = responseType.getConstructor()
222                 val body = constructor.newInstance()
223                 return WebClientResponse(statusCode, body)
224             }
225         }
226     fun canResponseHaveBody(response: HttpResponse): Boolean {
227         val status = response.statusLine.statusCode
228         return response.entity !== null &&
229             status != HttpStatus.SC_NO_CONTENT &&
230             status != HttpStatus.SC_NOT_MODIFIED &&
231             status != HttpStatus.SC_RESET_CONTENT
232     }
233
234     open suspend fun getNB(path: String): WebClientResponse<String> {
235         return getNB(path, null, String::class.java)
236     }
237
238     open suspend fun getNB(path: String, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
239         return getNB(path, additionalHeaders, String::class.java)
240     }
241
242     open suspend fun <T> getNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
243         WebClientResponse<T> = withContext(Dispatchers.IO) {
244             get(path, additionalHeaders!!, responseType)
245         }
246
247     open suspend fun postNB(path: String, request: Any): WebClientResponse<String> {
248         return postNB(path, request, null, String::class.java)
249     }
250
251     open suspend fun postNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
252         return postNB(path, request, additionalHeaders, String::class.java)
253     }
254
255     open suspend fun <T> postNB(
256         path: String,
257         request: Any,
258         additionalHeaders: Array<BasicHeader>?,
259         responseType: Class<T>
260     ): WebClientResponse<T> = withContext(Dispatchers.IO) {
261         post(path, request, additionalHeaders!!, responseType)
262     }
263
264     open suspend fun putNB(path: String, request: Any): WebClientResponse<String> {
265         return putNB(path, request, null, String::class.java)
266     }
267
268     open suspend fun putNB(
269         path: String,
270         request: Any,
271         additionalHeaders: Array<BasicHeader>?
272     ): WebClientResponse<String> {
273         return putNB(path, request, additionalHeaders, String::class.java)
274     }
275
276     open suspend fun <T> putNB(
277         path: String,
278         request: Any,
279         additionalHeaders: Array<BasicHeader>?,
280         responseType: Class<T>
281     ): WebClientResponse<T> = withContext(Dispatchers.IO) {
282         put(path, request, additionalHeaders!!, responseType)
283     }
284
285     open suspend fun <T> deleteNB(path: String): WebClientResponse<String> {
286         return deleteNB(path, null, String::class.java)
287     }
288
289     open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?):
290         WebClientResponse<String> {
291             return deleteNB(path, additionalHeaders, String::class.java)
292         }
293
294     open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
295         WebClientResponse<T> = withContext(Dispatchers.IO) {
296             delete(path, additionalHeaders!!, responseType)
297         }
298
299     open suspend fun <T> patchNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
300         WebClientResponse<T> = withContext(Dispatchers.IO) {
301             patch(path, request, additionalHeaders!!, responseType)
302         }
303
304     override suspend fun exchangeNB(methodType: String, path: String, request: Any): WebClientResponse<String> {
305         return exchangeNB(
306             methodType, path, request, hashMapOf(),
307             String::class.java
308         )
309     }
310
311     override suspend fun exchangeNB(methodType: String, path: String, request: Any, additionalHeaders: Map<String, String>?):
312         WebClientResponse<String> {
313             return exchangeNB(methodType, path, request, additionalHeaders, String::class.java)
314         }
315
316     override suspend fun <T> exchangeNB(
317         methodType: String,
318         path: String,
319         request: Any,
320         additionalHeaders: Map<String, String>?,
321         responseType: Class<T>
322     ): WebClientResponse<T> {
323
324         // TODO: possible inconsistency
325         // NOTE: this basic headers function is different from non-blocking
326         val convertedHeaders: Array<BasicHeader> = basicHeaders(additionalHeaders!!)
327         return when (HttpMethod.resolve(methodType)) {
328             HttpMethod.GET -> getNB(path, convertedHeaders, responseType)
329             HttpMethod.POST -> postNB(path, request, convertedHeaders, responseType)
330             HttpMethod.DELETE -> deleteNB(path, convertedHeaders, responseType)
331             HttpMethod.PUT -> putNB(path, request, convertedHeaders, responseType)
332             HttpMethod.PATCH -> patchNB(path, request, convertedHeaders, responseType)
333             else -> throw BluePrintProcessorException("Unsupported methodType($methodType)")
334         }
335     }
336
337     protected fun strRequest(request: Any): String {
338         return when (request) {
339             is String -> request.toString()
340             is JsonNode -> request.toString()
341             else -> JacksonUtils.getJson(request)
342         }
343     }
344
345     protected fun <T> getResponse(it: InputStream, responseType: Class<T>): T {
346         return if (responseType == String::class.java) {
347             IOUtils.toString(it, Charset.defaultCharset()) as T
348         } else {
349             JacksonUtils.readValue(it, responseType)!!
350         }
351     }
352
353     protected fun basicHeaders(headers: Map<String, String>?):
354         Array<BasicHeader> {
355             val basicHeaders = mutableListOf<BasicHeader>()
356             defaultHeaders().forEach { (name, value) ->
357                 basicHeaders.add(BasicHeader(name, value))
358             }
359             headers?.forEach { name, value ->
360                 basicHeaders.add(BasicHeader(name, value))
361             }
362             return basicHeaders.toTypedArray()
363         }
364
365     // Non Blocking Rest Implementation
366     suspend fun httpClientNB(): CloseableHttpClient {
367         return httpClient()
368     }
369
370     open fun verifyAdditionalHeaders(): Map<String, String> {
371         return verifyAdditionalHeaders(getRestClientProperties())
372     }
373
374     open fun verifyAdditionalHeaders(restClientProperties: RestClientProperties): Map<String, String> {
375         val customHeaders: MutableMap<String, String> = mutableMapOf()
376         // Extract additionalHeaders from the requestProperties and
377         // throw an error if HttpHeaders.AUTHORIZATION key (headers are case-insensitive)
378         restClientProperties.additionalHeaders?.let {
379             if (it.keys.map { k -> k.toLowerCase().trim() }.contains(HttpHeaders.AUTHORIZATION.toLowerCase())) {
380                 val errMsg = "Error in definition of endpoint ${restClientProperties.url}." +
381                     " User-supplied \"additionalHeaders\" cannot contain AUTHORIZATION header with" +
382                     " auth-type \"${RestLibConstants.TYPE_BASIC_AUTH}\""
383                 WebClientUtils.log.error(errMsg)
384                 throw BluePrintProcessorException(errMsg)
385             } else {
386                 customHeaders.putAll(it)
387             }
388         }
389         return customHeaders
390     }
391 }