diff --git a/kraken-app/kraken-app-hub/src/test/java/com/consoleconnect/kraken/operator/ResponseMappingTest.java b/kraken-app/kraken-app-hub/src/test/java/com/consoleconnect/kraken/operator/ResponseMappingTest.java index aaad29511..0b1f0bd12 100644 --- a/kraken-app/kraken-app-hub/src/test/java/com/consoleconnect/kraken/operator/ResponseMappingTest.java +++ b/kraken-app/kraken-app-hub/src/test/java/com/consoleconnect/kraken/operator/ResponseMappingTest.java @@ -37,7 +37,7 @@ private void validate(String expected, String input) { StateValueMappingDto responseTargetMapperDto = new StateValueMappingDto(); for (ComponentAPITargetFacets.Endpoint endpoint : facets.getEndpoints()) { - String transformedResp = transform(endpoint, responseTargetMapperDto); + String transformedResp = transform(endpoint, responseTargetMapperDto, new HashMap<>()); log.info("expected resp:{}", expected); log.info("transformed resp:{}", transformedResp); Assertions.assertEquals(expected, transformedResp); diff --git a/kraken-app/kraken-app-hub/src/test/resources/mock/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml b/kraken-app/kraken-app-hub/src/test/resources/mock/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml index 10f42c95d..f8dc126d2 100644 --- a/kraken-app/kraken-app-hub/src/test/resources/mock/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml +++ b/kraken-app/kraken-app-hub/src/test/resources/mock/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml @@ -19,14 +19,15 @@ spec: mappers: request: - name: mapper.quote.uni.add.sync.duration.amount - source: "@{{quoteItem[0].requestedQuoteItemTerm.duration.amount}}" + source: "@{{quoteItem[*].requestedQuoteItemTerm.duration.amount}}" sourceLocation: BODY sourceType: constantNumber target: "1" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.duration.units - source: "@{{quoteItem[0].requestedQuoteItemTerm.duration.units}}" + source: "@{{quoteItem[*].requestedQuoteItemTerm.duration.units}}" sourceLocation: BODY sourceType: enum sourceValues: @@ -42,8 +43,9 @@ spec: target: "@{{ports[*].durationUnit}}" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.endOfTermAction - source: "@{{quoteItem[0].requestedQuoteItemTerm.endOfTermAction}}" + source: "@{{quoteItem[*].requestedQuoteItemTerm.endOfTermAction}}" sourceLocation: BODY sourceType: enum sourceValues: @@ -54,15 +56,17 @@ spec: target: "roll" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.rollInterval.amount - source: "@{{quoteItem[0].requestedQuoteItemTerm.rollInterval.amount}}" + source: "@{{quoteItem[*].requestedQuoteItemTerm.rollInterval.amount}}" sourceLocation: BODY sourceType: constantNumber target: "1" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.rollInterval.units - source: "@{{quoteItem[0].requestedQuoteItemTerm.rollInterval.units}}" + source: "@{{quoteItem[*].requestedQuoteItemTerm.rollInterval.units}}" sourceLocation: BODY sourceType: enum sourceValues: @@ -77,22 +81,24 @@ spec: target: "calendarMonths" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.place.id - source: "@{{quoteItem[0].product.place[0].id}}" + source: "@{{quoteItem[*].product.place[0].id}}" sourceLocation: BODY sourceType: discreteStr target: "@{{ports[*].dcf}}" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.place.keyName - source: "@{{quoteItem[0].product.place[0].keyName}}" + source: "@{{quoteItem[*].product.place[0].keyName}}" sourceLocation: BODY sourceType: discreteStr target: "" targetLocation: BODY requiredMapping: false - name: mapper.quote.uni.add.sync.productConfiguration.bandwidth - source: "@{{quoteItem[0].product.productConfiguration.bandwidth}}" + source: "@{{quoteItem[*].product.productConfiguration.bandwidth}}" sourceLocation: BODY sourceType: discreteInt sourceValues: @@ -101,8 +107,9 @@ spec: target: "@{{ports[*].speed}}" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.productConfiguration.bandwidthUnit - source: "@{{quoteItem[0].product.productConfiguration.bandwidthUnit}}" + source: "@{{quoteItem[*].product.productConfiguration.bandwidthUnit}}" sourceLocation: BODY sourceType: enum sourceValues: @@ -112,76 +119,85 @@ spec: target: "MBPS" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.productOffering.id - source: "@{{quoteItem[0].product.productOffering.id}}" + source: "@{{quoteItem[*].product.productOffering.id}}" sourceLocation: BODY sourceType: discreteStr target: "UNI" targetLocation: BODY requiredMapping: false + mappingType: array response: - name: mapper.quote.uni.add.sync.unitOfMeasure title: Quote unitOfMeasure Mapping description: quote unitOfMeasure mapping source: "Gb" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].unitOfMeasure}}" + target: "@{{quoteItem[*].quoteItemPrice[0].unitOfMeasure}}" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.price.unit title: Quote Price Unit Mapping description: quote price mapping source: "USD" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].price.dutyFreeAmount.unit}}" + target: "@{{quoteItem[*].quoteItemPrice[0].price.dutyFreeAmount.unit}}" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.price.value title: Quote Price Value Mapping description: quote price mapping source: "@{{responseBody.results[*].price}}" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].price.dutyFreeAmount.value}}" + target: "@{{quoteItem[*].quoteItemPrice[0].price.dutyFreeAmount.value}}" targetLocation: BODY requiredMapping: true + mappingType: array - name: mapper.quote.uni.add.sync.taxRate title: Quote taxRate Mapping description: quote taxRate mapping source: "16" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].price.taxRate}}" + target: "@{{quoteItem[*].quoteItemPrice[0].price.taxRate}}" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.taxIncludedAmount.unit title: Quote taxIncludedAmount unit Mapping description: quote taxIncludedAmount unit mapping source: "USD" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].price.taxIncludedAmount.unit}}" + target: "@{{quoteItem[*].quoteItemPrice[0].price.taxIncludedAmount.unit}}" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.taxIncludedAmount.value title: Quote taxIncludedAmount value Mapping description: quote taxIncludedAmount value mapping source: "100" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].price.taxIncludedAmount.value}}" + target: "@{{quoteItem[*].quoteItemPrice[0].price.taxIncludedAmount.value}}" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.quoteItemPrice.name title: Quote quoteItemPrice name Mapping description: quote quoteItemPrice name mapping source: "name-here" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].name}}" + target: "@{{quoteItem[*].quoteItemPrice[0].name}}" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.quoteItemPrice.priceType title: Quote quoteItemPrice priceType Mapping description: quote quoteItemPrice priceType mapping source: "recurring" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].priceType}}" + target: "@{{quoteItem[*].quoteItemPrice[0].priceType}}" targetType: enum targetLocation: BODY requiredMapping: false @@ -190,20 +206,22 @@ spec: - nonRecurring - usageBased valueMapping: {} + mappingType: array - name: mapper.quote.uni.add.sync.quoteItemPrice.description title: Quote quoteItemPrice description Mapping description: quote quoteItemPrice description mapping source: "" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].description}}" + target: "@{{quoteItem[*].quoteItemPrice[0].description}}" targetLocation: BODY requiredMapping: false + mappingType: array - name: mapper.quote.uni.add.sync.quoteItemPrice.recurringChargePeriod title: Quote quoteItemPrice recurringChargePeriod Mapping description: quote quoteItemPrice recurringChargePeriod mapping source: "month" sourceLocation: BODY - target: "@{{quoteItem[0].quoteItemPrice[*].recurringChargePeriod}}" + target: "@{{quoteItem[*].quoteItemPrice[0].recurringChargePeriod}}" targetType: enum targetLocation: BODY requiredMapping: false @@ -213,4 +231,5 @@ spec: - week - month - year - valueMapping: {} \ No newline at end of file + valueMapping: {} + mappingType: array \ No newline at end of file diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/facet/ComponentAPITargetFacets.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/facet/ComponentAPITargetFacets.java index 9d2a216d8..a7cf4f197 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/facet/ComponentAPITargetFacets.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/model/facet/ComponentAPITargetFacets.java @@ -60,6 +60,7 @@ public static class Mapper { private String deletePath; private Boolean customizedField = false; private String convertValue; + private String mappingType; @Override public int hashCode() { diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtil.java b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtil.java index 35b3ce4cf..7711652d5 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtil.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/main/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtil.java @@ -30,15 +30,19 @@ public static List extractParam(String param, String patternStr) { return contents; } - public static String convertToJsonPointer(String path) { + public static String convertPathToJsonPointer(String path) { + return convertPathToJsonPointer(path, "[0]"); + } + + public static String convertPathToJsonPointer(String path, String arrayReplacement) { List params = extractMapperParam(path); String param = params.get(0); if (StringUtils.isNotBlank(param) && param.startsWith(ARRAY_ROOT_PREFIX)) { - param = param.substring(ARRAY_ROOT_PREFIX.length(), param.length()); + param = param.substring(ARRAY_ROOT_PREFIX.length()); } return "/" + param - .replaceAll("\\[(\\*)\\]", "[0]") + .replaceAll("\\[(\\*)\\]", arrayReplacement) .replaceAll("(?)\\[", "\\/") .replaceAll("(?)\\].", "\\/") .replaceAll("(\\.)", "\\/"); @@ -64,6 +68,21 @@ public static String constructBody(String source) { return source.replace("@{{", "${body.").replace("}}", "}"); } + public static String constructBodyOfArray(String source, int indexOfArray) { + return constructBodyOfArray(source, indexOfArray, "${body."); + } + + public static String constructBodyOfArray(String source, int indexOfArray, String arrayPrefix) { + return source + .replace("@{{", arrayPrefix) + .replace("}}", "}") + .replace("[*]", "[" + indexOfArray + "]"); + } + + public static String constructJsonPath(String prefix, String source) { + return source.replace("@{{", prefix).replace("}}", ""); + } + public static String constructJsonPathBody(String source) { return source.replace("@{{", "$.body.").replace("}}", ""); } diff --git a/kraken-java-sdk/kraken-java-sdk-core/src/test/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtilTest.java b/kraken-java-sdk/kraken-java-sdk-core/src/test/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtilTest.java index 7e503aa06..a1742dd44 100644 --- a/kraken-java-sdk/kraken-java-sdk-core/src/test/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtilTest.java +++ b/kraken-java-sdk/kraken-java-sdk-core/src/test/java/com/consoleconnect/kraken/operator/core/toolkit/ConstructExpressionUtilTest.java @@ -28,7 +28,7 @@ void givenExpression_whenConvert_thenResponseOK() { assertThat(s3).contains("body"); List pathParam = ConstructExpressionUtil.extractOriginalPathParam("/{path}/a/b/c"); assertThat(pathParam).contains("path"); - String s4 = ConstructExpressionUtil.convertToJsonPointer("@{{a[*].b.c[1]}}"); + String s4 = ConstructExpressionUtil.convertPathToJsonPointer("@{{a[*].b.c[1]}}"); String s5 = ConstructExpressionUtil.constructOriginalDBParam("abc"); assertThat(s5).contains("entity"); assertThat(s4).doesNotContain("*"); @@ -42,7 +42,7 @@ void givenExpression_whenConvert_thenResponseOK() { @Test void givenStatusInArray_whenConvert_thenOK() { String target = "@{{[*].status}}"; - String result = ConstructExpressionUtil.convertToJsonPointer(target); + String result = ConstructExpressionUtil.convertPathToJsonPointer(target); String expected = "/status"; Assertions.assertEquals(expected, result); } diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/AbstractBodyTransformerFunc.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/AbstractBodyTransformerFunc.java index 404f6c62a..789a1ffba 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/AbstractBodyTransformerFunc.java +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/AbstractBodyTransformerFunc.java @@ -70,7 +70,7 @@ public Publisher transform(ServerWebExchange exchange, String s, boolean context.put("responseStatus", responseStatus); } String engine = (String) context.get(INPUT_ENGINE); - log.info("engine:{}", engine); + log.info("postRequest:{}, engine:{}", postRequest, engine); String retJsonString = null; if (ENGINE_JAVASCRIPT.equals(engine)) { String script = (String) context.get(SpelEngineActionRunner.INPUT_CODE); @@ -88,7 +88,7 @@ public Publisher transform(ServerWebExchange exchange, String s, boolean retJsonString = JsonToolkit.toJson(expression); } } - log.info("retJsonString:{}", retJsonString); + log.info("postRequest:{}, retJsonString:{}", postRequest, retJsonString); if (postRequest) { exchange.getAttributes().put(KrakenFilterConstants.X_KRAKEN_RESPONSE_BODY, s); diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/LoadTargetAPIConfigActionRunner.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/LoadTargetAPIConfigActionRunner.java index d1157d546..e43edaa95 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/LoadTargetAPIConfigActionRunner.java +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/LoadTargetAPIConfigActionRunner.java @@ -78,7 +78,8 @@ public Optional runIt( } StateValueMappingDto stateValueMappingDto = new StateValueMappingDto(); - renderRequestService.parseRequest(facets, stateValueMappingDto); + renderRequestService.handlePath(facets); + renderRequestService.handleBody(facets, stateValueMappingDto, inputs); if (render != null && render) { facets @@ -92,8 +93,9 @@ public Optional runIt( endpoint.setRequestBody(renderedRequest); } if (Objects.nonNull(endpoint.getResponseBody())) { - String transformedResp = transform(endpoint, stateValueMappingDto); + String transformedResp = transform(endpoint, stateValueMappingDto, inputs); endpoint.setResponseBody(SpELEngine.evaluate(transformedResp, inputs)); + log.info("After rendering, the response body is:{}", endpoint.getResponseBody()); } if (Objects.nonNull(endpoint.getPath())) { String evaluate = diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingMatrixCheckerActionRunner.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingMatrixCheckerActionRunner.java index 06b093d5f..2e43a8139 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingMatrixCheckerActionRunner.java +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingMatrixCheckerActionRunner.java @@ -238,6 +238,7 @@ public void checkMapperConstraints(String targetKey, Map inputs) log.info("Skipped mapper due to blank target, source:{}", mapper.getSource()); continue; } + String source = replaceStar(mapper.getSource()); if (MappingTypeEnum.ENUM.getKind().equals(mapper.getSourceType()) || MappingTypeEnum.DISCRETE_STR.getKind().equals(mapper.getSourceType()) || MappingTypeEnum.DISCRETE_INT.getKind().equals(mapper.getSourceType()) diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingTransformer.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingTransformer.java index eae5bba27..f3559dc54 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingTransformer.java +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/runner/MappingTransformer.java @@ -1,7 +1,7 @@ package com.consoleconnect.kraken.operator.gateway.runner; import static com.consoleconnect.kraken.operator.core.toolkit.Constants.DOT; -import static com.consoleconnect.kraken.operator.core.toolkit.ConstructExpressionUtil.convertToJsonPointer; +import static com.consoleconnect.kraken.operator.core.toolkit.ConstructExpressionUtil.*; import com.consoleconnect.kraken.operator.core.dto.StateValueMappingDto; import com.consoleconnect.kraken.operator.core.model.facet.ComponentAPIFacets; @@ -10,6 +10,7 @@ import com.consoleconnect.kraken.operator.gateway.template.SpELEngine; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; +import io.jsonwebtoken.lang.Strings; import java.util.*; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONArray; @@ -22,6 +23,7 @@ public interface MappingTransformer { String REPLACEMENT_KEY_SUFFIX = "}}"; String ARRAY_WILD_MASK = "[*]"; String ARRAY_FIRST_ELE = "[0]"; + String ARRAY_FIRST_REGEX = "\\[0\\]"; String TARGET_VALUE_MAPPER_KEY = "targetValueMapping"; String JSON_PATH_EXPRESSION_PREFIX = "$."; String ENUM_KIND = "enum"; @@ -31,10 +33,27 @@ public interface MappingTransformer { String FORWARD_DOWNSTREAM = "forwardDownstream"; String REQUEST_BODY = "requestBody."; String RESPONSE_BODY = "responseBody"; + String MAPPING_TYPE = "array"; + String SLASH = "/"; + String MEF_REQ_BODY_JSON_ROOT = "$.mefRequestBody."; @Slf4j final class LogHolder {} + private boolean isMissingMappers(ComponentAPITargetFacets.Endpoint endpoint) { + return Objects.isNull(endpoint.getMappers()) + || CollectionUtils.isEmpty(endpoint.getMappers().getResponse()); + } + + private boolean isSkippableMapper(ComponentAPITargetFacets.Mapper mapper) { + return StringUtils.isBlank(mapper.getSource()) && StringUtils.isBlank(mapper.getDefaultValue()); + } + + private boolean isDeletableMapper(ComponentAPITargetFacets.Mapper mapper) { + return StringUtils.isNotBlank(mapper.getCheckPath()) + && StringUtils.isNotBlank(mapper.getDeletePath()); + } + /** * Transform mappers into response body * @@ -42,51 +61,120 @@ final class LogHolder {} * @return the response body replaced by mappers */ default String transform( - ComponentAPITargetFacets.Endpoint endpoint, StateValueMappingDto responseTargetMapperDto) { + ComponentAPITargetFacets.Endpoint endpoint, + StateValueMappingDto responseTargetMapperDto, + Map inputs) { String responseBody = endpoint.getResponseBody(); - ComponentAPITargetFacets.Mappers mappers = endpoint.getMappers(); - if (Objects.isNull(mappers) || CollectionUtils.isEmpty(mappers.getResponse())) { + if (isMissingMappers(endpoint)) { return com.consoleconnect.kraken.operator.core.toolkit.StringUtils.compact(responseBody); } String compactedResponseBody = com.consoleconnect.kraken.operator.core.toolkit.StringUtils.compact(responseBody); LogHolder.log.info("compactedResponseBody:{}", compactedResponseBody); - List response = mappers.getResponse(); - for (ComponentAPITargetFacets.Mapper mapper : response) { - // Preparing check and delete path for final result - if (StringUtils.isNotBlank(mapper.getCheckPath()) - && StringUtils.isNotBlank(mapper.getDeletePath())) { - responseTargetMapperDto - .getTargetCheckPathMapper() - .put(mapper.getCheckPath(), mapper.getDeletePath()); - } - // Reading response string, and find the target node - // Using the source value to replace the value of target node - if (StringUtils.isBlank(mapper.getSource()) - && StringUtils.isBlank(mapper.getDefaultValue())) { - continue; - } - String convertedSource = - convertSource( - mapper.getSource(), - mapper.getDefaultValue(), - mapper.getTargetType(), - mapper.getReplaceStar()); - String convertedTarget = convertTarget(mapper.getTarget()); - addTargetValueMapping(mapper, responseTargetMapperDto, convertedTarget); - LogHolder.log.info( - "converted source:{}, converted target:{},", convertedSource, convertedTarget); + List responseMappers = endpoint.getMappers().getResponse(); + for (ComponentAPITargetFacets.Mapper mapper : responseMappers) { compactedResponseBody = - JsonToolkit.generateJson( - convertToJsonPointer(mapper.getTarget().replace(RESPONSE_BODY, StringUtils.EMPTY)), - convertedSource, - compactedResponseBody); + processMapper(mapper, responseTargetMapperDto, inputs, compactedResponseBody); } LogHolder.log.info("response mapper transform result:{}", compactedResponseBody); return com.consoleconnect.kraken.operator.core.toolkit.StringUtils.compact( compactedResponseBody); } + private String processMapper( + ComponentAPITargetFacets.Mapper mapper, + StateValueMappingDto responseTargetMapperDto, + Map inputs, + String compactedResponseBody) { + // Preparing check and delete path for final result + if (isDeletableMapper(mapper)) { + responseTargetMapperDto + .getTargetCheckPathMapper() + .put(mapper.getCheckPath(), mapper.getDeletePath()); + } + // Reading response string, and find the target node + // Using the source value to replace the value of target node + if (isSkippableMapper(mapper)) { + return compactedResponseBody; + } + if (MAPPING_TYPE.equals(mapper.getMappingType())) { + String jsonPath = constructJsonPath(MEF_REQ_BODY_JSON_ROOT, mapper.getTarget()); + int length = lengthOfArrayNode(jsonPath, inputs); + LogHolder.log.info( + "Transforming responseBody array length:{}, json path:{}", length, jsonPath); + if (length < 0) { + compactedResponseBody = + processMappingBody(mapper, responseTargetMapperDto, compactedResponseBody, 0); + } else { + int count = 0; + while (count < length) { + compactedResponseBody = + processMappingBody(mapper, responseTargetMapperDto, compactedResponseBody, count); + count++; + } + } + } else { + compactedResponseBody = + processMappingBody(mapper, responseTargetMapperDto, compactedResponseBody, 0); + } + return compactedResponseBody; + } + + private String processMappingBody( + ComponentAPITargetFacets.Mapper mapper, + StateValueMappingDto responseTargetMapperDto, + String compactedResponseBody, + int replaceIndex) { + String convertedSource = + convertSource( + mapper.getSource(), mapper.getDefaultValue(), mapper.getReplaceStar(), replaceIndex); + String convertedTarget = + convertTarget(mapper.getTarget(), mapper.getReplaceStar(), replaceIndex); + addTargetValueMapping(mapper, responseTargetMapperDto, convertedTarget); + String jsonPointer = + convertPathToJsonPointer( + mapper.getTarget().replace(RESPONSE_BODY, StringUtils.EMPTY), + buildSquareBracketIndex(replaceIndex)); + LogHolder.log.info( + "jsonPointer:{}, converted source:{}, converted target:{},", + jsonPointer, + convertedSource, + convertedTarget); + compactedResponseBody = + JsonToolkit.generateJson(jsonPointer, convertedSource, compactedResponseBody); + // Expanding array items + if (replaceIndex > 0) { + return expandArrayItems(compactedResponseBody, mapper.getTarget(), replaceIndex, jsonPointer); + } + return compactedResponseBody; + } + + default String buildSquareBracketIndex(int index) { + return LEFT_SQUARE_BRACKET + index + RIGHT_SQUARE_BRACKET; + } + + private String expandArrayItems( + String compactedResponseBody, String target, int count, String jsonPointer) { + DocumentContext doc = JsonPath.parse(compactedResponseBody); + String arrayPath = constructJsonPath(Strings.EMPTY, target); + int idx = arrayPath.indexOf(ARRAY_WILD_MASK); + if (idx > 0) { + String arrayRoot = arrayPath.substring(0, idx); + Map map = + doc.read(JSON_PATH_EXPRESSION_PREFIX + arrayRoot + ARRAY_FIRST_ELE, Map.class); + for (Map.Entry entry : map.entrySet()) { + String pointer = SLASH + arrayRoot + SLASH + count + SLASH + entry.getKey(); + if (jsonPointer.startsWith(pointer)) { + continue; + } + String entryValue = replaceALL(entry.getValue().toString(), arrayRoot, count); + compactedResponseBody = + JsonToolkit.generateJson(pointer, entryValue, compactedResponseBody); + } + } + return compactedResponseBody; + } + default void addTargetValueMapping( ComponentAPITargetFacets.Mapper mapper, StateValueMappingDto responseTargetMapperDto, @@ -163,9 +251,7 @@ default void deleteByPath(String path, DocumentContext doc) { default String calculateBasedOnResponseBody(String responseBody, Map context) { String replace = responseBody.replace("((", "${").replace("))", "}"); - LogHolder.log.info("calculateBasedOnResponseBody replace:{}", replace); Object obj = JsonToolkit.fromJson(replace, Object.class); - LogHolder.log.info("calculateBasedOnResponseBody obj:{}", obj); return SpELEngine.evaluate(obj, context, true); } @@ -200,6 +286,29 @@ default String rewriteValueByJsonPath( return doc.jsonString(); } + default int lengthOfArrayNode(String pathExpression, Object jsonData) { + if (StringUtils.isEmpty(pathExpression) || Objects.isNull(jsonData)) { + return -1; + } + DocumentContext doc = JsonPath.parse(jsonData); + int idx = pathExpression.lastIndexOf(ARRAY_WILD_MASK); + if (idx > 0) { + String arrayRoot = pathExpression.substring(0, idx); + if (arrayRoot.endsWith(DOT)) { + arrayRoot = arrayRoot + LENGTH_FUNC; + } else { + arrayRoot = arrayRoot + DOT + LENGTH_FUNC; + } + try { + return doc.read(arrayRoot); + } catch (Exception e) { + String errMsg = "Failed to read json data:" + arrayRoot; + LogHolder.log.error(errMsg, e); + } + } + return -1; + } + default void overwritePathValue( DocumentContext doc, String pathExpression, Map map) { String origin = doc.read(pathExpression); @@ -208,19 +317,30 @@ default void overwritePathValue( } } - default String convertTarget(String customizedExpress) { - if (null == customizedExpress || !customizedExpress.startsWith(REPLACEMENT_KEY_PREFIX)) { - return customizedExpress; + default String convertTarget(String target) { + return convertTarget(target, false, 0); + } + + default String convertTarget(String target, Boolean replaceStar, int replaceIndex) { + if (null == target || !target.startsWith(REPLACEMENT_KEY_PREFIX)) { + return target; } - customizedExpress = customizedExpress.replace(REQUEST_BODY, StringUtils.EMPTY); + String strippedValue = target.replace(REQUEST_BODY, StringUtils.EMPTY); // Remove the leading "@" and double curly braces - return customizedExpress.substring( - REPLACEMENT_KEY_PREFIX.length(), - customizedExpress.length() - REPLACEMENT_KEY_SUFFIX.length()); + strippedValue = + strippedValue.substring( + REPLACEMENT_KEY_PREFIX.length(), + strippedValue.length() - REPLACEMENT_KEY_SUFFIX.length()); + LogHolder.log.info("target strippedValue:{}", strippedValue); + int loc = strippedValue.lastIndexOf(ARRAY_WILD_MASK); + if (loc > 0 && Boolean.TRUE.equals(replaceStar)) { + return replaceStar(strippedValue, replaceIndex); + } + return strippedValue; } default String convertSource( - String source, String defaultValue, String targetType, Boolean replaceStar) { + String source, String defaultValue, Boolean replaceStar, int replaceIndex) { if (null == source || !source.startsWith(REPLACEMENT_KEY_PREFIX)) { return (StringUtils.isBlank(source) ? defaultValue : source); } @@ -228,7 +348,7 @@ default String convertSource( String strippedValue = source.substring( REPLACEMENT_KEY_PREFIX.length(), source.length() - REPLACEMENT_KEY_SUFFIX.length()); - LogHolder.log.info("source strippedValue:{}, targetType:{}", strippedValue, targetType); + LogHolder.log.info("source strippedValue:{}", strippedValue); int loc = strippedValue.lastIndexOf(ARRAY_WILD_MASK); if (!strippedValue.startsWith(RESPONSE_BODY)) { if (strippedValue.startsWith(ARRAY_FIRST_ELE) || strippedValue.startsWith(ARRAY_WILD_MASK)) { @@ -239,7 +359,7 @@ default String convertSource( } if (loc > 0) { if (Boolean.TRUE.equals(replaceStar)) { - return String.format("${%s}", replaceStar(strippedValue)); + return String.format("${%s}", replaceStar(strippedValue, replaceIndex)); } return String.format("${%s}", strippedValue); } @@ -259,10 +379,25 @@ default void mergeMapper( } default String replaceStar(String str) { + return replaceStar(str, 0); + } + + default String replaceALL(String str, String arrayRootPrefix, int index) { + if (StringUtils.isBlank(str)) { + return str; + } + return str.replaceAll( + arrayRootPrefix + ARRAY_FIRST_REGEX, + arrayRootPrefix + LEFT_SQUARE_BRACKET + index + RIGHT_SQUARE_BRACKET); + } + + default String replaceStar(String str, int index) { if (StringUtils.isBlank(str)) { return str; } - return str.replace(ARRAY_WILD_MASK, ARRAY_FIRST_ELE); + String replacement = + (index < 0 ? ARRAY_FIRST_ELE : LEFT_SQUARE_BRACKET + index + RIGHT_SQUARE_BRACKET); + return str.replace(ARRAY_WILD_MASK, replacement); } default Boolean forwardDownstream(ComponentAPIFacets.Action config) { diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/service/RenderRequestService.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/service/RenderRequestService.java index c7d7ba535..519f77942 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/service/RenderRequestService.java +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/main/java/com/consoleconnect/kraken/operator/gateway/service/RenderRequestService.java @@ -88,17 +88,20 @@ private String convertPath( public void parseRequest( ComponentAPITargetFacets facets, StateValueMappingDto stateValueMappingDto) { handlePath(facets); - handleBody(facets, stateValueMappingDto); + handleBody(facets, stateValueMappingDto, null); } - private void handleBody( - ComponentAPITargetFacets facets, StateValueMappingDto stateValueMappingDto) { + public void handleBody( + ComponentAPITargetFacets facets, + StateValueMappingDto stateValueMappingDto, + Map inputs) { String requestBody = facets.getEndpoints().get(0).getRequestBody(); List request = facets.getEndpoints().get(0).getMappers().getRequest(); if (StringUtils.isBlank(requestBody) || CollectionUtils.isEmpty(request)) { return; } + for (ComponentAPITargetFacets.Mapper mapper : request) { if (Objects.equals(BODY.name(), mapper.getTargetLocation())) { // Skipping constant target @@ -107,17 +110,7 @@ private void handleBody( log.info("handleBody skip source:{}, target:{}", mapper.getSource(), mapper.getTarget()); continue; } - String source = constructBody(mapper.getSource()); - requestBody = - JsonToolkit.generateJson( - convertToJsonPointer(mapper.getTarget().replace(REQUEST_BODY, StringUtils.EMPTY)), - source, - requestBody); - log.info( - "handleBody inserted source:{}, target:{}, requestBody:{}", - source, - mapper.getTarget(), - requestBody); + requestBody = expandRequestBody(mapper, inputs, requestBody); if (ENUM_KIND.equalsIgnoreCase(mapper.getSourceType()) && StringUtils.isNotBlank(mapper.getTarget()) && MapUtils.isNotEmpty(mapper.getValueMapping())) { @@ -136,6 +129,40 @@ private void handleBody( log.info("handleBody rendered request body:{}", requestBody); } + public String expandRequestBody( + ComponentAPITargetFacets.Mapper mapper, Map inputs, String requestBody) { + String source = ""; + if (MAPPING_TYPE.equals(mapper.getMappingType())) { + String jsonPath = constructJsonPath("$.body.", mapper.getSource()); + log.info("expandRequestBody json path:{}", jsonPath); + int length = lengthOfArrayNode(jsonPath, inputs); + int count = 0; + while (count < length) { + source = constructBodyOfArray(mapper.getSource(), count); + requestBody = + JsonToolkit.generateJson( + convertPathToJsonPointer( + mapper.getTarget().replace(REQUEST_BODY, StringUtils.EMPTY), "[" + count + "]"), + source, + requestBody); + count++; + } + } else { + source = constructBody(mapper.getSource()); + requestBody = + JsonToolkit.generateJson( + convertPathToJsonPointer(mapper.getTarget().replace(REQUEST_BODY, StringUtils.EMPTY)), + source, + requestBody); + } + log.info( + "handleBody inserted source:{}, target:{}, requestBody:{}", + source, + mapper.getTarget(), + requestBody); + return requestBody; + } + private void handlePathRefer(ComponentAPITargetFacets.Mapper mapper) { String convertValue = mapper.getConvertValue(); String[] pathReferIds = convertValue.split("#"); diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/MappingTransformerTest.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/MappingTransformerTest.java index 509772687..eaf77266d 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/MappingTransformerTest.java +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/MappingTransformerTest.java @@ -4,13 +4,27 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; +import com.consoleconnect.kraken.operator.core.dto.StateValueMappingDto; +import com.consoleconnect.kraken.operator.core.model.UnifiedAsset; +import com.consoleconnect.kraken.operator.core.model.facet.ComponentAPITargetFacets; +import com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit; +import com.consoleconnect.kraken.operator.gateway.helper.AssetConfigReader; import com.consoleconnect.kraken.operator.gateway.runner.MappingTransformer; +import com.consoleconnect.kraken.operator.test.MockIntegrationTest; +import com.fasterxml.jackson.core.type.TypeReference; import java.util.HashMap; import java.util.Map; +import lombok.SneakyThrows; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.test.context.ContextConfiguration; -class MappingTransformerTest implements MappingTransformer { +@MockIntegrationTest +@ContextConfiguration(classes = CustomConfig.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MappingTransformerTest extends AssetConfigReader implements MappingTransformer { @Test void givenJsonInput_whenDeleteNode_thenReturnOK() { @@ -23,6 +37,18 @@ void givenJsonInput_whenDeleteNode_thenReturnOK() { Assertions.assertEquals("{\"key\":\"hello kraken\"}", result); } + @Test + void givenJsonArray_whenCounting_thenReturnOK() { + String pathExpression = "$.quoteItem[*].requestedQuoteItemTerm.duration.units"; + String jsonData = + "{\"quoteItem\":[{\"quoteItemTerm\":[{\"duration\":{\"amount\":1,\"units\":\"calendarMonths\"}}]}]}"; + Map quoteItemMap = + JsonToolkit.fromJson(jsonData, new TypeReference>() {}); + int result = lengthOfArrayNode(pathExpression, quoteItemMap); + System.out.println(result); + Assertions.assertTrue(result > -1); + } + @Test void givenJson_whenDeleteNodeByPath_thenDeleteNodeSuccess() { Map checkPathMap = new HashMap<>(); @@ -36,4 +62,30 @@ void givenJson_whenDeleteNodeByPath_thenDeleteNodeSuccess() { String s = deleteNodeByPath(checkPathMap, input); assertThat(s, hasJsonPath("$.productOrderItem[0].completionDate"), notNullValue()); } + + @SneakyThrows + @Test + void givenArrayRequest_whenTransform_thenReturnOK() { + Map map = + JsonToolkit.fromJson( + readFileToString("mockData/quoteArrayRequest.json"), + new TypeReference>() {}); + Map inputs = new HashMap<>(); + inputs.put("mefRequestBody", map); + StateValueMappingDto stateValueMappingDto = new StateValueMappingDto(); + UnifiedAsset targetAsset = + getTarget( + "deployment-config/components/api-targets/api-target.quote.uni.add.sync.yaml", + "deployment-config/components/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml"); + ComponentAPITargetFacets facets = + UnifiedAsset.getFacets(targetAsset, ComponentAPITargetFacets.class); + facets + .getEndpoints() + .forEach( + endpoint -> { + String transformedResp = transform(endpoint, stateValueMappingDto, inputs); + String expectedResult = readCompactedFile("mockData/expected-quote-array-resp.json"); + Assertions.assertEquals(expectedResult, transformedResp); + }); + } } diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/RequestMapperTest.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/RequestMapperTest.java index ed3ebe225..b50733d5f 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/RequestMapperTest.java +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/RequestMapperTest.java @@ -1,6 +1,5 @@ package com.consoleconnect.kraken.operator.gateway; -import static com.consoleconnect.kraken.operator.core.toolkit.ConstructExpressionUtil.convertToJsonPointer; import static com.consoleconnect.kraken.operator.gateway.SpELEngineTest.readFileToString; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static org.hamcrest.MatcherAssert.assertThat; @@ -13,6 +12,7 @@ import com.consoleconnect.kraken.operator.core.model.UnifiedAsset; import com.consoleconnect.kraken.operator.core.model.facet.ComponentAPITargetFacets; import com.consoleconnect.kraken.operator.core.service.UnifiedAssetService; +import com.consoleconnect.kraken.operator.core.toolkit.ConstructExpressionUtil; import com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit; import com.consoleconnect.kraken.operator.core.toolkit.YamlToolkit; import com.consoleconnect.kraken.operator.gateway.runner.LoadTargetAPIConfigActionRunner; @@ -91,13 +91,20 @@ private ComponentAPITargetFacets renderFacets(String path) throws IOException { @Test void givenPathKey_whenGenerate_returnJson() { String s = - JsonToolkit.generateJson(convertToJsonPointer("@{{user.class[*].name}}"), "John", "{}"); + JsonToolkit.generateJson( + ConstructExpressionUtil.convertPathToJsonPointer("@{{user.class[*].name}}"), + "John", + "{}"); String s2 = JsonToolkit.generateJson( - convertToJsonPointer("@{{user.class[*].password}}"), "password", s); + ConstructExpressionUtil.convertPathToJsonPointer("@{{user.class[*].password}}"), + "password", + s); String s3 = JsonToolkit.generateJson( - convertToJsonPointer("@{{user.class[*].secret.key}}"), "secretKey", s2); + ConstructExpressionUtil.convertPathToJsonPointer("@{{user.class[*].secret.key}}"), + "secretKey", + s2); log.info("result: {}", s3); assertThat(s3, hasJsonPath("$.user.class[0].secret.key", equalTo("secretKey"))); Map params = new HashMap<>(); @@ -105,7 +112,8 @@ void givenPathKey_whenGenerate_returnJson() { params.put("key2", "value2"); String s4 = JsonToolkit.generateJson( - convertToJsonPointer("@{{user.class[*].quoteItemPrice[*]}}"), + ConstructExpressionUtil.convertPathToJsonPointer( + "@{{user.class[*].quoteItemPrice[*]}}"), JsonToolkit.toJson(params), s3); log.info("s4:{}", s4); diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/helper/AssetConfigReader.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/helper/AssetConfigReader.java new file mode 100644 index 000000000..6511e1f49 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/helper/AssetConfigReader.java @@ -0,0 +1,65 @@ +package com.consoleconnect.kraken.operator.gateway.helper; + +import static com.consoleconnect.kraken.operator.core.toolkit.Constants.MAPPER_SIGN; + +import com.consoleconnect.kraken.operator.core.model.UnifiedAsset; +import com.consoleconnect.kraken.operator.core.model.facet.ComponentAPITargetFacets; +import com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit; +import com.consoleconnect.kraken.operator.core.toolkit.YamlToolkit; +import com.consoleconnect.kraken.operator.test.AbstractIntegrationTest; +import com.fasterxml.jackson.core.type.TypeReference; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; + +public class AssetConfigReader extends AbstractIntegrationTest { + + @SneakyThrows + public String readCompactedFile(String path) { + return com.consoleconnect.kraken.operator.core.toolkit.StringUtils.compact( + readFileToString(path)); + } + + public UnifiedAsset getTarget(String targetApiPath, String mapperApiPath) throws IOException { + Optional unifiedAsset = + YamlToolkit.parseYaml(readFileToString(targetApiPath), UnifiedAsset.class); + Optional mapperAssetOpt = + YamlToolkit.parseYaml(readFileToString(mapperApiPath), UnifiedAsset.class); + + UnifiedAsset targetAsset = unifiedAsset.get(); + UnifiedAsset targetMapperAsset = mapperAssetOpt.get(); + String targetKey = extractTargetKey(targetMapperAsset.getMetadata().getKey()); + Assertions.assertEquals(targetAsset.getMetadata().getKey(), targetKey); + + ComponentAPITargetFacets facets = + UnifiedAsset.getFacets(targetAsset, ComponentAPITargetFacets.class); + ComponentAPITargetFacets mapperFacets = + UnifiedAsset.getFacets(targetMapperAsset, ComponentAPITargetFacets.class); + facets.getEndpoints().get(0).setPath(mapperFacets.getEndpoints().get(0).getPath()); + facets.getEndpoints().get(0).setMethod(mapperFacets.getEndpoints().get(0).getMethod()); + facets.getEndpoints().get(0).setMappers(mapperFacets.getEndpoints().get(0).getMappers()); + targetAsset.setFacets( + JsonToolkit.fromJson( + JsonToolkit.toJson(facets), new TypeReference>() {})); + return targetAsset; + } + + public String extractTargetKey(String targetMapperKey) { + if (StringUtils.isBlank(targetMapperKey)) { + return ""; + } + int loc = targetMapperKey.indexOf(MAPPER_SIGN); + if (loc < 0) { + return ""; + } + if (loc + MAPPER_SIGN.length() == targetMapperKey.length()) { + return targetMapperKey.substring(0, loc); + } else { + return targetMapperKey.substring(0, loc) + + targetMapperKey.substring(loc + MAPPER_SIGN.length()); + } + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/service/RenderRequestServiceTest.java b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/service/RenderRequestServiceTest.java new file mode 100644 index 000000000..a648d5752 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/java/com/consoleconnect/kraken/operator/gateway/service/RenderRequestServiceTest.java @@ -0,0 +1,57 @@ +package com.consoleconnect.kraken.operator.gateway.service; + +import com.consoleconnect.kraken.operator.core.dto.StateValueMappingDto; +import com.consoleconnect.kraken.operator.core.model.UnifiedAsset; +import com.consoleconnect.kraken.operator.core.model.facet.ComponentAPITargetFacets; +import com.consoleconnect.kraken.operator.core.service.UnifiedAssetService; +import com.consoleconnect.kraken.operator.core.toolkit.JsonToolkit; +import com.consoleconnect.kraken.operator.gateway.CustomConfig; +import com.consoleconnect.kraken.operator.gateway.helper.AssetConfigReader; +import com.consoleconnect.kraken.operator.test.MockIntegrationTest; +import com.fasterxml.jackson.core.type.TypeReference; +import java.util.HashMap; +import java.util.Map; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; + +@Slf4j +@MockIntegrationTest +@ContextConfiguration(classes = CustomConfig.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RenderRequestServiceTest extends AssetConfigReader { + + @Autowired private UnifiedAssetService unifiedAssetService; + + @SpyBean RenderRequestService renderRequestService; + + @SneakyThrows + @Test + void givenArrayItems_whenHandleBody_thenReturnOK() { + Map map = + JsonToolkit.fromJson( + readFileToString("mockData/quoteArrayRequest.json"), + new TypeReference>() {}); + Map inputs = new HashMap<>(); + inputs.put("body", map); + StateValueMappingDto stateValueMappingDto = new StateValueMappingDto(); + UnifiedAsset targetAsset = + getTarget( + "deployment-config/components/api-targets/api-target.quote.uni.add.sync.yaml", + "deployment-config/components/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml"); + ComponentAPITargetFacets facets = + UnifiedAsset.getFacets(targetAsset, ComponentAPITargetFacets.class); + renderRequestService.handleBody(facets, stateValueMappingDto, inputs); + String transformedRequest = + com.consoleconnect.kraken.operator.core.toolkit.StringUtils.compact( + facets.getEndpoints().get(0).getRequestBody()); + String expectedResult = readCompactedFile("mockData/expected-quote-array-request.json"); + Assertions.assertEquals(expectedResult, transformedRequest); + } +} diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/components/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/components/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml new file mode 100644 index 000000000..18adc5812 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/components/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml @@ -0,0 +1,220 @@ +--- +kind: kraken.component.api-target-mapper +apiVersion: v1 +metadata: + key: mef.sonata.api-target-mapper.quote.uni.add.sync + name: Mapper Of Creating Quote UNI + description: This operation creates a Quote entity +spec: + trigger: + path: /mefApi/sonata/quoteManagement/v8/quote + method: post + productType: uni + actionType: add + endpoints: + - id: create quote for port + path: /api/pricing/calculate + method: post + serverKey: mef.sonata.api-target-spec.con1718940696857 + mappers: + request: + - name: mapper.quote.uni.add.sync.duration.amount + source: "@{{quoteItem[*].requestedQuoteItemTerm.duration.amount}}" + sourceLocation: BODY + target: "1" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.duration.units + source: "@{{quoteItem[*].requestedQuoteItemTerm.duration.units}}" + sourceLocation: BODY + sourceType: enum + sourceValues: + - calendarMonths + - calendarDays + - calendarHours + - calendarMinutes + - businessDays + - businessHours + - businessMinutes + valueMapping: + calendarMonths: m + target: "@{{ports[*].durationUnit}}" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.endOfTermAction + source: "@{{quoteItem[*].requestedQuoteItemTerm.endOfTermAction}}" + sourceLocation: BODY + sourceType: enum + sourceValues: + - roll + - autoDisconnect + - autoRenew + valueMapping: {} + target: "roll" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.rollInterval.amount + source: "@{{quoteItem[*].requestedQuoteItemTerm.rollInterval.amount}}" + sourceLocation: BODY + target: "1" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.rollInterval.units + source: "@{{quoteItem[*].requestedQuoteItemTerm.rollInterval.units}}" + sourceLocation: BODY + sourceType: enum + sourceValues: + - calendarMonths + - calendarDays + - calendarHours + - calendarMinutes + - businessDays + - businessHours + - businessMinutes + valueMapping: {} + target: "calendarMonths" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.place.id + source: "@{{quoteItem[*].product.place[0].id}}" + sourceLocation: BODY + target: "@{{ports[*].dcf}}" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.productConfiguration.bandwidth + source: "@{{quoteItem[*].product.productConfiguration.bandwidth}}" + sourceLocation: BODY + target: "@{{ports[*].speed}}" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.productConfiguration.bandwidthUnit + source: "@{{quoteItem[*].product.productConfiguration.bandwidthUnit}}" + sourceLocation: BODY + sourceType: enum + sourceValues: + - GBPS + - MBPS + valueMapping: {} + target: "MBPS" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.productOffering.id + source: "@{{quoteItem[*].product.productOffering.id}}" + sourceLocation: BODY + target: "UNI" + targetLocation: BODY + requiredMapping: false + mappingType: array + response: + - name: mapper.quote.uni.add.sync.unitOfMeasure + title: Quote unitOfMeasure Mapping + description: quote unitOfMeasure mapping + source: "Gb" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].unitOfMeasure}}" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.price.unit + title: Quote Price Unit Mapping + description: quote price mapping + source: "USD" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].price.dutyFreeAmount.unit}}" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.price.value + title: Quote Price Value Mapping + description: quote price mapping + source: "@{{responseBody.results[*].price}}" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].price.dutyFreeAmount.value}}" + targetLocation: BODY + requiredMapping: true + mappingType: array + - name: mapper.quote.uni.add.sync.taxRate + title: Quote taxRate Mapping + description: quote taxRate mapping + source: "16" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].price.taxRate}}" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.taxIncludedAmount.unit + title: Quote taxIncludedAmount unit Mapping + description: quote taxIncludedAmount unit mapping + source: "USD" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].price.taxIncludedAmount.unit}}" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.taxIncludedAmount.value + title: Quote taxIncludedAmount value Mapping + description: quote taxIncludedAmount value mapping + source: "100" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].price.taxIncludedAmount.value}}" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.quoteItemPrice.name + title: Quote quoteItemPrice name Mapping + description: quote quoteItemPrice name mapping + source: "name-here" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].name}}" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.quoteItemPrice.priceType + title: Quote quoteItemPrice priceType Mapping + description: quote quoteItemPrice priceType mapping + source: "recurring" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].priceType}}" + targetType: enum + targetLocation: BODY + requiredMapping: false + targetValues: + - recurring + - nonRecurring + - usageBased + valueMapping: {} + mappingType: array + - name: mapper.quote.uni.add.sync.quoteItemPrice.description + title: Quote quoteItemPrice description Mapping + description: quote quoteItemPrice description mapping + source: "" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].description}}" + targetLocation: BODY + requiredMapping: false + mappingType: array + - name: mapper.quote.uni.add.sync.quoteItemPrice.recurringChargePeriod + title: Quote quoteItemPrice recurringChargePeriod Mapping + description: quote quoteItemPrice recurringChargePeriod mapping + source: "month" + sourceLocation: BODY + target: "@{{quoteItem[*].quoteItemPrice[0].recurringChargePeriod}}" + targetType: enum + targetLocation: BODY + requiredMapping: false + targetValues: + - hour + - day + - week + - month + - year + valueMapping: {} + mappingType: array \ No newline at end of file diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/components/api-targets/api-target.quote.uni.add.sync.yaml b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/components/api-targets/api-target.quote.uni.add.sync.yaml new file mode 100644 index 000000000..097279f18 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/components/api-targets/api-target.quote.uni.add.sync.yaml @@ -0,0 +1,64 @@ +--- +kind: kraken.component.api-target +apiVersion: v1 +metadata: + key: mef.sonata.api-target.quote.uni.add.sync + name: Quote Management API + mapperKey: mef.sonata.api-target-mapper.quote.uni.add.sync + version: 0 +spec: + inputs: + - mefQuery + - mefRequestBody + - entity + trigger: + path: /mefApi/sonata/quoteManagement/v8/quote + method: post + productType: uni + actionType: add + endpoints: + - id: create quote for port + path: + method: + requestBody: | + {} + responseBody: | + { + "id": "${entity.id}", + "buyerRequestedQuoteLevel": "${mefRequestBody.buyerRequestedQuoteLevel}", + "relatedContactInformation": "${T(com.consoleconnect.kraken.operator.gateway.func.SpelFunc).appendSellerInformation('sellerContactInformation', env.seller.name, env.seller.emailAddress, env.seller.number, mefRequestBody[relatedContactInformation]?:'')}", + "quoteItem": [ + { + "requestedQuoteItemTerm": "${mefRequestBody.quoteItem[0].requestedQuoteItemTerm}", + "product": "${mefRequestBody.quoteItem[0].product}", + "action": "add", + "id": "${mefRequestBody.quoteItem[0].id}", + "state": "(((mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value == '') or (T(java.lang.Double).parseDouble(mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value) < 0)? 'unableToProvide': 'approved.orderable'))", + "quoteItemTerm": "${T(java.util.Arrays).asList(mefRequestBody.quoteItem[0].requestedQuoteItemTerm)}", + "quoteItemPrice": [{ + "unitOfMeasure":"", + "price": { + "dutyFreeAmount": { + "unit": "", + "value": "" + }, + "taxRate":"", + "taxIncludedAmount": { + "unit": "", + "value": "" + } + }, + "name":"", + "priceType":"", + "description":"", + "recurringChargePeriod":"" + }] + } + ], + "quoteDate": "${T(com.consoleconnect.kraken.operator.core.toolkit.DateTime).nowInUTCFormatted()}", + "externalId":"${mefRequestBody[externalId]?:''}", + "instantSyncQuote":"${mefRequestBody[instantSyncQuote]?:''}", + "requestedQuoteCompletionDate": "${mefRequestBody[requestedQuoteCompletionDate]?:''}", + "quoteLevel": "${mefRequestBody.buyerRequestedQuoteLevel}", + "state": "(((mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value == '') or (T(java.lang.Double).parseDouble(mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value) < 0)? 'unableToProvide': 'approved.orderable'))" + } \ No newline at end of file diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/kraken-product-sonata.yaml b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/kraken-product-sonata.yaml index 2da9ce65c..c8b8da805 100644 --- a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/kraken-product-sonata.yaml +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/deployment-config/kraken-product-sonata.yaml @@ -23,9 +23,11 @@ spec: - classpath:deployment-config/components/api-targets/api-target.order.eline.read.yaml - classpath:deployment-config/components/api-targets/api-target.hub.add.yaml - classpath:deployment-config/components/api-targets/api-target.quote.uni.add.yaml + - classpath:deployment-config/components/api-targets/api-target.quote.uni.add.sync.yaml - classpath:deployment-config/components/api-targets/api-target.order.notification.state.change.yaml - classpath:deployment-config/components/api-targets-mappers/api-target-mapper.hub.add.yaml - classpath:deployment-config/components/api-targets-mappers/api-target-mapper.quote.uni.add.yaml + - classpath:deployment-config/components/api-targets-mappers/api-target-mapper.quote.uni.add.sync.yaml - classpath:deployment-config/components/api-targets-mappers/api-target-mapper.order.uni.add.yaml - classpath:deployment-config/components/api-targets-mappers/api-target-mapper.order.eline.add.yaml - classpath:deployment-config/components/api-targets-mappers/api-target-mapper.order.notification.state.change.yaml diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/expected-quote-array-request.json b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/expected-quote-array-request.json new file mode 100644 index 000000000..47dfa0c68 --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/expected-quote-array-request.json @@ -0,0 +1,15 @@ +{ + "ports" : [ { + "durationUnit" : "${body.quoteItem[0].requestedQuoteItemTerm.duration.units}", + "dcf" : "${body.quoteItem[0].product.place[0].id}", + "speed" : "${body.quoteItem[0].product.productConfiguration.bandwidth}" + }, { + "durationUnit" : "${body.quoteItem[1].requestedQuoteItemTerm.duration.units}", + "dcf" : "${body.quoteItem[1].product.place[0].id}", + "speed" : "${body.quoteItem[1].product.productConfiguration.bandwidth}" + }, { + "durationUnit" : "${body.quoteItem[2].requestedQuoteItemTerm.duration.units}", + "dcf" : "${body.quoteItem[2].product.place[0].id}", + "speed" : "${body.quoteItem[2].product.productConfiguration.bandwidth}" + } ] +} \ No newline at end of file diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/expected-quote-array-resp.json b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/expected-quote-array-resp.json new file mode 100644 index 000000000..be2bf25fc --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/expected-quote-array-resp.json @@ -0,0 +1,95 @@ +{ + "id": "${entity.id}", + "buyerRequestedQuoteLevel": "${mefRequestBody.buyerRequestedQuoteLevel}", + "relatedContactInformation": "${T(com.consoleconnect.kraken.operator.gateway.func.SpelFunc).appendSellerInformation('sellerContactInformation',env.seller.name,env.seller.emailAddress,env.seller.number,mefRequestBody[relatedContactInformation]?:'')}", + "quoteItem": [ + { + "requestedQuoteItemTerm": "${mefRequestBody.quoteItem[0].requestedQuoteItemTerm}", + "product": "${mefRequestBody.quoteItem[0].product}", + "action": "add", + "id": "${mefRequestBody.quoteItem[0].id}", + "state": "(((mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value=='')or(T(java.lang.Double).parseDouble(mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value)<0)?'unableToProvide':'approved.orderable'))", + "quoteItemTerm": "${T(java.util.Arrays).asList(mefRequestBody.quoteItem[0].requestedQuoteItemTerm)}", + "quoteItemPrice": [ + { + "unitOfMeasure": "Gb", + "price": { + "dutyFreeAmount": { + "unit": "USD", + "value": "${responseBody.results[0].price}" + }, + "taxRate": "16", + "taxIncludedAmount": { + "unit": "USD", + "value": "100" + } + }, + "name": "name-here", + "priceType": "recurring", + "description": "", + "recurringChargePeriod": "month" + } + ] + }, + { + "quoteItemPrice": [ + { + "unitOfMeasure": "Gb", + "price": { + "dutyFreeAmount": { + "unit": "USD", + "value": "${responseBody.results[1].price}" + }, + "taxRate": "16", + "taxIncludedAmount": { + "unit": "USD", + "value": "100" + } + }, + "name": "name-here", + "priceType": "recurring", + "recurringChargePeriod": "month" + } + ], + "requestedQuoteItemTerm": "${mefRequestBody.quoteItem[1].requestedQuoteItemTerm}", + "product": "${mefRequestBody.quoteItem[1].product}", + "action": "add", + "id": "${mefRequestBody.quoteItem[1].id}", + "state": "(((mefResponseBody.quoteItem[1].quoteItemPrice[0].price.dutyFreeAmount.value=='')or(T(java.lang.Double).parseDouble(mefResponseBody.quoteItem[1].quoteItemPrice[0].price.dutyFreeAmount.value)<0)?'unableToProvide':'approved.orderable'))", + "quoteItemTerm": "${T(java.util.Arrays).asList(mefRequestBody.quoteItem[1].requestedQuoteItemTerm)}" + }, + { + "quoteItemPrice": [ + { + "unitOfMeasure": "Gb", + "price": { + "dutyFreeAmount": { + "unit": "USD", + "value": "${responseBody.results[2].price}" + }, + "taxRate": "16", + "taxIncludedAmount": { + "unit": "USD", + "value": "100" + } + }, + "name": "name-here", + "priceType": "recurring", + "recurringChargePeriod": "month" + } + ], + "requestedQuoteItemTerm": "${mefRequestBody.quoteItem[2].requestedQuoteItemTerm}", + "product": "${mefRequestBody.quoteItem[2].product}", + "action": "add", + "id": "${mefRequestBody.quoteItem[2].id}", + "state": "(((mefResponseBody.quoteItem[2].quoteItemPrice[0].price.dutyFreeAmount.value=='')or(T(java.lang.Double).parseDouble(mefResponseBody.quoteItem[2].quoteItemPrice[0].price.dutyFreeAmount.value)<0)?'unableToProvide':'approved.orderable'))", + "quoteItemTerm": "${T(java.util.Arrays).asList(mefRequestBody.quoteItem[2].requestedQuoteItemTerm)}" + } + ], + "quoteDate": "${T(com.consoleconnect.kraken.operator.core.toolkit.DateTime).nowInUTCFormatted()}", + "externalId": "${mefRequestBody[externalId]?:''}", + "instantSyncQuote": "${mefRequestBody[instantSyncQuote]?:''}", + "requestedQuoteCompletionDate": "${mefRequestBody[requestedQuoteCompletionDate]?:''}", + "quoteLevel": "${mefRequestBody.buyerRequestedQuoteLevel}", + "state": "(((mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value=='')or(T(java.lang.Double).parseDouble(mefResponseBody.quoteItem[0].quoteItemPrice[0].price.dutyFreeAmount.value)<0)?'unableToProvide':'approved.orderable'))" +} \ No newline at end of file diff --git a/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/quoteArrayRequest.json b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/quoteArrayRequest.json new file mode 100644 index 000000000..c85ad79ef --- /dev/null +++ b/kraken-java-sdk/kraken-java-sdk-gateway/src/test/resources/mockData/quoteArrayRequest.json @@ -0,0 +1,252 @@ +{ + "note": [], + "requestedQuoteCompletionDate": "2027-12-31T23:00:00Z", + "relatedContactInformation": [ + { + "number": "111 222 333", + "emailAddress": "buyerContact@mef.com", + "role": "buyerContactInformation", + "postalAddress": null, + "organization": "MEF", + "name": "Joe Buyer", + "numberExtension": null + } + ], + "externalId": "ORDERABLE_ALTERNATE [8203] Sync Quote FIRM - ADD", + "buyerRequestedQuoteLevel": "firm", + "projectId": "MEF OIT", + "instantSyncQuote": true, + "quoteItem": [ + { + "requestedQuoteItemTerm": { + "name": "UNI Item Term 1", + "duration": { + "amount": 1, + "units": "calendarMonths" + }, + "endOfTermAction": "roll", + "description": "description 1", + "rollInterval": { + "amount": 1, + "units": "calendarMonths" + } + }, + "note": [], + "product": { + "id": null, + "href": null, + "place": [ + { + "role": "INSTALL_LOCATION", + "href": null, + "id": "6356792a4806220015488631", + "keyName": "qe-dcf-1", + "@type": "GeographicAddressRef", + "@schemaLocation": null + } + ], + "productConfiguration": { + "@type": "UNI", + "bandwidth": 10001, + "bandwidthUnit": "MBPS", + "name": "test-qc01", + "paymentType": "invoice" + }, + "productOffering": { + "id": "UNI", + "href": null + }, + "productRelationship": [] + }, + "productOfferingQualificationItem": { + "productOfferingQualificationId": "", + "alternateProductProposalId": null, + "productOfferingQualificationHref": null, + "id": "UNI" + }, + "relatedContactInformation": [ + { + "number": "111 222 333", + "emailAddress": "quoteItemTechnicalContact@mef.com", + "role": "quoteItemTechnicalContact", + "postalAddress": null, + "organization": "MEF", + "name": "David Item", + "numberExtension": null + }, + { + "number": "111 222 333", + "emailAddress": "quoteItemLocationContact@mef.com", + "role": "quoteItemLocationContact", + "postalAddress": null, + "organization": "MEF", + "name": "Konrad Item Location", + "numberExtension": null + } + ], + "agreementName": null, + "action": "add", + "dealReference": null, + "id": "UNI", + "requestedQuoteItemInstallationInterval": { + "amount": 10, + "units": "calendarMonths" + }, + "quoteItemRelationship": [] + }, + { + "requestedQuoteItemTerm": { + "name": "UNI Item Term 2", + "duration": { + "amount": 1, + "units": "calendarMonths" + }, + "endOfTermAction": "roll", + "description": "description 2", + "rollInterval": { + "amount": 1, + "units": "calendarMonths" + } + }, + "note": [], + "product": { + "id": null, + "href": null, + "place": [ + { + "role": "INSTALL_LOCATION", + "href": null, + "id": "6356792a4806220015488631", + "keyName": "qe-dcf-2", + "@type": "GeographicAddressRef", + "@schemaLocation": null + } + ], + "productConfiguration": { + "@type": "UNI", + "bandwidth": 10001, + "bandwidthUnit": "MBPS", + "name": "test-qc01", + "paymentType": "invoice" + }, + "productOffering": { + "id": "UNI", + "href": null + }, + "productRelationship": [] + }, + "productOfferingQualificationItem": { + "productOfferingQualificationId": "", + "alternateProductProposalId": null, + "productOfferingQualificationHref": null, + "id": "UNI" + }, + "relatedContactInformation": [ + { + "number": "111 222 333", + "emailAddress": "quoteItemTechnicalContact@mef.com", + "role": "quoteItemTechnicalContact", + "postalAddress": null, + "organization": "MEF", + "name": "David Item", + "numberExtension": null + }, + { + "number": "111 222 333", + "emailAddress": "quoteItemLocationContact@mef.com", + "role": "quoteItemLocationContact", + "postalAddress": null, + "organization": "MEF", + "name": "Konrad Item Location", + "numberExtension": null + } + ], + "agreementName": null, + "action": "add", + "dealReference": null, + "id": "UNI", + "requestedQuoteItemInstallationInterval": { + "amount": 10, + "units": "calendarMonths" + }, + "quoteItemRelationship": [] + }, + { + "requestedQuoteItemTerm": { + "name": "UNI Item Term 3", + "duration": { + "amount": 1, + "units": "calendarMonths" + }, + "endOfTermAction": "roll", + "description": "description 3", + "rollInterval": { + "amount": 1, + "units": "calendarMonths" + } + }, + "note": [], + "product": { + "id": null, + "href": null, + "place": [ + { + "role": "INSTALL_LOCATION", + "href": null, + "id": "6356792a4806220015488631", + "keyName": "qe-dcf-2", + "@type": "GeographicAddressRef", + "@schemaLocation": null + } + ], + "productConfiguration": { + "@type": "UNI", + "bandwidth": 10001, + "bandwidthUnit": "MBPS", + "name": "test-qc01", + "paymentType": "invoice" + }, + "productOffering": { + "id": "UNI", + "href": null + }, + "productRelationship": [] + }, + "productOfferingQualificationItem": { + "productOfferingQualificationId": "", + "alternateProductProposalId": null, + "productOfferingQualificationHref": null, + "id": "UNI" + }, + "relatedContactInformation": [ + { + "number": "111 222 333", + "emailAddress": "quoteItemTechnicalContact@mef.com", + "role": "quoteItemTechnicalContact", + "postalAddress": null, + "organization": "MEF", + "name": "David Item", + "numberExtension": null + }, + { + "number": "111 222 333", + "emailAddress": "quoteItemLocationContact@mef.com", + "role": "quoteItemLocationContact", + "postalAddress": null, + "organization": "MEF", + "name": "Konrad Item Location", + "numberExtension": null + } + ], + "agreementName": null, + "action": "add", + "dealReference": null, + "id": "UNI", + "requestedQuoteItemInstallationInterval": { + "amount": 10, + "units": "calendarMonths" + }, + "quoteItemRelationship": [] + } + ] +} \ No newline at end of file