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