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