diff options
9 files changed, 133 insertions, 121 deletions
diff --git a/docs/openapi/components.yml b/docs/openapi/components.yml index 736639d8..a03cb1f8 100644 --- a/docs/openapi/components.yml +++ b/docs/openapi/components.yml @@ -40,10 +40,7 @@ components: revision: type: string cmHandleProperties: - type: object - additionalProperties: - type: string - example: system-001 + $ref: '#/components/schemas/cmHandleProperties' ModuleSet: type: object @@ -82,9 +79,7 @@ components: type: string enum: [ read ] cmHandleProperties: - type: object - additionalProperties: - type: string + $ref: '#/components/schemas/cmHandleProperties' DataAccessWriteRequest: type: object @@ -97,9 +92,13 @@ components: data: type: string cmHandleProperties: - type: object - additionalProperties: - type: string + $ref: '#/components/schemas/cmHandleProperties' + + cmHandleProperties: + type: object + additionalProperties: + type: string + example: {"prop1":"value1","prop2":"value2"} responses: NotFound: @@ -174,19 +173,21 @@ components: type: string enum: [ application/json, application/yang-data+json ] - fieldsParamInQuery: - name: fields + optionsParamInQuery: + name: options in: query - description: Fields parameter to filter resource + description: options parameter in query, it is mandatory to wrap key(s)=value(s) in parenthesis'()'. required: false schema: type: string - - depthParamInQuery: - name: depth - in: query - description: Depth parameter for response - required: false - schema: - type: integer - minimum: 1
\ No newline at end of file + allowReserved: true + examples: + sample1: + value: + options: (key1=value1,key2=value2) + sample2: + value: + options: (key1=value1,key2=value1/value2) + sample3: + value: + options: (key1=10,key2=value2,key3=[val31,val32])
\ No newline at end of file diff --git a/docs/openapi/openapi.yml b/docs/openapi/openapi.yml index 1e7b38c9..66f2e6b1 100644 --- a/docs/openapi/openapi.yml +++ b/docs/openapi/openapi.yml @@ -139,8 +139,7 @@ paths: - $ref: 'components.yml#/components/parameters/cmHandleInPath' - $ref: 'components.yml#/components/parameters/resourceIdentifierInQuery' - $ref: 'components.yml#/components/parameters/acceptParamInHeader' - - $ref: 'components.yml#/components/parameters/fieldsParamInQuery' - - $ref: 'components.yml#/components/parameters/depthParamInQuery' + - $ref: 'components.yml#/components/parameters/optionsParamInQuery' requestBody: description: Operational body content: @@ -168,8 +167,7 @@ paths: - $ref: 'components.yml#/components/parameters/cmHandleInPath' - $ref: 'components.yml#/components/parameters/resourceIdentifierInQuery' - $ref: 'components.yml#/components/parameters/acceptParamInHeader' - - $ref: 'components.yml#/components/parameters/fieldsParamInQuery' - - $ref: 'components.yml#/components/parameters/depthParamInQuery' + - $ref: 'components.yml#/components/parameters/optionsParamInQuery' requestBody: description: Operational body content: diff --git a/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java b/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java index 2c2536ff..908ccbe2 100644 --- a/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java +++ b/src/main/java/org/onap/cps/ncmp/dmi/rest/controller/DmiRestController.java @@ -26,7 +26,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import javax.validation.Valid; -import javax.validation.constraints.Min; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.dmi.model.CmHandles; import org.onap.cps.ncmp.dmi.model.DataAccessReadRequest; @@ -114,56 +113,50 @@ public class DmiRestController implements DmiPluginApi, DmiPluginInternalApi { /** * This method fetches the resource for given cm handle using pass through operational. It filters the response on - * the basis of depth and field query parameters and returns response. + * the basis of options query parameters and returns response. * * @param resourceIdentifier resource identifier to fetch data * @param cmHandle cm handle identifier * @param dataAccessReadRequest data Access Read Request - * @param accept accept header parameter - * @param fields fields to filter the response data - * @param depth depth parameter for the response + * @param acceptParamInHeader accept header parameter + * @param optionsParamInQuery options query parameter * @return {@code ResponseEntity} response entity */ @Override public ResponseEntity<Object> getResourceDataOperationalForCmHandle(final String resourceIdentifier, final String cmHandle, final @Valid DataAccessReadRequest dataAccessReadRequest, - final String accept, - final @Valid String fields, - final @Min(1) @Valid Integer depth) { + final String acceptParamInHeader, + final @Valid String optionsParamInQuery) { final var modulesListAsJson = dmiService.getResourceDataOperationalForCmHandle(cmHandle, resourceIdentifier, - accept, - fields, - depth, + acceptParamInHeader, + optionsParamInQuery, dataAccessReadRequest.getCmHandleProperties()); return ResponseEntity.ok(modulesListAsJson); } /** * This method fetches the resource for given cm handle using pass through running. It filters the response on the - * basis of depth and field query parameters and returns response. + * basis of options query parameters and returns response. * * @param resourceIdentifier resource identifier to fetch data * @param cmHandle cm handle identifier * @param dataAccessReadRequest data Access Read Request - * @param accept accept header parameter - * @param fields fields to filter the response data - * @param depth depth parameter for the response + * @param acceptParamInHeader accept header parameter + * @param optionsParamInQuery options query parameter * @return {@code ResponseEntity} response entity */ @Override public ResponseEntity<Object> getResourceDataPassthroughRunningForCmHandle(final String resourceIdentifier, final String cmHandle, final @Valid DataAccessReadRequest dataAccessReadRequest, - final String accept, - final @Valid String fields, - final @Min(1) @Valid Integer depth) { + final String acceptParamInHeader, + final @Valid String optionsParamInQuery) { final var modulesListAsJson = dmiService.getResourceDataPassThroughRunningForCmHandle(cmHandle, resourceIdentifier, - accept, - fields, - depth, + acceptParamInHeader, + optionsParamInQuery, dataAccessReadRequest.getCmHandleProperties()); return ResponseEntity.ok(modulesListAsJson); } diff --git a/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java b/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java index 753810a2..c6910399 100644 --- a/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java +++ b/src/main/java/org/onap/cps/ncmp/dmi/service/DmiService.java @@ -61,40 +61,36 @@ public interface DmiService { /** * This method use to fetch the resource data from cm handle for datastore pass-through operational and resource - * Identifier. Fields and depths query parameter are used to filter the response from network resource. + * Identifier. Options query parameter are used to filter the response from network resource. * * @param cmHandle cm handle identifier * @param resourceIdentifier resource identifier - * @param acceptParam accept header parameter - * @param fieldsQuery fields query parameter - * @param depthQuery depth query parameter + * @param acceptParamInHeader accept header parameter + * @param optionsParamInQuery options query parameter * @param cmHandlePropertyMap cm handle properties * @return {@code Object} response from network function */ Object getResourceDataOperationalForCmHandle(@NotNull String cmHandle, @NotNull String resourceIdentifier, - String acceptParam, - String fieldsQuery, - Integer depthQuery, + String acceptParamInHeader, + String optionsParamInQuery, Map<String, String> cmHandlePropertyMap); /** * This method use to fetch the resource data from cm handle for datastore pass-through running and resource - * Identifier. Fields and depths query parameter are used to filter the response from network resource. + * Identifier. Options query parameter are used to filter the response from network resource. * * @param cmHandle cm handle identifier * @param resourceIdentifier resource identifier - * @param acceptParam accept header parameter - * @param fieldsQuery fields query parameter - * @param depthQuery depth query parameter + * @param acceptParamInHeader accept header parameter + * @param optionsParamInQuery options query parameter * @param cmHandlePropertyMap cm handle properties * @return {@code Object} response from network function */ Object getResourceDataPassThroughRunningForCmHandle(@NotNull String cmHandle, @NotNull String resourceIdentifier, - String acceptParam, - String fieldsQuery, - Integer depthQuery, + String acceptParamInHeader, + String optionsParamInQuery, Map<String, String> cmHandlePropertyMap); /** diff --git a/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java b/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java index 844cc4de..b66e5c1b 100644 --- a/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java +++ b/src/main/java/org/onap/cps/ncmp/dmi/service/DmiServiceImpl.java @@ -60,13 +60,12 @@ public class DmiServiceImpl implements DmiService { private NcmpRestClient ncmpRestClient; private ObjectMapper objectMapper; private DmiPluginProperties dmiPluginProperties; - private static final String CONTENT_QUERY_PASSTHROUGH_OPERATIONAL = "content=all"; - private static final String CONTENT_QUERY_PASSTHROUGH_RUNNING = "content=config"; + private static final String RESTCONF_CONTENT_PASSTHROUGH_OPERATIONAL_QUERY_PARAM = "content=all"; + private static final String REST_CONF_CONTENT_PASSTHROUGH_RUNNING_QUERY_PARAM = "content=config"; private static final String RESPONSE_CODE = "response code : "; private static final String MESSAGE = " message : "; private static final String IETF_NETCONF_MONITORING_OUTPUT = "ietf-netconf-monitoring:output"; - /** * Constructor. * @@ -178,34 +177,30 @@ public class DmiServiceImpl implements DmiService { @Override public Object getResourceDataOperationalForCmHandle(final @NotNull String cmHandle, final @NotNull String resourceIdentifier, - final String acceptParam, - final String fieldsQuery, - final Integer depthQuery, + final String acceptParamInHeader, + final String optionsParamInQuery, final Map<String, String> cmHandlePropertyMap) { // not using cmHandlePropertyMap of onap dmi impl , other dmi impl might use this. final ResponseEntity<String> responseEntity = sdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceIdentifier, - fieldsQuery, - depthQuery, - acceptParam, - CONTENT_QUERY_PASSTHROUGH_OPERATIONAL); + optionsParamInQuery, + acceptParamInHeader, + RESTCONF_CONTENT_PASSTHROUGH_OPERATIONAL_QUERY_PARAM); return prepareAndSendResponse(responseEntity, cmHandle); } @Override public Object getResourceDataPassThroughRunningForCmHandle(final @NotNull String cmHandle, final @NotNull String resourceIdentifier, - final String acceptParam, - final String fieldsQuery, - final Integer depthQuery, + final String acceptParamInHeader, + final String optionsParamInQuery, final Map<String, String> cmHandlePropertyMap) { // not using cmHandlePropertyMap of onap dmi impl , other dmi impl might use this. final ResponseEntity<String> responseEntity = sdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceIdentifier, - fieldsQuery, - depthQuery, - acceptParam, - CONTENT_QUERY_PASSTHROUGH_RUNNING); + optionsParamInQuery, + acceptParamInHeader, + REST_CONF_CONTENT_PASSTHROUGH_RUNNING_QUERY_PARAM); return prepareAndSendResponse(responseEntity, cmHandle); } diff --git a/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java b/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java index 73503d23..98371bf9 100644 --- a/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java +++ b/src/main/java/org/onap/cps/ncmp/dmi/service/operation/SdncOperations.java @@ -20,6 +20,7 @@ package org.onap.cps.ncmp.dmi.service.operation; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import org.apache.groovy.parser.antlr4.util.StringUtils; @@ -92,23 +93,22 @@ public class SdncOperations { * * @param nodeId network resource identifier * @param resourceId resource identifier - * @param fieldsValue fields query - * @param depthValue depth query - * @param acceptParam accept parameter + * @param optionsParamInQuery fields query + * @param acceptParamInHeader accept parameter + * @param restconfContentQueryParam restconf content query param * @return {@code ResponseEntity} response entity */ public ResponseEntity<String> getResouceDataForOperationalAndRunning(final String nodeId, final String resourceId, - final String fieldsValue, - final Integer depthValue, - final String acceptParam, - final String contentQuery) { + final String optionsParamInQuery, + final String acceptParamInHeader, + final String restconfContentQueryParam) { final String getResourceDataUrl = prepareResourceDataUrl(nodeId, resourceId, - getQueryList(fieldsValue, depthValue, contentQuery)); + buildQueryParamList(optionsParamInQuery, restconfContentQueryParam)); final HttpHeaders httpHeaders = new HttpHeaders(); - if (!StringUtils.isEmpty(acceptParam)) { - httpHeaders.set(HttpHeaders.ACCEPT, acceptParam); + if (!StringUtils.isEmpty(acceptParamInHeader)) { + httpHeaders.set(HttpHeaders.ACCEPT, acceptParamInHeader); } return sdncRestconfClient.getOperation(getResourceDataUrl, httpHeaders); } @@ -131,20 +131,25 @@ public class SdncOperations { } @NotNull - private List<String> getQueryList(final String fieldsValue, final Integer depthValue, final String contentQuery) { - final List<String> queryList = new LinkedList<>(); - if (!StringUtils.isEmpty(fieldsValue)) { - queryList.add("fields=" + fieldsValue); - } - if (depthValue != null) { - queryList.add("depth=" + depthValue); - } - if (!StringUtils.isEmpty(contentQuery)) { - queryList.add(contentQuery); + private List<String> buildQueryParamList(final String optionsParamInQuery, final String restconfContentQueryParam) { + final List<String> queryParamAsList = getOptionsParamAsList(optionsParamInQuery); + queryParamAsList.add(restconfContentQueryParam); + return queryParamAsList; + } + + private List<String> getOptionsParamAsList(final String optionsParamInQuery) { + final List<String> queryParamAsList = new LinkedList<>(); + if (!StringUtils.isEmpty(optionsParamInQuery)) { + final String tempQuery = stripParenthesisFromOptionsQuery(optionsParamInQuery); + queryParamAsList.addAll(Arrays.asList(tempQuery.split(","))); } - return queryList; + return queryParamAsList; } + @NotNull + private String stripParenthesisFromOptionsQuery(final String optionsParamInQuery) { + return optionsParamInQuery.substring(1, optionsParamInQuery.length() - 1); + } @NotNull private String prepareGetSchemaUrl(final String nodeId) { diff --git a/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy b/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy index 256db4e9..3bc1f3be 100644 --- a/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy +++ b/src/test/groovy/org/onap/cps/ncmp/dmi/rest/controller/DmiRestControllerSpec.groovy @@ -198,7 +198,7 @@ class DmiRestControllerSpec extends Specification { def 'Get resource data for pass-through operational from cm handle.'() { given: 'Get resource data url' def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-operational" + - "?resourceIdentifier=abc/xyz&fields=myfields&depth=5" + "?resourceIdentifier=parent/child&options=(fields=myfields,depth=5)" def json = '{"cmHandleProperties" : { "prop1" : "value1", "prop2" : "value2"}}' when: 'get resource data PUT api is invoked' def response = mvc.perform( @@ -209,10 +209,9 @@ class DmiRestControllerSpec extends Specification { response.status == HttpStatus.OK.value() and: 'dmi service called with get resource data for cm handle' 1 * mockDmiService.getResourceDataOperationalForCmHandle('some-cmHandle', - 'abc/xyz', + 'parent/child', 'application/json', - 'myfields', - 5, + '(fields=myfields,depth=5)', ['prop1': 'value1', 'prop2': 'value2']) } @@ -243,7 +242,7 @@ class DmiRestControllerSpec extends Specification { def 'Get resource data for pass-through running from cm handle with #scenario value in resource identifier param.'() { given: 'Get resource data url' def getResourceDataForCmHandleUrl = "${basePathV1}/ch/some-cmHandle/data/ds/ncmp-datastore:passthrough-running" + - "?resourceIdentifier="+resourceIdentifier+"&fields=testFields&depth=5" + "?resourceIdentifier="+resourceIdentifier+"&options=(fields=myfields,depth=5)" def json = '{"cmHandleProperties" : { "prop1" : "value1", "prop2" : "value2"}}' when: 'get resource data PUT api is invoked' def response = mvc.perform( @@ -256,8 +255,7 @@ class DmiRestControllerSpec extends Specification { 1 * mockDmiService.getResourceDataPassThroughRunningForCmHandle('some-cmHandle', resourceIdentifier, 'application/json', - 'testFields', - 5, + '(fields=myfields,depth=5)', ['prop1':'value1', 'prop2':'value2']) where: 'tokens are used in the resource identifier parameter' scenario | resourceIdentifier diff --git a/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy b/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy index 4c6bc750..9325d59b 100644 --- a/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy +++ b/src/test/groovy/org/onap/cps/ncmp/dmi/service/DmiServiceImplSpec.groovy @@ -210,15 +210,14 @@ class DmiServiceImplSpec extends Specification { def cmHandle = 'testCmHandle' def resourceId = 'testResourceId' def acceptHeaderParam = 'testAcceptParam' - def fieldsParam = 'testFields' - def depthParam = 10 + def optionsParam = '(fields=x/y/z,depth=10,test=abc)' def contentQuery = 'content=all' and: 'sdnc operation returns OK response' - mockSdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceId, fieldsParam, depthParam, acceptHeaderParam, contentQuery) >> new ResponseEntity<>('response json', HttpStatus.OK) + mockSdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceId, optionsParam, acceptHeaderParam, contentQuery) >> new ResponseEntity<>('response json', HttpStatus.OK) when: 'get resource data from cm handles service method invoked' def response = objectUnderTest.getResourceDataOperationalForCmHandle(cmHandle, resourceId, acceptHeaderParam, - fieldsParam, depthParam, null) + optionsParam, null) then: 'response have expected json' response == 'response json' } @@ -228,14 +227,13 @@ class DmiServiceImplSpec extends Specification { def cmHandle = 'testCmHandle' def resourceId = 'testResourceId' def acceptHeaderParam = 'testAcceptParam' - def fieldsParam = 'testFields' - def depthParam = 10 + def optionsParam = '(fields=x/y/z,depth=10,test=abc)' and: 'sdnc operation returns "NOT_FOUND" response' - mockSdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceId, fieldsParam, depthParam, acceptHeaderParam, _ as String) >> new ResponseEntity<>(HttpStatus.NOT_FOUND) + mockSdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceId, optionsParam, acceptHeaderParam, _ as String) >> new ResponseEntity<>(HttpStatus.NOT_FOUND) when: 'get resource data from cm handles service method invoked' objectUnderTest.getResourceDataOperationalForCmHandle(cmHandle, resourceId, acceptHeaderParam, - fieldsParam, depthParam, null) + optionsParam, null) then: 'resource data not found' thrown(ResourceDataNotFound.class) } @@ -245,16 +243,15 @@ class DmiServiceImplSpec extends Specification { def cmHandle = 'testCmHandle' def resourceId = 'testResourceId' def acceptHeaderParam = 'testAcceptParam' - def fieldsParam = 'testFields' - def depthParam = 10 + def optionsParam = '(fields=x/y/z,depth=10,test=abc)' def contentQuery = 'content=config' and: 'sdnc operation returns OK response' - mockSdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceId, fieldsParam, - depthParam, acceptHeaderParam, contentQuery) >> new ResponseEntity<>('response json', HttpStatus.OK) + mockSdncOperations.getResouceDataForOperationalAndRunning(cmHandle, resourceId, optionsParam, + acceptHeaderParam, contentQuery) >> new ResponseEntity<>('response json', HttpStatus.OK) when: 'get resource data from cm handles service method invoked' def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle(cmHandle, resourceId, acceptHeaderParam, - fieldsParam, depthParam, null) + optionsParam, null) then: 'response have expected json' response == 'response json' } diff --git a/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy b/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy index 95a9c0a7..4411971a 100644 --- a/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy +++ b/src/test/groovy/org/onap/cps/ncmp/dmi/service/operation/SdncOperationsSpec.groovy @@ -61,10 +61,10 @@ class SdncOperationsSpec extends Specification { def 'Get resource data from node to SDNC.'() { given: 'expected url, topology-id, sdncOperation object' - def expectedUrl = '/rests/data/network-topology:network-topology/topology=test-topology/node=node1/yang-ext:mount/testResourceId?fields=testFields&depth=10&content=testContent' + def expectedUrl = '/rests/data/network-topology:network-topology/topology=test-topology/node=node1/yang-ext:mount/testResourceId?a=1&b=2&content=testContent' when: 'called get modules from node' objectUnderTest.getResouceDataForOperationalAndRunning('node1', 'testResourceId', - 'testFields', 10, 'testAcceptParam', 'content=testContent') + '(a=1,b=2)', 'testAcceptParam', 'content=testContent') then: 'the get operation is executed with the correct URL' 1 * mockSdncRestClient.getOperation(expectedUrl, _ as HttpHeaders) } @@ -77,4 +77,33 @@ class SdncOperationsSpec extends Specification { then: 'the post operation is executed with the correct URL and data' 1 * mockSdncRestClient.postOperationWithJsonData(expectedUrl, 'requestData', _ as HttpHeaders) } + + def 'build query param list for SDNC where options contains a #scenario'() { + when: 'build query param list is called with #scenario' + def result = objectUnderTest.buildQueryParamList(optionsParamInQuery,'d=4') + then: 'result equals to expected result' + result == expectedResult + where: 'following parameters are used' + scenario | optionsParamInQuery || expectedResult + 'single key-value pair' | '(a=x)' || ['a=x','d=4'] + 'multiple key-value pairs'| '(a=x,b=y,c=z)' || ['a=x','b=y','c=z','d=4'] + '/ as special char' | '(a=x,b=y,c=t/z)' || ['a=x','b=y','c=t/z','d=4'] + '" as special char' | '(a=x,b=y,c="z")' || ['a=x','b=y','c="z"','d=4'] + '[] as special char' | '(a=x,b=y,c=[z])' || ['a=x','b=y','c=[z]','d=4'] + '= in value' | '(a=(x=y),b=x=y)' || ['a=(x=y)','b=x=y','d=4'] + } + + def 'options parameters contains a comma #scenario'() { + // https://jira.onap.org/browse/CPS-719 + when: 'build query param list is called with #scenario' + def result = objectUnderTest.buildQueryParamList(optionsParamInQuery,'d=4') + then: 'expect 2 elements from options +1 from content query param (2+1) = 3 elements' + def expectedNoOfElements = 3 + and: 'results contains more elements than expected' + result.size() > expectedNoOfElements + where: 'following parameters are used' + scenario | optionsParamInQuery + '"," in value' | '(a=(x,y),b=y)' + '"," in string value' | '(a="x,y",b=y)' + } } |