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