/* * Copyright © 2017-2019 AT&T, Bell Canada, Nordix Foundation * Modifications Copyright © 2018-2019 IBM. * Modifications Copyright © 2019 Huawei. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onap.ccsdk.cds.blueprintsprocessor.rest.service import com.fasterxml.jackson.databind.JsonNode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.apache.commons.io.IOUtils import org.apache.http.client.ClientProtocolException import org.apache.http.client.methods.* import org.apache.http.entity.StringEntity import org.apache.http.impl.client.CloseableHttpClient import org.apache.http.impl.client.HttpClients import org.apache.http.message.BasicHeader import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestClientProperties import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestLibConstants import org.onap.ccsdk.cds.blueprintsprocessor.rest.utils.WebClientUtils import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintRetryException import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintIOUtils import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import java.io.IOException import java.io.InputStream import java.nio.charset.Charset interface BlueprintWebClientService { fun defaultHeaders(): Map fun host(uri: String): String fun httpClient(): CloseableHttpClient { return HttpClients.custom() .addInterceptorFirst(WebClientUtils.logRequest()) .addInterceptorLast(WebClientUtils.logResponse()) .build() } /** High performance non blocking Retry function, If execution block [block] throws BluePrintRetryException * exception then this will perform wait and retrigger accoring to times [times] with delay [delay] */ suspend fun retry(times: Int = 1, initialDelay: Long = 0, delay: Long = 1000, block: suspend (Int) -> T): T { val exceptionBlock = { e: Exception -> if (e !is BluePrintRetryException) { throw e } } return BluePrintIOUtils.retry(times, initialDelay, delay, block, exceptionBlock) } fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse { return this.exchangeResource(methodType, path, request, defaultHeaders()) } fun exchangeResource(methodType: String, path: String, request: String, headers: Map): WebClientResponse { /** * TODO: Basic headers in the implementations of this client do not get added * in blocking version, whereas in NB version defaultHeaders get added. * the difference is in convertToBasicHeaders vs basicHeaders */ val convertedHeaders: Array = convertToBasicHeaders(headers) return when (HttpMethod.resolve(methodType)) { HttpMethod.DELETE -> delete(path, convertedHeaders, String::class.java) HttpMethod.GET -> get(path, convertedHeaders, String::class.java) HttpMethod.POST -> post(path, request, convertedHeaders, String::class.java) HttpMethod.PUT -> put(path, request, convertedHeaders, String::class.java) HttpMethod.PATCH -> patch(path, request, convertedHeaders, String::class.java) else -> throw BluePrintProcessorException( "Unsupported methodType($methodType) attempted on path($path)") } } //TODO: convert to multi-map fun convertToBasicHeaders(headers: Map): Array { return headers.map { BasicHeader(it.key, it.value) }.toTypedArray() } fun delete(path: String, headers: Array, responseType: Class): WebClientResponse { val httpDelete = HttpDelete(host(path)) httpDelete.setHeaders(headers) return performCallAndExtractTypedWebClientResponse(httpDelete, responseType) } fun get(path: String, headers: Array, responseType: Class): WebClientResponse { val httpGet = HttpGet(host(path)) httpGet.setHeaders(headers) return performCallAndExtractTypedWebClientResponse(httpGet, responseType) } fun post(path: String, request: Any, headers: Array, responseType: Class): WebClientResponse { val httpPost = HttpPost(host(path)) val entity = StringEntity(strRequest(request)) httpPost.entity = entity httpPost.setHeaders(headers) return performCallAndExtractTypedWebClientResponse(httpPost, responseType) } fun put(path: String, request: Any, headers: Array, responseType: Class): WebClientResponse { val httpPut = HttpPut(host(path)) val entity = StringEntity(strRequest(request)) httpPut.entity = entity httpPut.setHeaders(headers) return performCallAndExtractTypedWebClientResponse(httpPut, responseType) } fun patch(path: String, request: Any, headers: Array, responseType: Class): WebClientResponse { val httpPatch = HttpPatch(host(path)) val entity = StringEntity(strRequest(request)) httpPatch.entity = entity httpPatch.setHeaders(headers) return performCallAndExtractTypedWebClientResponse(httpPatch, responseType) } /** * Perform the HTTP call and return HTTP status code and body. * @param httpUriRequest {@link HttpUriRequest} object * @return {@link WebClientResponse} object * http client may throw IOException and ClientProtocolException on error */ @Throws(IOException::class, ClientProtocolException::class) private fun performCallAndExtractTypedWebClientResponse( httpUriRequest: HttpUriRequest, responseType: Class): WebClientResponse { val httpResponse = httpClient().execute(httpUriRequest) val statusCode = httpResponse.statusLine.statusCode httpResponse.entity.content.use { val body = getResponse(it, responseType) return WebClientResponse(statusCode, body) } } suspend fun getNB(path: String): WebClientResponse { return getNB(path, null, String::class.java) } suspend fun getNB(path: String, additionalHeaders: Array?): WebClientResponse { return getNB(path, additionalHeaders, String::class.java) } suspend fun getNB(path: String, additionalHeaders: Array?, responseType: Class): WebClientResponse = withContext(Dispatchers.IO) { get(path, additionalHeaders!!, responseType) } suspend fun postNB(path: String, request: Any): WebClientResponse { return postNB(path, request, null, String::class.java) } suspend fun postNB(path: String, request: Any, additionalHeaders: Array?): WebClientResponse { return postNB(path, request, additionalHeaders, String::class.java) } suspend fun postNB(path: String, request: Any, additionalHeaders: Array?, responseType: Class): WebClientResponse = withContext(Dispatchers.IO) { post(path, request, additionalHeaders!!, responseType) } suspend fun putNB(path: String, request: Any): WebClientResponse { return putNB(path, request, null, String::class.java) } suspend fun putNB(path: String, request: Any, additionalHeaders: Array?): WebClientResponse { return putNB(path, request, additionalHeaders, String::class.java) } suspend fun putNB(path: String, request: Any, additionalHeaders: Array?, responseType: Class): WebClientResponse = withContext(Dispatchers.IO) { put(path, request, additionalHeaders!!, responseType) } suspend fun deleteNB(path: String): WebClientResponse { return deleteNB(path, null, String::class.java) } suspend fun deleteNB(path: String, additionalHeaders: Array?): WebClientResponse { return deleteNB(path, additionalHeaders, String::class.java) } suspend fun deleteNB(path: String, additionalHeaders: Array?, responseType: Class): WebClientResponse = withContext(Dispatchers.IO) { delete(path, additionalHeaders!!, responseType) } suspend fun patchNB(path: String, request: Any, additionalHeaders: Array?, responseType: Class): WebClientResponse = withContext(Dispatchers.IO) { patch(path, request, additionalHeaders!!, responseType) } suspend fun exchangeNB(methodType: String, path: String, request: Any): WebClientResponse { return exchangeNB(methodType, path, request, hashMapOf(), String::class.java) } suspend fun exchangeNB(methodType: String, path: String, request: Any, additionalHeaders: Map?): WebClientResponse { return exchangeNB(methodType, path, request, additionalHeaders, String::class.java) } suspend fun exchangeNB(methodType: String, path: String, request: Any, additionalHeaders: Map?, responseType: Class): WebClientResponse { //TODO: possible inconsistency //NOTE: this basic headers function is different from non-blocking val convertedHeaders: Array = basicHeaders(additionalHeaders!!) return when (HttpMethod.resolve(methodType)) { HttpMethod.GET -> getNB(path, convertedHeaders, responseType) HttpMethod.POST -> postNB(path, request, convertedHeaders, responseType) HttpMethod.DELETE -> deleteNB(path, convertedHeaders, responseType) HttpMethod.PUT -> putNB(path, request, convertedHeaders, responseType) HttpMethod.PATCH -> patchNB(path, request, convertedHeaders, responseType) else -> throw BluePrintProcessorException("Unsupported methodType($methodType)") } } private fun strRequest(request: Any): String { return when (request) { is String -> request.toString() is JsonNode -> request.toString() else -> JacksonUtils.getJson(request) } } private fun getResponse(it: InputStream, responseType: Class): T { return if (responseType == String::class.java) { IOUtils.toString(it, Charset.defaultCharset()) as T } else { JacksonUtils.readValue(it, responseType)!! } } private fun basicHeaders(headers: Map?): Array { val basicHeaders = mutableListOf() defaultHeaders().forEach { (name, value) -> basicHeaders.add(BasicHeader(name, value)) } headers?.forEach { name, value -> basicHeaders.add(BasicHeader(name, value)) } return basicHeaders.toTypedArray() } // Non Blocking Rest Implementation suspend fun httpClientNB(): CloseableHttpClient { return HttpClients.custom() .addInterceptorFirst(WebClientUtils.logRequest()) .addInterceptorLast(WebClientUtils.logResponse()) .build() } //TODO maybe there could be cases where we care about return headers? data class WebClientResponse(val status: Int, val body: T) fun verifyAdditionalHeaders(restClientProperties: RestClientProperties): Map { val customHeaders: MutableMap = mutableMapOf() //Extract additionalHeaders from the requestProperties and //throw an error if HttpHeaders.AUTHORIZATION key (headers are case-insensitive) restClientProperties.additionalHeaders?.let { if (it.keys.map { k -> k.toLowerCase().trim() }.contains(HttpHeaders.AUTHORIZATION.toLowerCase())) { val errMsg = "Error in definition of endpoint ${restClientProperties.url}." + " User-supplied \"additionalHeaders\" cannot contain AUTHORIZATION header with" + " auth-type \"${RestLibConstants.TYPE_BASIC_AUTH}\"" WebClientUtils.log.error(errMsg) throw BluePrintProcessorException(errMsg) } else { customHeaders.putAll(it) } } return customHeaders } }