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 java.io.IOException
46 import java.io.InputStream
47 import java.nio.charset.Charset
49 interface BlueprintWebClientService {
51 fun defaultHeaders(): Map<String, String>
53 fun host(uri: String): String
55 fun httpClient(): CloseableHttpClient {
56 return HttpClients.custom()
57 .addInterceptorFirst(WebClientUtils.logRequest())
58 .addInterceptorLast(WebClientUtils.logResponse())
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]
65 suspend fun <T> retry(
67 initialDelay: Long = 0,
69 block: suspend (Int) -> T
71 val exceptionBlock = { e: Exception ->
72 if (e !is BluePrintRetryException) {
76 return BluePrintIOUtils.retry(times, initialDelay, delay, block, exceptionBlock)
79 fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> {
80 return this.exchangeResource(methodType, path, request, defaultHeaders())
87 headers: Map<String, String>
88 ): WebClientResponse<String> {
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
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)"
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()
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)
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)
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)
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)
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)
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
160 @Throws(IOException::class, ClientProtocolException::class)
161 private fun <T> performCallAndExtractTypedWebClientResponse(
162 httpUriRequest: HttpUriRequest,
163 responseType: Class<T>
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)
174 suspend fun getNB(path: String): WebClientResponse<String> {
175 return getNB(path, null, String::class.java)
178 suspend fun getNB(path: String, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
179 return getNB(path, additionalHeaders, String::class.java)
182 suspend fun <T> getNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
183 WebClientResponse<T> = withContext(Dispatchers.IO) {
184 get(path, additionalHeaders!!, responseType)
187 suspend fun postNB(path: String, request: Any): WebClientResponse<String> {
188 return postNB(path, request, null, String::class.java)
191 suspend fun postNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
192 return postNB(path, request, additionalHeaders, String::class.java)
195 suspend fun <T> postNB(
198 additionalHeaders: Array<BasicHeader>?,
199 responseType: Class<T>
200 ): WebClientResponse<T> = withContext(Dispatchers.IO) {
201 post(path, request, additionalHeaders!!, responseType)
204 suspend fun putNB(path: String, request: Any): WebClientResponse<String> {
205 return putNB(path, request, null, String::class.java)
211 additionalHeaders: Array<BasicHeader>?
212 ): WebClientResponse<String> {
213 return putNB(path, request, additionalHeaders, String::class.java)
216 suspend fun <T> putNB(
219 additionalHeaders: Array<BasicHeader>?,
220 responseType: Class<T>
221 ): WebClientResponse<T> = withContext(Dispatchers.IO) {
222 put(path, request, additionalHeaders!!, responseType)
225 suspend fun <T> deleteNB(path: String): WebClientResponse<String> {
226 return deleteNB(path, null, String::class.java)
229 suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?):
230 WebClientResponse<String> {
231 return deleteNB(path, additionalHeaders, String::class.java)
234 suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
235 WebClientResponse<T> = withContext(Dispatchers.IO) {
236 delete(path, additionalHeaders!!, responseType)
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)
244 suspend fun exchangeNB(methodType: String, path: String, request: Any): WebClientResponse<String> {
246 methodType, path, request, hashMapOf(),
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)
256 suspend fun <T> exchangeNB(
260 additionalHeaders: Map<String, String>?,
261 responseType: Class<T>
262 ): WebClientResponse<T> {
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)")
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)
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
289 JacksonUtils.readValue(it, responseType)!!
293 private fun basicHeaders(headers: Map<String, String>?):
295 val basicHeaders = mutableListOf<BasicHeader>()
296 defaultHeaders().forEach { (name, value) ->
297 basicHeaders.add(BasicHeader(name, value))
299 headers?.forEach { name, value ->
300 basicHeaders.add(BasicHeader(name, value))
302 return basicHeaders.toTypedArray()
305 // Non Blocking Rest Implementation
306 suspend fun httpClientNB(): CloseableHttpClient {
307 return HttpClients.custom()
308 .addInterceptorFirst(WebClientUtils.logRequest())
309 .addInterceptorLast(WebClientUtils.logResponse())
313 // TODO maybe there could be cases where we care about return headers?
314 data class WebClientResponse<T>(val status: Int, val body: T)
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)
328 customHeaders.putAll(it)