2 * Copyright © 2017-2019 AT&T, Bell Canada, Nordix Foundation
3 * Modifications Copyright © 2018-2019 IBM.
4 * Modifications Copyright © 2019 Huawei.
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
10 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 package org.onap.ccsdk.cds.blueprintsprocessor.rest.service
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
51 interface BlueprintWebClientService {
53 fun defaultHeaders(): Map<String, String>
55 fun host(uri: String): String
57 fun httpClient(): CloseableHttpClient {
58 return HttpClients.custom()
59 .addInterceptorFirst(WebClientUtils.logRequest())
60 .addInterceptorLast(WebClientUtils.logResponse())
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]
67 suspend fun <T> retry(
69 initialDelay: Long = 0,
71 block: suspend (Int) -> T
73 val exceptionBlock = { e: Exception ->
74 if (e !is BluePrintRetryException) {
78 return BluePrintIOUtils.retry(times, initialDelay, delay, block, exceptionBlock)
81 fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> {
82 return this.exchangeResource(methodType, path, request, defaultHeaders())
89 headers: Map<String, String>
90 ): WebClientResponse<String> {
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
96 val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(headers)
97 val encodedPath = UriUtils.encodePath(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)"
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()
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)
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)
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)
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)
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)
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
163 @Throws(IOException::class, ClientProtocolException::class)
164 private fun <T> performCallAndExtractTypedWebClientResponse(
165 httpUriRequest: HttpUriRequest,
166 responseType: Class<T>
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)
177 suspend fun getNB(path: String): WebClientResponse<String> {
178 return getNB(path, null, String::class.java)
181 suspend fun getNB(path: String, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
182 return getNB(path, additionalHeaders, String::class.java)
185 suspend fun <T> getNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
186 WebClientResponse<T> = withContext(Dispatchers.IO) {
187 get(path, additionalHeaders!!, responseType)
190 suspend fun postNB(path: String, request: Any): WebClientResponse<String> {
191 return postNB(path, request, null, String::class.java)
194 suspend fun postNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
195 return postNB(path, request, additionalHeaders, String::class.java)
198 suspend fun <T> postNB(
201 additionalHeaders: Array<BasicHeader>?,
202 responseType: Class<T>
203 ): WebClientResponse<T> = withContext(Dispatchers.IO) {
204 post(path, request, additionalHeaders!!, responseType)
207 suspend fun putNB(path: String, request: Any): WebClientResponse<String> {
208 return putNB(path, request, null, String::class.java)
214 additionalHeaders: Array<BasicHeader>?
215 ): WebClientResponse<String> {
216 return putNB(path, request, additionalHeaders, String::class.java)
219 suspend fun <T> putNB(
222 additionalHeaders: Array<BasicHeader>?,
223 responseType: Class<T>
224 ): WebClientResponse<T> = withContext(Dispatchers.IO) {
225 put(path, request, additionalHeaders!!, responseType)
228 suspend fun <T> deleteNB(path: String): WebClientResponse<String> {
229 return deleteNB(path, null, String::class.java)
232 suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?):
233 WebClientResponse<String> {
234 return deleteNB(path, additionalHeaders, String::class.java)
237 suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
238 WebClientResponse<T> = withContext(Dispatchers.IO) {
239 delete(path, additionalHeaders!!, responseType)
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)
247 suspend fun exchangeNB(methodType: String, path: String, request: Any): WebClientResponse<String> {
249 methodType, path, request, hashMapOf(),
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)
259 suspend fun <T> exchangeNB(
263 additionalHeaders: Map<String, String>?,
264 responseType: Class<T>
265 ): WebClientResponse<T> {
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)")
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)
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
292 JacksonUtils.readValue(it, responseType)!!
296 private fun basicHeaders(headers: Map<String, String>?):
298 val basicHeaders = mutableListOf<BasicHeader>()
299 defaultHeaders().forEach { (name, value) ->
300 basicHeaders.add(BasicHeader(name, value))
302 headers?.forEach { name, value ->
303 basicHeaders.add(BasicHeader(name, value))
305 return basicHeaders.toTypedArray()
308 // Non Blocking Rest Implementation
309 suspend fun httpClientNB(): CloseableHttpClient {
310 return HttpClients.custom()
311 .addInterceptorFirst(WebClientUtils.logRequest())
312 .addInterceptorLast(WebClientUtils.logResponse())
316 // TODO maybe there could be cases where we care about return headers?
317 data class WebClientResponse<T>(val status: Int, val body: T)
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)
331 customHeaders.putAll(it)