diff options
author | NikhilMohan <nikmohan81@gmail.com> | 2020-08-26 20:30:48 +0530 |
---|---|---|
committer | nikhil mohan <nikmohan81@gmail.com> | 2020-08-26 20:34:02 +0530 |
commit | 93fabeb929bb1b422d7473e23935f47fead4f4d8 (patch) | |
tree | f81cd4b669efda82154362c95beaba4b977fbd5f | |
parent | b337097739ba195b5d7a8cbb71c2f0e7f0e74d30 (diff) |
Implementation for TMF 633 API - POST /serviceSpecification
Issue-ID: EXTAPI-488
Signed-off-by: nikhil mohan <nikmohan81@gmail.com>
Change-Id: I382e3b2bfbde656508bcfb0c86e748f21a7790d2
8 files changed, 517 insertions, 23 deletions
diff --git a/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java b/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java index e362903..29a4a25 100644 --- a/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java +++ b/src/main/java/org/onap/nbi/apis/servicecatalog/SdcClient.java @@ -22,9 +22,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; import javax.annotation.PostConstruct; import org.apache.commons.io.IOUtils; @@ -42,6 +40,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -66,6 +65,8 @@ public class SdcClient { private static final String HEADER_ECOMP_INSTANCE_ID = "x-ecomp-instanceid"; private static final String HEADER_AUTHORIZATION = "Authorization"; + // changes for Post Implementation + private static final String USER_ID = "USER_ID"; private static final Logger LOGGER = LoggerFactory.getLogger(SdcClient.class); @@ -156,6 +157,19 @@ public class SdcClient { return createTmpFile(inputStream); } + /** + * + * @param serviceCatalogObject + * @param userId + */ + public Map callPost(HashMap<Object, Object> serviceCatalogObject, String userId) { + // post url is the same as find url + UriComponentsBuilder callURI = UriComponentsBuilder.fromHttpUrl(sdcFindUrl); + ResponseEntity<Object> response = callSdcForPost(callURI.build().encode().toUri(), serviceCatalogObject, + userId); + // return (List<LinkedHashMap>) response.getBody(); + return (LinkedHashMap) response.getBody(); + } private Path createTmpFile(InputStream csarInputStream) throws IOException { Path csarFile = Files.createTempFile("csar", ".zip"); @@ -208,4 +222,46 @@ public class SdcClient { response.getBody().toString()); } } + + //changes for Post implementation start + /** + * + * @param callURI + * @param obj + * @param userId + * @return + */ + private ResponseEntity<Object> callSdcForPost(URI callURI, Object obj, String userId) { + ResponseEntity<Object> response = restTemplate.exchange(callURI, HttpMethod.POST, + new HttpEntity<>(obj, buildRequestHeaderForPost(userId)), Object.class); + + if (null == response) { + return null; + } else { + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("response body : {} ", response.getBody().toString()); + } + LOGGER.info("response status : {}", response.getStatusCodeValue()); + return response; + } + + } + /** + * + * @param userId + * @return + */ + private HttpHeaders buildRequestHeaderForPost(String userId) { + HttpHeaders httpHeaders = new HttpHeaders(); + + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + httpHeaders.add(HEADER_ECOMP_INSTANCE_ID, ecompInstanceId); + httpHeaders.add(HEADER_AUTHORIZATION, sdcHeaderAuthorization); + httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + httpHeaders.add(USER_ID, userId); + + return httpHeaders; + } + } diff --git a/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationResource.java b/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationResource.java index 128bc6c..a0c27e7 100644 --- a/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationResource.java +++ b/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationResource.java @@ -16,22 +16,26 @@ package org.onap.nbi.apis.servicecatalog; +import java.net.URI; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.onap.nbi.OnapComponentsUrlPaths; +import org.onap.nbi.apis.servicecatalog.model.ServiceSpecificationRequest; import org.onap.nbi.commons.JsonRepresentation; import org.onap.nbi.commons.ResourceManagement; +import org.onap.nbi.exceptions.ValidationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.MultiValueMap; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.validation.Valid; @RestController @RequestMapping(OnapComponentsUrlPaths.SERVICE_SPECIFICATION_PATH) @@ -42,17 +46,17 @@ public class ServiceSpecificationResource extends ResourceManagement { @GetMapping(value = "/{serviceSpecId}", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Object> getServiceSpecification(@PathVariable String serviceSpecId, - @RequestParam MultiValueMap<String, String> params) { + @RequestParam MultiValueMap<String, String> params) { Map response = serviceSpecificationService.get(serviceSpecId); if (response != null) { - ArrayList<Map<String, Object>> resourseSpecificationMap= (ArrayList<Map<String, Object>>) response.get("resourceSpecification"); - for (Map<String, Object> map : resourseSpecificationMap) { - map.remove("childResourceSpecification"); - map.remove("serviceInstanceParams"); - map.remove("InstanceSpecification"); - } - response.put("resourceSpecification", resourseSpecificationMap); + ArrayList<Map<String, Object>> resourseSpecificationMap= (ArrayList<Map<String, Object>>) response.get("resourceSpecification"); + for (Map<String, Object> map : resourseSpecificationMap) { + map.remove("childResourceSpecification"); + map.remove("serviceInstanceParams"); + map.remove("InstanceSpecification"); + } + response.put("resourceSpecification", resourseSpecificationMap); } JsonRepresentation filter = new JsonRepresentation(params); @@ -73,10 +77,35 @@ public class ServiceSpecificationResource extends ResourceManagement { @GetMapping(value = "/{serviceSpecId}/specificationInputSchema", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Object> findSpecificationInputSchema(@PathVariable String serviceSpecId, - @RequestParam MultiValueMap<String, String> params) { + @RequestParam MultiValueMap<String, String> params) { String response = serviceSpecificationService.getInputSchema(serviceSpecId); JsonRepresentation filter = new JsonRepresentation(params); return this.getResponse(response, filter); } + @PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public Object createServiceSpecification(@RequestHeader(value = "USER_ID", required = true) String userId, + @Valid @RequestBody ServiceSpecificationRequest serviceSpecificationRequest, BindingResult result) { + if (null == userId || userId.isEmpty()) { + result.addError(new ObjectError("USER_ID", "USER_ID is missing in header!")); + } + if (result.hasErrors()) { + throw new ValidationException(result.getAllErrors()); + } + Map serviceCatalogResponse = serviceSpecificationService.create(userId, serviceSpecificationRequest); + + return createResponse(serviceCatalogResponse); + } + /** + * + * @param resource + * @return + */ + private ResponseEntity<Object> createResponse(final Map resource) { + URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(resource.get("id")) + .toUri(); + return ResponseEntity.created(location).body(resource); + + } + }
\ No newline at end of file diff --git a/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationService.java b/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationService.java index 9fc0ec0..45a4f3f 100644 --- a/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationService.java +++ b/src/main/java/org/onap/nbi/apis/servicecatalog/ServiceSpecificationService.java @@ -17,13 +17,15 @@ package org.onap.nbi.apis.servicecatalog; import java.io.File; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; + +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.FileUtils; import org.onap.nbi.apis.servicecatalog.jolt.FindServiceSpecJsonTransformer; import org.onap.nbi.apis.servicecatalog.jolt.GetServiceSpecJsonTransformer; +import org.onap.nbi.apis.servicecatalog.jolt.PostServiceResponseSpecJsonTransformer; +import org.onap.nbi.apis.servicecatalog.jolt.PostServiceSpecJsonTransformer; +import org.onap.nbi.apis.servicecatalog.model.ServiceSpecificationRequest; import org.onap.nbi.apis.serviceorder.ServiceCatalogUrl; import org.onap.sdc.tosca.parser.exceptions.SdcToscaParserException; import org.slf4j.Logger; @@ -45,6 +47,13 @@ public class ServiceSpecificationService { @Autowired FindServiceSpecJsonTransformer findServiceSpecJsonTransformer; + // Change for processing POST request + @Autowired + PostServiceSpecJsonTransformer postServiceSpecJsonTransformer; + + @Autowired + PostServiceResponseSpecJsonTransformer postServiceResponseSpecJsonTransformer ; + @Autowired ToscaInfosProcessor toscaInfosProcessor; @@ -101,4 +110,21 @@ public class ServiceSpecificationService { return null; } } + + public Map create(String userId, ServiceSpecificationRequest specRequest) { + ObjectMapper mapper = new ObjectMapper(); + LinkedHashMap specRequestMap = mapper.convertValue(specRequest, LinkedHashMap.class); + HashMap<Object, Object> serviceCatalogInput = (HashMap) postServiceSpecJsonTransformer.transform(specRequestMap); + + //Call SDC Post API + Map sdcResponse = sdcClient.callPost(serviceCatalogInput,userId); + LOGGER.info("SDC response " + sdcResponse); + //Transform SDC Response + LinkedHashMap<Object,Object> serviceCatalogResponse =null; + if (!CollectionUtils.isEmpty(sdcResponse)) { + serviceCatalogResponse = (LinkedHashMap)postServiceResponseSpecJsonTransformer.transform(sdcResponse); + } + return serviceCatalogResponse; + } + } diff --git a/src/main/java/org/onap/nbi/exceptions/ApiExceptionHandler.java b/src/main/java/org/onap/nbi/exceptions/ApiExceptionHandler.java index c68e6d3..1fe6a8c 100644 --- a/src/main/java/org/onap/nbi/exceptions/ApiExceptionHandler.java +++ b/src/main/java/org/onap/nbi/exceptions/ApiExceptionHandler.java @@ -16,6 +16,7 @@ package org.onap.nbi.exceptions; +import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -56,4 +57,11 @@ public class ApiExceptionHandler { ApiError apiError = new ApiError("400", HttpStatus.BAD_REQUEST.getReasonPhrase(), exception.getMessages(), ""); return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(JsonProcessingException.class) + @ResponseBody + public ResponseEntity<ApiError> validationExceptionHandler(final JsonProcessingException exception) { + ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST.name(), HttpStatus.BAD_REQUEST.getReasonPhrase(), "Request data is invalid!", ""); + return new ResponseEntity<>(apiError, HttpStatus.BAD_REQUEST); + } } diff --git a/src/main/java/org/onap/nbi/exceptions/BackendErrorHandler.java b/src/main/java/org/onap/nbi/exceptions/BackendErrorHandler.java index 48bf630..488ebd7 100644 --- a/src/main/java/org/onap/nbi/exceptions/BackendErrorHandler.java +++ b/src/main/java/org/onap/nbi/exceptions/BackendErrorHandler.java @@ -19,6 +19,8 @@ package org.onap.nbi.exceptions; import java.io.IOException; import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.ResponseErrorHandler; @@ -26,7 +28,7 @@ import org.springframework.web.client.ResponseErrorHandler; public class BackendErrorHandler implements ResponseErrorHandler { private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler(); - + private static final Logger LOGGER = LoggerFactory.getLogger(BackendErrorHandler.class); @Override public boolean hasError(ClientHttpResponse response) throws IOException { return errorHandler.hasError(response); @@ -38,6 +40,7 @@ public class BackendErrorHandler implements ResponseErrorHandler { String body = null; if (response.getBody() != null) { body = IOUtils.toString(response.getBody(), StandardCharsets.UTF_8.name()); + LOGGER.error("BackendErrorHandler {} error : {}", response.getBody()); } throw new BackendFunctionalException(response.getStatusCode(), response.getStatusText(), body); diff --git a/src/test/resources/karatetest/data/serviceSpecification.json b/src/test/resources/karatetest/data/serviceSpecification.json new file mode 100644 index 0000000..858aa11 --- /dev/null +++ b/src/test/resources/karatetest/data/serviceSpecification.json @@ -0,0 +1,195 @@ +[ + { + "name": "partner-service", + "description": "A test service through TMF API 633", + "@type": "ServiceSpecification", + "@schemaLocation": null, + "@baseType": "Service", + "toscaModelURL": "/", + "toscaResourceName": "", + "category": "Partner Domain Service", + "subcategory": "subservice", + "version": "1.0", + "lifecycleStatus": "NOT_CERTIFIED_CHECKOUT", + "DistributionStatus": "DISTRIBUTION_NOT_APPROVED", + "targetServiceSchema": { + "@type": "string", + "@schemaLocation": "/" + }, + "attachment": [{ + "id" : "1eadef", + "name" : "info-artifact", + "description" : "informational", + "artifactLabel" : "notes", + "artifactGroupType" : "info", + "artifactTimeout" : "500", + "artifactChecksum" : "adef23", + "artifactVersion" : "1.0", + "generatedFromUUID" : "a12e", + "url" : "/artifact/info/1eadef", + "mimeType" : "text", + "@type" : "ONAPartifact" + } + + ], + "relatedParty": [ + { + "id": "cs0008", + "role": "designer", + "name": "Carlos Santana" + } + ], + "resourceSpecification": [ + { + "id" : "281abc4d", + "version" : "1.0", + "name" : "vfirewall", + "resourceInstanceName" : "vfirewallinst", + "modelCustomizationName" : "vfirewallinst", + "resourceInvariantUUID" : "123ebdf", + "resourceType" : "VNF", + "@type" : "ONAPresource" + } + ], + "serviceSpecCharacteristic": [ + { + "name": "firewall", + "description": "Firewall characteristic", + "valueType": "string", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "string", + "isDefault": true, + "value": "NA" + } + ] + }, + { + "name": "isBundled", + "description": "is bundled or not", + "valueType": "boolean", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "boolean", + "isDefault": true, + "value": true + } + ] + }, + { + "name": "NumberofPort", + "description": "NumberofPorts", + "valueType": "integer", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "string", + "isDefault": false, + "value": "10" + } + ] + }, + { + "name": "ipaddress", + "description": "ipaddress", + "valueType": "float", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "string", + "isDefault": false, + "value": "15.123.9.101" + } + ] + } + ] + }, + { + "@type": "ServiceSpecification", + "@schemaLocation": null, + "@baseType": "Service", + "toscaModelURL": "/", + "toscaResourceName": "", + "subcategory": "subservice", + "version": "1.0", + "lifecycleStatus": "NOT_CERTIFIED_CHECKOUT", + "targetServiceSchema": { + "@type": "string", + "@schemaLocation": "/" + }, + "attachment": [], + "resourceSpecification": [], + "serviceSpecCharacteristic": [ + { + "name": "firewall", + "description": "Firewall characteristic", + "valueType": "string", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "string", + "isDefault": true, + "value": "NA" + } + ] + }, + { + "name": "isBundled", + "description": "is bundled or not", + "valueType": "boolean", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "boolean", + "isDefault": true, + "value": true + } + ] + }, + { + "name": "NumberofPorts", + "description": "Number of Ports", + "valueType": "integer", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "string", + "isDefault": false, + "value": "10" + } + ] + }, + { + "name": "ipaddress", + "description": "ipaddress", + "valueType": "string", + "@type": "string", + "@schemaLocation": "string", + "required": true, + "serviceSpecCharacteristicValue": [ + { + "valueType": "string", + "isDefault": true, + "value": "10.244.34.1" + } + ] + } + ] + } +]
\ No newline at end of file diff --git a/src/test/resources/karatetest/features/00--ServiceCatalog.feature b/src/test/resources/karatetest/features/00--ServiceCatalog.feature index 2a4a5c0..1887498 100644 --- a/src/test/resources/karatetest/features/00--ServiceCatalog.feature +++ b/src/test/resources/karatetest/features/00--ServiceCatalog.feature @@ -20,9 +20,10 @@ Feature: Service Catalog Background: * url nbiBaseUrl * def Context = Java.type('org.onap.nbi.test.Context'); +* def data = read('../data/serviceSpecification.json') * configure readTimeout = 30000 * call Context.startServers(); - + Scenario: testServiceCatalogGetResourceWithoutTosca Given path 'serviceSpecification','1e3feeb0-8e36-46c6-862c-236d9c626439_withoutTosca' When method get @@ -127,4 +128,74 @@ When method get Then status 500 * call Context.startServers(); +Scenario: testCreateServiceSpec +Given path 'serviceSpecification' +And header USER_ID = 'cs0008' +And request data[0] +When method post +Then status 201 +And match $.id contains '#notnull' +And match $.lifecycleStatus == 'NOT_CERTIFIED_CHECKOUT' +And match $.serviceSpecCharacteristic == +""" +[ { + "name" : "isBundle", + "description" : "is bundled or not", + "valueType" : "boolean", + "required" : true, + "serviceSpecCharacteristicValue" : [ { + "value" : "true", + "isDefault" : true + } ] + }, { + "name" : "ipaddress", + "description" : "ipaddress", + "valueType" : "string", + "required" : true, + "serviceSpecCharacteristicValue" : [ { + "value" : "10.244.34.1", + "isDefault" : true + } ] + }, { + "name" : "firewall", + "description" : "Firewall characteristic", + "valueType" : "string", + "required" : true, + "serviceSpecCharacteristicValue" : [ { + "value" : "NA", + "isDefault" : true + } ] + }, { + "name" : "NumberofPorts", + "description" : "Number of Ports", + "valueType" : "integer", + "required" : true, + "serviceSpecCharacteristicValue" : [ { + "value" : "10", + "isDefault" : true + } ] + } ] +""" + +Scenario: testCreateServiceSpecWithoutUser +Given path 'serviceSpecification' +And request data[0] +When method post +Then status 400 +And match $.message contains "Missing request header 'USER_ID'" + +Scenario: testCreateServiceSpecWithoutPayload +Given path 'serviceSpecification' +And header USER_ID = 'cs0008' +And request {} +When method post +Then status 400 + +Scenario: testCreateServiceSpecWithoutMandatoryDetails +Given path 'serviceSpecification' +And header USER_ID = 'cs0008' +And request data[1] +When method post +Then status 400 +And match $.message contains 'Bad Request' diff --git a/src/test/resources/mappings/sdc/sdc_post_spec.json b/src/test/resources/mappings/sdc/sdc_post_spec.json new file mode 100644 index 0000000..41e724f --- /dev/null +++ b/src/test/resources/mappings/sdc/sdc_post_spec.json @@ -0,0 +1,106 @@ +{ + "request": { + "method": "POST", + "url": "/sdc/v1/catalog/services", + "headers": { + "USER_ID": { + "contains": "cs0008" + } + }, + "bodyPatterns": [ + { + "contains": "\"name\":\"partner-service\"" + } + ] + }, + "response": { + "status": 201, + "jsonBody": { + "uuid": "1e3feeb0-8e36-46c6-862c-236d9c626439", + "href": "/serviceSpecification/1e3feeb0-8e36-46c6-862c-236d9c626439", + "name": "partner-service", + "description": "service", + "@type": "ServiceSpecification", + "@schemaLocation": null, + "@baseType": "Service", + "toscaModelURL": "/", + "toscaResourceName": "string", + "category": "Partner Domain Service", + "subcategory": "string", + "version": "1.0", + "lifecycleState": "NOT_CERTIFIED_CHECKOUT", + "targetServiceSchema": { + "@type": "string", + "@schemaLocation": "/" + }, + "artifacts": [ + { + "artifactUUID": "1eadef", + "artifactName": "info-artifact", + "artifactDescription": "informational", + "artifactLabel": "notes", + "artifactGroupType": "info", + "artifactTimeout": "500", + "artifactChecksum": "adef23", + "artifactVersion": "1.0", + "generatedFromUUID": "a12e", + "artifactURL": "/artifact/info/1eadef", + "artifactType": "text" + } + ], + "lastUpdaterUserId": "cs0008", + "lastUpdaterFullName": "Carlos Santana", + "resources": [ + { + "resourceUUID": "281abc4d", + "resourceVersion": "1.0", + "resourceName": "vfirewall", + "resourceInstanceName": "vfirewallinst", + "resourceInvariantUUID": "123ebdf", + "resoucreType": "VNF" + } + ], + "properties": [ + { + "type": "boolean", + "required": true, + "definition": false, + "description": "is bundled or not", + "name": "isBundle", + "value": "true", + "defaultValue": true + }, + { + "type": "string", + "required": true, + "definition": false, + "description": "ipaddress", + "name": "ipaddress", + "value": "10.244.34.1", + "defaultValue": true + }, + { + "type": "string", + "required": true, + "definition": false, + "description": "Firewall characteristic", + "name": "firewall", + "value": "NA", + "defaultValue": true + }, + { + "type": "integer", + "required": true, + "definition": false, + "description": "Number of Ports", + "name": "NumberofPorts", + "value": "10", + "defaultValue": true + } + ] + }, + "headers": { + "Content-Type": "application/json" + } + } +}
\ No newline at end of file |