Http 204 response results with exception in rest resolution
[ccsdk/cds.git] / ms / blueprintsprocessor / modules / commons / rest-lib / src / main / kotlin / org / onap / ccsdk / cds / blueprintsprocessor / rest / service / BaseBlueprintWebClientService.kt
1 /*
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.
6  *
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
10  *
11  *     http://www.apache.org/licenses/LICENSE-2.0
12  *
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.
18  */
19
20 package org.onap.ccsdk.cds.blueprintsprocessor.rest.service
21
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.HttpResponse
28 import org.apache.http.HttpStatus
29 import org.apache.http.client.ClientProtocolException
30 import org.apache.http.client.config.RequestConfig
31 import org.apache.http.client.methods.HttpDelete
32 import org.apache.http.client.methods.HttpGet
33 import org.apache.http.client.methods.HttpPatch
34 import org.apache.http.client.methods.HttpPost
35 import org.apache.http.client.methods.HttpPut
36 import org.apache.http.client.methods.HttpUriRequest
37 import org.apache.http.entity.StringEntity
38 import org.apache.http.impl.client.CloseableHttpClient
39 import org.apache.http.impl.client.HttpClients
40 import org.apache.http.message.BasicHeader
41 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestClientProperties
42 import org.onap.ccsdk.cds.blueprintsprocessor.rest.RestLibConstants
43 import org.onap.ccsdk.cds.blueprintsprocessor.rest.utils.WebClientUtils
44 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
45 import org.onap.ccsdk.cds.controllerblueprints.core.utils.JacksonUtils
46 import org.springframework.http.HttpHeaders
47 import org.springframework.http.HttpMethod
48 import java.io.IOException
49 import java.io.InputStream
50 import java.net.URI
51 import java.nio.charset.Charset
52
53 abstract class BaseBlueprintWebClientService<out E : RestClientProperties> : BlueprintWebClientService {
54
55     open fun host(uri: String): String {
56         val uri: URI = URI.create(getRestClientProperties().url + uri)
57         return uri.resolve(uri).toString()
58     }
59
60     abstract fun getRestClientProperties(): E
61
62     open fun getRequestConfig(): RequestConfig {
63         val requestConfigBuilder = RequestConfig.custom()
64         if (getRestClientProperties().connectionRequestTimeout > 0)
65             requestConfigBuilder.setConnectionRequestTimeout(getRestClientProperties().connectionRequestTimeout)
66         if (getRestClientProperties().connectTimeout > 0)
67             requestConfigBuilder.setConnectTimeout(getRestClientProperties().connectTimeout)
68         if (getRestClientProperties().socketTimeout > 0)
69             requestConfigBuilder.setSocketTimeout(getRestClientProperties().socketTimeout)
70         return requestConfigBuilder.build()
71     }
72
73     open fun httpClient(): CloseableHttpClient {
74         return HttpClients.custom()
75             .addInterceptorFirst(WebClientUtils.logRequest())
76             .addInterceptorLast(WebClientUtils.logResponse())
77             .setDefaultRequestConfig(getRequestConfig())
78             .build()
79     }
80
81     override fun exchangeResource(methodType: String, path: String, request: String): BlueprintWebClientService.WebClientResponse<String> {
82         return this.exchangeResource(methodType, path, request, defaultHeaders())
83     }
84
85     override fun exchangeResource(
86         methodType: String,
87         path: String,
88         request: String,
89         headers: Map<String, String>
90     ): BlueprintWebClientService.WebClientResponse<String> {
91         /**
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
95          */
96         val convertedHeaders: Array<BasicHeader> = convertToBasicHeaders(headers)
97         return when (HttpMethod.resolve(methodType)) {
98             HttpMethod.DELETE -> delete(path, convertedHeaders, String::class.java)
99             HttpMethod.GET -> get(path, convertedHeaders, String::class.java)
100             HttpMethod.POST -> post(path, request, convertedHeaders, String::class.java)
101             HttpMethod.PUT -> put(path, request, convertedHeaders, String::class.java)
102             HttpMethod.PATCH -> patch(path, request, convertedHeaders, String::class.java)
103             else -> throw BluePrintProcessorException(
104                 "Unsupported methodType($methodType) attempted on path($path)"
105             )
106         }
107     }
108
109     // TODO: convert to multi-map
110     override fun convertToBasicHeaders(headers: Map<String, String>): Array<BasicHeader> {
111         return headers.map { BasicHeader(it.key, it.value) }.toTypedArray()
112     }
113
114     open fun <T> delete(path: String, headers: Array<BasicHeader>, responseType: Class<T>): BlueprintWebClientService.WebClientResponse<T> {
115         val httpDelete = HttpDelete(host(path))
116         RestLoggerService.httpInvoking(headers)
117         httpDelete.setHeaders(headers)
118         return performCallAndExtractTypedWebClientResponse(httpDelete, responseType)
119     }
120
121     open fun <T> get(path: String, headers: Array<BasicHeader>, responseType: Class<T>): BlueprintWebClientService.WebClientResponse<T> {
122         val httpGet = HttpGet(host(path))
123         RestLoggerService.httpInvoking(headers)
124         httpGet.setHeaders(headers)
125         return performCallAndExtractTypedWebClientResponse(httpGet, responseType)
126     }
127
128     open fun <T> post(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): BlueprintWebClientService.WebClientResponse<T> {
129         val httpPost = HttpPost(host(path))
130         val entity = StringEntity(strRequest(request))
131         httpPost.entity = entity
132         RestLoggerService.httpInvoking(headers)
133         httpPost.setHeaders(headers)
134         return performCallAndExtractTypedWebClientResponse(httpPost, responseType)
135     }
136
137     open fun <T> put(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): BlueprintWebClientService.WebClientResponse<T> {
138         val httpPut = HttpPut(host(path))
139         val entity = StringEntity(strRequest(request))
140         httpPut.entity = entity
141         RestLoggerService.httpInvoking(headers)
142         httpPut.setHeaders(headers)
143         return performCallAndExtractTypedWebClientResponse(httpPut, responseType)
144     }
145
146     open fun <T> patch(path: String, request: Any, headers: Array<BasicHeader>, responseType: Class<T>): BlueprintWebClientService.WebClientResponse<T> {
147         val httpPatch = HttpPatch(host(path))
148         val entity = StringEntity(strRequest(request))
149         httpPatch.entity = entity
150         RestLoggerService.httpInvoking(headers)
151         httpPatch.setHeaders(headers)
152         return performCallAndExtractTypedWebClientResponse(httpPatch, responseType)
153     }
154
155     /**
156      * Perform the HTTP call and return HTTP status code and body.
157      * @param httpUriRequest {@link HttpUriRequest} object
158      * @return {@link WebClientResponse} object
159      * http client may throw IOException and ClientProtocolException on error
160      */
161
162     @Throws(IOException::class, ClientProtocolException::class)
163     protected fun <T> performCallAndExtractTypedWebClientResponse(
164         httpUriRequest: HttpUriRequest,
165         responseType: Class<T>
166     ):
167         BlueprintWebClientService.WebClientResponse<T> {
168             val httpResponse = httpClient().execute(httpUriRequest)
169             val statusCode = httpResponse.statusLine.statusCode
170             val entity: HttpEntity? = httpResponse.entity
171             if (canResponseHaveBody(httpResponse)) {
172                 entity!!.content.use {
173                     val body = getResponse(it, responseType)
174                     return BlueprintWebClientService.WebClientResponse(statusCode, body)
175                 }
176             } else {
177                 val constructor = responseType.getConstructor()
178                 val body = constructor.newInstance()
179                 return BlueprintWebClientService.WebClientResponse(statusCode, body)
180             }
181         }
182     fun canResponseHaveBody(response: HttpResponse): Boolean {
183         val status = response.statusLine.statusCode
184         return response.entity !== null &&
185             status != HttpStatus.SC_NO_CONTENT &&
186             status != HttpStatus.SC_NOT_MODIFIED &&
187             status != HttpStatus.SC_RESET_CONTENT
188     }
189
190     open suspend fun getNB(path: String): BlueprintWebClientService.WebClientResponse<String> {
191         return getNB(path, null, String::class.java)
192     }
193
194     open suspend fun getNB(path: String, additionalHeaders: Array<BasicHeader>?): BlueprintWebClientService.WebClientResponse<String> {
195         return getNB(path, additionalHeaders, String::class.java)
196     }
197
198     open suspend fun <T> getNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
199         BlueprintWebClientService.WebClientResponse<T> = withContext(Dispatchers.IO) {
200             get(path, additionalHeaders!!, responseType)
201         }
202
203     open suspend fun postNB(path: String, request: Any): BlueprintWebClientService.WebClientResponse<String> {
204         return postNB(path, request, null, String::class.java)
205     }
206
207     open suspend fun postNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?): BlueprintWebClientService.WebClientResponse<String> {
208         return postNB(path, request, additionalHeaders, String::class.java)
209     }
210
211     open suspend fun <T> postNB(
212         path: String,
213         request: Any,
214         additionalHeaders: Array<BasicHeader>?,
215         responseType: Class<T>
216     ): BlueprintWebClientService.WebClientResponse<T> = withContext(Dispatchers.IO) {
217         post(path, request, additionalHeaders!!, responseType)
218     }
219
220     open suspend fun putNB(path: String, request: Any): BlueprintWebClientService.WebClientResponse<String> {
221         return putNB(path, request, null, String::class.java)
222     }
223
224     open suspend fun putNB(
225         path: String,
226         request: Any,
227         additionalHeaders: Array<BasicHeader>?
228     ): BlueprintWebClientService.WebClientResponse<String> {
229         return putNB(path, request, additionalHeaders, String::class.java)
230     }
231
232     open suspend fun <T> putNB(
233         path: String,
234         request: Any,
235         additionalHeaders: Array<BasicHeader>?,
236         responseType: Class<T>
237     ): BlueprintWebClientService.WebClientResponse<T> = withContext(Dispatchers.IO) {
238         put(path, request, additionalHeaders!!, responseType)
239     }
240
241     open suspend fun <T> deleteNB(path: String): BlueprintWebClientService.WebClientResponse<String> {
242         return deleteNB(path, null, String::class.java)
243     }
244
245     open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?):
246         BlueprintWebClientService.WebClientResponse<String> {
247             return deleteNB(path, additionalHeaders, String::class.java)
248         }
249
250     open suspend fun <T> deleteNB(path: String, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
251         BlueprintWebClientService.WebClientResponse<T> = withContext(Dispatchers.IO) {
252             delete(path, additionalHeaders!!, responseType)
253         }
254
255     open suspend fun <T> patchNB(path: String, request: Any, additionalHeaders: Array<BasicHeader>?, responseType: Class<T>):
256         BlueprintWebClientService.WebClientResponse<T> = withContext(Dispatchers.IO) {
257             patch(path, request, additionalHeaders!!, responseType)
258         }
259
260     override suspend fun exchangeNB(methodType: String, path: String, request: Any): BlueprintWebClientService.WebClientResponse<String> {
261         return exchangeNB(
262             methodType, path, request, hashMapOf(),
263             String::class.java
264         )
265     }
266
267     override suspend fun exchangeNB(methodType: String, path: String, request: Any, additionalHeaders: Map<String, String>?):
268         BlueprintWebClientService.WebClientResponse<String> {
269             return exchangeNB(methodType, path, request, additionalHeaders, String::class.java)
270         }
271
272     override suspend fun <T> exchangeNB(
273         methodType: String,
274         path: String,
275         request: Any,
276         additionalHeaders: Map<String, String>?,
277         responseType: Class<T>
278     ): BlueprintWebClientService.WebClientResponse<T> {
279
280         // TODO: possible inconsistency
281         // NOTE: this basic headers function is different from non-blocking
282         val convertedHeaders: Array<BasicHeader> = basicHeaders(additionalHeaders!!)
283         return when (HttpMethod.resolve(methodType)) {
284             HttpMethod.GET -> getNB(path, convertedHeaders, responseType)
285             HttpMethod.POST -> postNB(path, request, convertedHeaders, responseType)
286             HttpMethod.DELETE -> deleteNB(path, convertedHeaders, responseType)
287             HttpMethod.PUT -> putNB(path, request, convertedHeaders, responseType)
288             HttpMethod.PATCH -> patchNB(path, request, convertedHeaders, responseType)
289             else -> throw BluePrintProcessorException("Unsupported methodType($methodType)")
290         }
291     }
292
293     protected fun strRequest(request: Any): String {
294         return when (request) {
295             is String -> request.toString()
296             is JsonNode -> request.toString()
297             else -> JacksonUtils.getJson(request)
298         }
299     }
300
301     protected fun <T> getResponse(it: InputStream, responseType: Class<T>): T {
302         return if (responseType == String::class.java) {
303             IOUtils.toString(it, Charset.defaultCharset()) as T
304         } else {
305             JacksonUtils.readValue(it, responseType)!!
306         }
307     }
308
309     protected fun basicHeaders(headers: Map<String, String>?):
310         Array<BasicHeader> {
311             val basicHeaders = mutableListOf<BasicHeader>()
312             defaultHeaders().forEach { (name, value) ->
313                 basicHeaders.add(BasicHeader(name, value))
314             }
315             headers?.forEach { name, value ->
316                 basicHeaders.add(BasicHeader(name, value))
317             }
318             return basicHeaders.toTypedArray()
319         }
320
321     // Non Blocking Rest Implementation
322     suspend fun httpClientNB(): CloseableHttpClient {
323         return httpClient()
324     }
325
326     open fun verifyAdditionalHeaders(): Map<String, String> {
327         return verifyAdditionalHeaders(getRestClientProperties())
328     }
329
330     open fun verifyAdditionalHeaders(restClientProperties: RestClientProperties): Map<String, String> {
331         val customHeaders: MutableMap<String, String> = mutableMapOf()
332         // Extract additionalHeaders from the requestProperties and
333         // throw an error if HttpHeaders.AUTHORIZATION key (headers are case-insensitive)
334         restClientProperties.additionalHeaders?.let {
335             if (it.keys.map { k -> k.toLowerCase().trim() }.contains(HttpHeaders.AUTHORIZATION.toLowerCase())) {
336                 val errMsg = "Error in definition of endpoint ${restClientProperties.url}." +
337                     " User-supplied \"additionalHeaders\" cannot contain AUTHORIZATION header with" +
338                     " auth-type \"${RestLibConstants.TYPE_BASIC_AUTH}\""
339                 WebClientUtils.log.error(errMsg)
340                 throw BluePrintProcessorException(errMsg)
341             } else {
342                 customHeaders.putAll(it)
343             }
344         }
345         return customHeaders
346     }
347 }