2 * Copyright © 2017-2019 AT&T, Bell Canada, Nordix Foundation
3 * Modifications Copyright © 2018-2019 IBM.
4 * Modifications Copyright © 2019 Huawei.
5 * Modifications Copyright © 2022 Deutsche Telekom AG.
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
20 package org.onap.ccsdk.cds.blueprintsprocessor.rest.service
22 import com.fasterxml.jackson.databind.JsonNode
23 import kotlinx.coroutines.Dispatchers
24 import kotlinx.coroutines.withContext
25 import org.apache.commons.io.IOUtils
26 import org.apache.http.HttpEntity
27 import org.apache.http.HttpHost
28 import org.apache.http.HttpResponse
29 import org.apache.http.HttpStatus
30 import org.apache.http.client.ClientProtocolException
31 import org.apache.http.client.config.RequestConfig
32 import org.apache.http.client.entity.EntityBuilder
33 import org.apache.http.client.methods.HttpDelete
34 import org.apache.http.client.methods.HttpGet
35 import org.apache.http.client.methods.HttpPatch
36 import org.apache.http.client.methods.HttpPost
37 import org.apache.http.client.methods.HttpPut
38 import org.apache.http.client.methods.HttpUriRequest
39 import org.apache.http.conn.ssl.NoopHostnameVerifier
40 import org.apache.http.conn.ssl.SSLContextBuilder
41 import org.apache.http.conn.ssl.TrustAllStrategy
42 import org.apache.http.entity.StringEntity
43 import org.apache.http.impl.client.CloseableHttpClient
44 import org.apache.http.impl.client.HttpClients
45 import org.apache.http.message.BasicHeader
46 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestClientProperties
47 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestLibConstants
48 import org.onap.ccsdk.cds.blueprintsprocessor.rest.service.BlueprintWebClientService.WebClientResponse
49 import org.onap.ccsdk.cds.blueprintsprocessor.rest.utils.WebClientUtils
50 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
51 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
52 import org.springframework.http.HttpHeaders
53 import org.springframework.http.HttpMethod
54 import java.io.IOException
55 import java.io.InputStream
57 import java.nio.charset.Charset
58 import java.nio.file.Files
59 import java.nio.file.Path
61 abstract class BaseBlueprintWebClientService<out E : RestClientProperties> : BlueprintWebClientService {
63 open fun host(uri: String): String {
64 val uri: URI = URI.create(getRestClientProperties().url + uri)
65 return uri.resolve(uri).toString()
68 abstract fun getRestClientProperties(): E
70 open fun getRequestConfig(): RequestConfig {
71 val requestConfigBuilder = RequestConfig.custom()
72 if (getRestClientProperties().connectionRequestTimeout > 0)
73 requestConfigBuilder.setConnectionRequestTimeout(getRestClientProperties().connectionRequestTimeout)
74 if (getRestClientProperties().connectTimeout > 0)
75 requestConfigBuilder.setConnectTimeout(getRestClientProperties().connectTimeout)
76 if (getRestClientProperties().socketTimeout > 0)
77 requestConfigBuilder.setSocketTimeout(getRestClientProperties().socketTimeout)
78 return requestConfigBuilder.build()
81 open fun https_proxy(): String? {
82 return getRestClientProperties().proxy
85 open fun httpClient(): CloseableHttpClient {
86 var httpClients = HttpClients.custom()
87 if (https_proxy() != null && https_proxy() != "") {
88 val proxyProtocol = https_proxy()?.split(':')?.get(0) ?: "http"
89 val proxyUri = https_proxy()?.split(':')?.get(1)?.replace("/", "") ?: ""
90 val proxyPort = https_proxy()?.split(':')?.get(2)?.toInt() ?: 0
91 if (proxyUri != "" && proxyPort != 0) {
92 val proxy = HttpHost(proxyUri, proxyPort, proxyProtocol)
93 httpClients = httpClients.setProxy(proxy)
94 .setSSLContext(SSLContextBuilder().loadTrustMaterial(null, TrustAllStrategy.INSTANCE).build())
95 .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
99 .addInterceptorFirst(WebClientUtils.logRequest())
100 .addInterceptorLast(WebClientUtils.logResponse())
101 .setDefaultRequestConfig(getRequestConfig())
105 override fun exchangeResource(methodType: String, path: String, request: String): WebClientResponse<String> {
106 return this.exchangeResource(methodType, path, request, defaultHeaders())
109 override fun exchangeResource(
113 headers: Map<String, String>
114 ): WebClientResponse<String> {
116 * TODO: Basic headers in the implementations of this client do not get added
117 * in blocking version, whereas in NB version defaultHeaders get added.
118 * the difference is in convertToBasicHeaders vs basicHeaders
120 val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(headers)
121 return when (HttpMethod.resolve(methodType)) {
122 HttpMethod.DELETE -> delete(path, convertedHeaders, String::class.java)
123 HttpMethod.GET -> get(path, convertedHeaders, String::class.java)
124 HttpMethod.POST -> post(path, request, convertedHeaders, String::class.java)
125 HttpMethod.PUT -> put(path, request, convertedHeaders, String::class.java)
126 HttpMethod.PATCH -> patch(path, request, convertedHeaders, String::class.java)
127 else -> throw BluePrintProcessorException(
128 "Unsupported methodType($methodType) attempted on path($path)"
133 @Throws(IOException::class, ClientProtocolException::class)
134 protected fun performHttpCall(httpUriRequest: HttpUriRequest): WebClientResponse<String> {
135 val httpResponse = httpClient().execute(httpUriRequest)
136 val statusCode = httpResponse.statusLine.statusCode
137 httpResponse.entity.content.use {
138 val body = IOUtils.toString(it, Charset.defaultCharset())
139 return WebClientResponse(statusCode, body)
143 open override fun uploadBinaryFile(path: String, filePath: Path): WebClientResponse<String> {
144 val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(defaultHeaders())
145 val httpPost = HttpPost(host(path))
146 val entity = EntityBuilder.create().setBinary(Files.readAllBytes(filePath)).build()
147 httpPost.setEntity(entity)
148 RestLoggerService.httpInvoking(convertedHeaders)
149 httpPost.setHeaders(convertedHeaders)
150 return performHttpCall(httpPost)
153 // TODO: convert to multi-map
154 override fun convertToBasicHeaders(headers: Map<String, String>): Array<BasicHeader> {
155 return headers.map { BasicHeader(it.key, it.value) }.toTypedArray()
158 open fun <T> delete(path: String, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
159 val httpDelete = HttpDelete(host(path))
160 RestLoggerService.httpInvoking(headers)
161 httpDelete.setHeaders(headers)
162 return performCallAndExtractTypedWebClientResponse(httpDelete, responseType)
165 open fun <T> get(path: String, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
166 val httpGet = HttpGet(host(path))
167 RestLoggerService.httpInvoking(headers)
168 httpGet.setHeaders(headers)
169 return performCallAndExtractTypedWebClientResponse(httpGet, responseType)
172 open fun <T> post(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
173 val httpPost = HttpPost(host(path))
174 val entity = StringEntity(strRequest(request))
175 httpPost.entity = entity
176 RestLoggerService.httpInvoking(headers)
177 httpPost.setHeaders(headers)
178 return performCallAndExtractTypedWebClientResponse(httpPost, responseType)
181 open fun <T> put(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
182 val httpPut = HttpPut(host(path))
183 val entity = StringEntity(strRequest(request))
184 httpPut.entity = entity
185 RestLoggerService.httpInvoking(headers)
186 httpPut.setHeaders(headers)
187 return performCallAndExtractTypedWebClientResponse(httpPut, responseType)
190 open fun <T> patch(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): WebClientResponse<T> {
191 val httpPatch = HttpPatch(host(path))
192 val entity = StringEntity(strRequest(request))
193 httpPatch.entity = entity
194 RestLoggerService.httpInvoking(headers)
195 httpPatch.setHeaders(headers)
196 return performCallAndExtractTypedWebClientResponse(httpPatch, responseType)
200 * Perform the HTTP call and return HTTP status code and body.
201 * @param httpUriRequest {@link HttpUriRequest} object
202 * @return {@link WebClientResponse} object
203 * http client may throw IOException and ClientProtocolException on error
206 @Throws(IOException::class, ClientProtocolException::class)
207 protected fun <T> performCallAndExtractTypedWebClientResponse(
208 httpUriRequest: HttpUriRequest,
209 responseType: Class<T>
211 WebClientResponse<T> {
212 val httpResponse = httpClient().execute(httpUriRequest)
213 val statusCode = httpResponse.statusLine.statusCode
214 val entity: HttpEntity? = httpResponse.entity
215 if (canResponseHaveBody(httpResponse)) {
216 entity!!.content.use {
217 val body = getResponse(it, responseType)
218 return WebClientResponse(statusCode, body)
221 val constructor = responseType.getConstructor()
222 val body = constructor.newInstance()
223 return WebClientResponse(statusCode, body)
226 fun canResponseHaveBody(response: HttpResponse): Boolean {
227 val status = response.statusLine.statusCode
228 return response.entity !== null &&
229 status != HttpStatus.SC_NO_CONTENT &&
230 status != HttpStatus.SC_NOT_MODIFIED &&
231 status != HttpStatus.SC_RESET_CONTENT
234 open suspend fun getNB(path: String): WebClientResponse<String> {
235 return getNB(path, null, String::class.java)
238 open suspend fun getNB(path: String, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
239 return getNB(path, additionalHeaders, String::class.java)
242 open suspend fun <T> getNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
243 WebClientResponse<T> = withContext(Dispatchers.IO) {
244 get(path, additionalHeaders!!, responseType)
247 open suspend fun postNB(path: String, request: Any): WebClientResponse<String> {
248 return postNB(path, request, null, String::class.java)
251 open suspend fun postNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?): WebClientResponse<String> {
252 return postNB(path, request, additionalHeaders, String::class.java)
255 open suspend fun <T> postNB(
258 additionalHeaders: Array<BasicHeader>?,
259 responseType: Class<T>
260 ): WebClientResponse<T> = withContext(Dispatchers.IO) {
261 post(path, request, additionalHeaders!!, responseType)
264 open suspend fun putNB(path: String, request: Any): WebClientResponse<String> {
265 return putNB(path, request, null, String::class.java)
268 open suspend fun putNB(
271 additionalHeaders: Array<BasicHeader>?
272 ): WebClientResponse<String> {
273 return putNB(path, request, additionalHeaders, String::class.java)
276 open suspend fun <T> putNB(
279 additionalHeaders: Array<BasicHeader>?,
280 responseType: Class<T>
281 ): WebClientResponse<T> = withContext(Dispatchers.IO) {
282 put(path, request, additionalHeaders!!, responseType)
285 open suspend fun <T> deleteNB(path: String): WebClientResponse<String> {
286 return deleteNB(path, null, String::class.java)
289 open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?):
290 WebClientResponse<String> {
291 return deleteNB(path, additionalHeaders, String::class.java)
294 open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
295 WebClientResponse<T> = withContext(Dispatchers.IO) {
296 delete(path, additionalHeaders!!, responseType)
299 open suspend fun <T> patchNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
300 WebClientResponse<T> = withContext(Dispatchers.IO) {
301 patch(path, request, additionalHeaders!!, responseType)
304 override suspend fun exchangeNB(methodType: String, path: String, request: Any): WebClientResponse<String> {
306 methodType, path, request, hashMapOf(),
311 override suspend fun exchangeNB(methodType: String, path: String, request: Any, additionalHeaders: Map<String, String>?):
312 WebClientResponse<String> {
313 return exchangeNB(methodType, path, request, additionalHeaders, String::class.java)
316 override suspend fun <T> exchangeNB(
320 additionalHeaders: Map<String, String>?,
321 responseType: Class<T>
322 ): WebClientResponse<T> {
324 // TODO: possible inconsistency
325 // NOTE: this basic headers function is different from non-blocking
326 val convertedHeaders: Array<BasicHeader> = basicHeaders(additionalHeaders!!)
327 return when (HttpMethod.resolve(methodType)) {
328 HttpMethod.GET -> getNB(path, convertedHeaders, responseType)
329 HttpMethod.POST -> postNB(path, request, convertedHeaders, responseType)
330 HttpMethod.DELETE -> deleteNB(path, convertedHeaders, responseType)
331 HttpMethod.PUT -> putNB(path, request, convertedHeaders, responseType)
332 HttpMethod.PATCH -> patchNB(path, request, convertedHeaders, responseType)
333 else -> throw BluePrintProcessorException("Unsupported methodType($methodType)")
337 protected fun strRequest(request: Any): String {
338 return when (request) {
339 is String -> request.toString()
340 is JsonNode -> request.toString()
341 else -> JacksonUtils.getJson(request)
345 protected fun <T> getResponse(it: InputStream, responseType: Class<T>): T {
346 return if (responseType == String::class.java) {
347 IOUtils.toString(it, Charset.defaultCharset()) as T
349 JacksonUtils.readValue(it, responseType)!!
353 protected fun basicHeaders(headers: Map<String, String>?):
355 val basicHeaders = mutableListOf<BasicHeader>()
356 defaultHeaders().forEach { (name, value) ->
357 basicHeaders.add(BasicHeader(name, value))
359 headers?.forEach { name, value ->
360 basicHeaders.add(BasicHeader(name, value))
362 return basicHeaders.toTypedArray()
365 // Non Blocking Rest Implementation
366 suspend fun httpClientNB(): CloseableHttpClient {
370 open fun verifyAdditionalHeaders(): Map<String, String> {
371 return verifyAdditionalHeaders(getRestClientProperties())
374 open fun verifyAdditionalHeaders(restClientProperties: RestClientProperties): Map<String, String> {
375 val customHeaders: MutableMap<String, String> = mutableMapOf()
376 // Extract additionalHeaders from the requestProperties and
377 // throw an error if HttpHeaders.AUTHORIZATION key (headers are case-insensitive)
378 restClientProperties.additionalHeaders?.let {
379 if (it.keys.map { k -> k.toLowerCase().trim() }.contains(HttpHeaders.AUTHORIZATION.toLowerCase())) {
380 val errMsg = "Error in definition of endpoint ${restClientProperties.url}." +
381 " User-supplied \"additionalHeaders\" cannot contain AUTHORIZATION header with" +
382 " auth-type \"${RestLibConstants.TYPE_BASIC_AUTH}\""
383 WebClientUtils.log.error(errMsg)
384 throw BluePrintProcessorException(errMsg)
386 customHeaders.putAll(it)