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