From 67a2fcc2599bd0d527197a33ff3d8b9ebb085689 Mon Sep 17 00:00:00 2001 From: Jannik Fried Date: Fri, 8 Mar 2024 07:55:41 +0100 Subject: [PATCH] Implements Patch Submodel Value Only (#230) * Adds Patch SubmodelValueOnly Endpoint to service and repository Signed-off-by: Jannik Fried * Adds missing JSON files Signed-off-by: Jannik Fried * Fixes tests Signed-off-by: Jannik Fried * Replaces wrong JSON file Signed-off-by: Jannik Fried * Applies changes according to code review Signed-off-by: Jannik Fried * Update Readme.md * Moves endpoint to correct row * Adapts test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepositoryTestSuite.java Signed-off-by: Jannik Fried --------- Signed-off-by: Jannik Fried --- .../backend/CrudSubmodelRepository.java | 7 + .../client/ConnectedSubmodelRepository.java | 5 + .../SubmodelRepository.java | 9 + ...modelRepositorySubmodelServiceWrapper.java | 4 + .../Readme.md | 7 +- .../AuthorizedSubmodelRepository.java | 92 ++-- ...AuthorizedSubmodelRepositoryTestSuite.java | 84 +++- .../authorization/newSubmodelValue.json | 148 ++++++ .../feature/mqtt/MqttSubmodelRepository.java | 6 + ...OperationDelegationSubmodelRepository.java | 5 + ...RegistryIntegrationSubmodelRepository.java | 5 + .../SubmodelRepositoryApiHTTPController.java | 16 +- .../http/SubmodelRepositoryHTTPApi.java | 23 +- .../InMemorySubmodelService.java | 5 + .../client/ConnectedSubmodelService.java | 5 + .../submodelservice/SubmodelService.java | 7 + .../http/SubmodelServiceHTTPApi.java | 19 +- .../SubmodelServiceHTTPApiController.java | 10 + ...lServiceSubmodelElementsTestSuiteHTTP.java | 14 + .../value/expectedNewSubmodelValue.json | 131 ++++++ .../resources/value/newSubmodelValue.json | 440 ++++++++++++++++++ 21 files changed, 968 insertions(+), 74 deletions(-) create mode 100644 basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/resources/authorization/newSubmodelValue.json create mode 100644 basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/expectedNewSubmodelValue.json create mode 100644 basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/newSubmodelValue.json diff --git a/basyx.submodelrepository/basyx.submodelrepository-backend/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/backend/CrudSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-backend/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/backend/CrudSubmodelRepository.java index 86cf704dc..c8ef370e7 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-backend/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/backend/CrudSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-backend/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/backend/CrudSubmodelRepository.java @@ -438,4 +438,11 @@ private void throwIfSubmodelDoesNotExist(String submodelId) { throw new ElementDoesNotExistException(submodelId); } + @Override + public void patchSubmodelElements(String submodelId, List submodelElementList) { + Submodel submodel = getSubmodel(submodelId); + submodel.setSubmodelElements(submodelElementList); + submodelBackend.save(submodel); + } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java index ee05f8a23..be13d0d3f 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/client/ConnectedSubmodelRepository.java @@ -240,4 +240,9 @@ private RuntimeException mapExceptionSubmodelAccess(String submodelId, ApiExcept return e; } + @Override + public void patchSubmodelElements(String submodelId, List submodelElementList) { + throw new FeatureNotImplementedException(); + } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelRepository.java index 4f0435681..2b0381ee0 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/SubmodelRepository.java @@ -263,4 +263,13 @@ public default String getName() { * @throws FileDoesNotExistException */ public void deleteFileValue(String submodelId, String idShortPath) throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException; + + /** + * Replaces the submodel elements in a submodel + * + * @param submodelId + * the Submodel id + * @param submodelElementList + */ + public void patchSubmodelElements(String submodelId, List submodelElementList); } diff --git a/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySubmodelServiceWrapper.java b/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySubmodelServiceWrapper.java index 4fd6ee792..4ffeeadb8 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySubmodelServiceWrapper.java +++ b/basyx.submodelrepository/basyx.submodelrepository-core/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/core/SubmodelRepositorySubmodelServiceWrapper.java @@ -110,5 +110,9 @@ public OperationVariable[] invokeOperation(String idShortPath, OperationVariable return repoApi.invokeOperation(submodelId, idShortPath, input); } + @Override + public void patchSubmodelElements(List submodelElementList) { + repoApi.patchSubmodelElements(submodelId, submodelElementList); + } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/Readme.md b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/Readme.md index 7d3e5a571..306f0fbab 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/Readme.md +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/Readme.md @@ -75,7 +75,7 @@ The role defines which role is allowed to perform the defined actions. The role The targetInformation defines coarse-grained control over the resource, you may define the submodelId and submodelElementIdShortPath with a wildcard (\*), it means the defined role x with action y can access any Submodel and any SubmodelElement on the repository. You can also define a specific Submodel Identifier in place of the wildcard (\*), then the role x with action y could be performed only on that particular Submodel. Similarly, you can define a specific SubmodelElement IdShort path, then you can only access the SubmodelElement corresponding to that IdShort path. It means that the whole Submodel GET request would not be possible if the IdShort path for a specific SubmodelElement is provided, because the requestor only has access for a specific SubmodelElement. -Note: The Action are fixed as of now and limited to (CREATE, READ, UPDATE, DELETE, and EXECUTE) but later user configurable mapping of these actions would be provided. +Note: The Action are fixed as of now and limited to (CREATE, READ, UPDATE, DELETE and EXECUTE) but later user configurable mapping of these actions would be provided. ## Action table for RBAC @@ -84,12 +84,11 @@ Below is a reference table that shows which actions are used in what endpoints o | Action | Endpoint | |---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | READ | GET /submodels
GET /submodels/{submodelIdentifier}
GET /submodels/{submodelIdentifier}/$value
GET /submodels/{submodelIdentifier}/$metadata
GET /submodels/{submodelIdentifier}/submodel-elements
GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}
GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value
GET /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment | -| CREATE | POST /submodels
| -| UPDATE | PUT /submodels/{submodelIdentifier}
PUT /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment
POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}
POST /submodels/{submodelIdentifier}/submodel-elements
PATCH /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value
DELETE /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}
DELETE /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment | +| CREATE | POST /submodels
| +| UPDATE | PUT /submodels/{submodelIdentifier}
PUT /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment
POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}
POST /submodels/{submodelIdentifier}/submodel-elements
PATCH /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/$value
PATCH /submodels/{submodelIdentifier}/$value
DELETE /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}
DELETE /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/attachment | | DELETE | DELETE /submodels/{submodelIdentifier} | | EXECUTE | POST /submodels/{submodelIdentifier}/submodel-elements/{idShortPath}/invoke
| - Note: The invoke operation is not supported currently for off-the-shelf component diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java index 1241fca45..270a15b7c 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java @@ -56,7 +56,6 @@ public class AuthorizedSubmodelRepository implements SubmodelRepository { private static final String ALL_ALLOWED_WILDCARD = "*"; private SubmodelRepository decorated; private RbacPermissionResolver permissionResolver; - public AuthorizedSubmodelRepository(SubmodelRepository decorated, RbacPermissionResolver permissionResolver) { this.decorated = decorated; @@ -66,174 +65,183 @@ public AuthorizedSubmodelRepository(SubmodelRepository decorated, RbacPermission @Override public CursorResult> getAllSubmodels(PaginationInfo pInfo) { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(ALL_ALLOWED_WILDCARD, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getAllSubmodels(pInfo); } @Override public Submodel getSubmodel(String submodelId) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getSubmodel(submodelId); } @Override public void updateSubmodel(String submodelId, Submodel submodel) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.updateSubmodel(submodelId, submodel); } @Override public void createSubmodel(Submodel submodel) throws CollidingIdentifierException { boolean isAuthorized = permissionResolver.hasPermission(Action.CREATE, new SubmodelTargetInformation(submodel.getId(), ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.createSubmodel(submodel); } @Override public void deleteSubmodel(String submodelId) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.DELETE, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.deleteSubmodel(submodelId); } @Override public CursorResult> getSubmodelElements(String submodelId, PaginationInfo pInfo) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getSubmodelElements(submodelId, pInfo); } @Override public SubmodelElement getSubmodelElement(String submodelId, String smeIdShortPath) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(submodelId, smeIdShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getSubmodelElement(submodelId, smeIdShortPath); } @Override public SubmodelElementValue getSubmodelElementValue(String submodelId, String smeIdShortPath) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(submodelId, smeIdShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getSubmodelElementValue(submodelId, smeIdShortPath); } @Override public void setSubmodelElementValue(String submodelId, String smeIdShortPath, SubmodelElementValue value) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, smeIdShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.setSubmodelElementValue(submodelId, smeIdShortPath, value); } @Override public void createSubmodelElement(String submodelId, SubmodelElement smElement) { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.createSubmodelElement(submodelId, smElement); } @Override public void createSubmodelElement(String submodelId, String idShortPath, SubmodelElement smElement) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, idShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.createSubmodelElement(submodelId, idShortPath, smElement); } - + @Override public void updateSubmodelElement(String submodelId, String idShortPath, SubmodelElement submodelElement) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, idShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.updateSubmodelElement(submodelId, idShortPath, submodelElement); } @Override public void deleteSubmodelElement(String submodelId, String idShortPath) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, idShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - - decorated.deleteSubmodelElement(submodelId, idShortPath); + + decorated.deleteSubmodelElement(submodelId, idShortPath); } @Override public OperationVariable[] invokeOperation(String submodelId, String idShortPath, OperationVariable[] input) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.EXECUTE, new SubmodelTargetInformation(submodelId, idShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.invokeOperation(submodelId, idShortPath, input); } @Override public SubmodelValueOnly getSubmodelByIdValueOnly(String submodelId) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getSubmodelByIdValueOnly(submodelId); } @Override public Submodel getSubmodelByIdMetadata(String submodelId) throws ElementDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getSubmodelByIdMetadata(submodelId); } @Override public File getFileByPathSubmodel(String submodelId, String idShortPath) throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.READ, new SubmodelTargetInformation(submodelId, idShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + return decorated.getFileByPathSubmodel(submodelId, idShortPath); } @Override public void setFileValue(String submodelId, String idShortPath, String fileName, InputStream inputStream) throws ElementDoesNotExistException, ElementNotAFileException { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, idShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.setFileValue(submodelId, idShortPath, fileName, inputStream); } @Override public void deleteFileValue(String submodelId, String idShortPath) throws ElementDoesNotExistException, ElementNotAFileException, FileDoesNotExistException { boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, idShortPath)); - + throwExceptionIfInsufficientPermission(isAuthorized); - + decorated.deleteFileValue(submodelId, idShortPath); } - + + @Override + public void patchSubmodelElements(String submodelId, List submodelElementList) { + boolean isAuthorized = permissionResolver.hasPermission(Action.UPDATE, new SubmodelTargetInformation(submodelId, ALL_ALLOWED_WILDCARD)); + + throwExceptionIfInsufficientPermission(isAuthorized); + + decorated.patchSubmodelElements(submodelId, submodelElementList); + } + private void throwExceptionIfInsufficientPermission(boolean isAuthorized) { if (!isAuthorized) throw new InsufficientPermissionException("Insufficient Permission: The current subject does not have the required permissions for this operation."); diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepositoryTestSuite.java b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepositoryTestSuite.java index ce9ed8d2a..d996d4cfc 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepositoryTestSuite.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepositoryTestSuite.java @@ -63,11 +63,11 @@ public class AuthorizedSubmodelRepositoryTestSuite { private static final String PARENT_SUBMODEL_ELEMENT_IDSHORT = "smc2"; private static final String SUBMODEL_ELEMENT_IDSHORT_PATH_2 = "smc1.specificSubmodelElementIdShort-2"; private static final String SUBMODEL_ELEMENT_IDSHORT_PATH = PARENT_SUBMODEL_ELEMENT_IDSHORT + ".specificSubmodelElementIdShort"; - + private static final String OPERATION_SQUARE_SUBMODEL_ELEMENT_IDSHORT = "square"; private static final String OPERATION_CUBE_SUBMODEL_ELEMENT_IDSHORT = "cube"; private static final String OPERATION_SUBMODEL_ELEMENT_IDSHORT_PATH = "smc1." + OPERATION_SQUARE_SUBMODEL_ELEMENT_IDSHORT; - + private static final String NEW_SUBMODEL_ELEMENT_IDSHORT = "specificSMEProperty"; private static final String NEW_SUBMODEL_ELEMENT_IDSHORT_PATH = PARENT_SUBMODEL_ELEMENT_IDSHORT + "." + NEW_SUBMODEL_ELEMENT_IDSHORT; private static final String FILE_SUBMODEL_ELEMENT_IDSHORT_PATH = PARENT_SUBMODEL_ELEMENT_IDSHORT + ".specificFileSubmodelElementIdShort"; @@ -85,14 +85,14 @@ public static void setUp() throws FileNotFoundException, IOException { createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getSubmodelJSONString(SUBMODEL_SIMPLE_1_JSON), getAdminAccessToken()); } - + @Test public void healthEndpointWithoutAuthorization() throws IOException, ParseException { String expectedHealthEndpointOutput = getJSONValueAsString("authorization/HealthOutput.json"); - + CloseableHttpResponse healthCheckResponse = BaSyxHttpTestUtils.executeGetOnURL(healthEndpointUrl); assertEquals(HttpStatus.OK.value(), healthCheckResponse.getCode()); - + BaSyxHttpTestUtils.assertSameJSONContent(expectedHealthEndpointOutput, BaSyxHttpTestUtils.getResponseAsString(healthCheckResponse)); } @@ -547,7 +547,6 @@ public void createSubmodelElementWithUnauthorizedSpecificSubmodel() throws IOExc assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); } - @Test public void createSubmodelElementWithNoAuthorization() throws IOException { String element = getJSONValueAsString("authorization/SubmodelElementNew.json"); @@ -599,7 +598,7 @@ public void updateSubmodelElementWithUnauthorizedSpecificSME() throws IOExceptio CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPutRequest(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID, FILE_SUBMODEL_ELEMENT_IDSHORT_PATH), element, accessToken); assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); } - + @Test public void updateSubmodelElementWithNoAuthorization() throws IOException { String element = getJSONValueAsString("authorization/FileSubmodelElementUpdate.json"); @@ -607,7 +606,7 @@ public void updateSubmodelElementWithNoAuthorization() throws IOException { CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPutRequest(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID, FILE_SUBMODEL_ELEMENT_IDSHORT_PATH), element); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); } - + @Test public void deleteSubmodelElementWithCorrectRoleAndPermission() throws IOException { createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getSubmodelJSONString(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); @@ -639,45 +638,44 @@ public void deleteSubmodelElementWithCorrectRoleAndSpecificSMEPermission() throw @Test public void deleteSubmodelElementWithCorrectRoleAndUnauthorizedSpecificSME() throws IOException { createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getSubmodelJSONString(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); - + String accessToken = getAccessToken(DummyCredentialStore.BASYX_SME_UPDATER_THREE_CREDENTIAL); CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID_2, SUBMODEL_ELEMENT_IDSHORT_PATH), accessToken); assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); assertElementExistsOnServer(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID_2, SUBMODEL_ELEMENT_IDSHORT_PATH_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); - + deleteElementWithAuthorization(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); } @Test public void deleteSubmodelElementWithInsufficientPermissionRole() throws IOException { createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getSubmodelJSONString(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); - + String accessToken = getAccessToken(DummyCredentialStore.BASYX_CREATOR_CREDENTIAL); CloseableHttpResponse retrievalResponse = deleteElementWithAuthorization(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID_2, SUBMODEL_ELEMENT_IDSHORT_PATH_2), accessToken); assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); assertElementExistsOnServer(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID_2, SUBMODEL_ELEMENT_IDSHORT_PATH_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); - + deleteElementWithAuthorization(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); } @Test public void deleteSubmodelElementWithNoAuthorization() throws IOException { createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getSubmodelJSONString(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); - + CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID_2, SUBMODEL_ELEMENT_IDSHORT_PATH_2)); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); assertElementExistsOnServer(getSpecificSubmodelElementAccessURL(SPECIFIC_SUBMODEL_ID_2, SUBMODEL_ELEMENT_IDSHORT_PATH_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); - + deleteElementWithAuthorization(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); } - @Test @Ignore public void invokeWithCorrectRoleAndPermission() throws IOException, ParseException { @@ -736,7 +734,7 @@ public void invokeWithUnauthorizedSpecificSME() throws IOException { @Ignore public void invokeWithNoAuthorization() throws IOException { String parameters = getJSONValueAsString("authorization/parameters.json"); - + CloseableHttpResponse retrievalResponse = requestOperationInvocationNoAuthorization(getInvocationURL(SPECIFIC_SUBMODEL_ID, OPERATION_SUBMODEL_ELEMENT_IDSHORT_PATH), parameters); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); } @@ -787,7 +785,6 @@ public void getSubmodelValueOnlyWithNoAuthorization() throws IOException { assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); } - @Test public void getSubmodelByMetadataWithCorrectRoleAndPermission() throws IOException { DummyCredential dummyCredential = DummyCredentialStore.BASYX_READER_CREDENTIAL; @@ -834,7 +831,6 @@ public void getSubmodelByMetadataWithNoAuthorization() throws IOException { assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); } - @Test public void getFileWithCorrectRoleAndPermission() throws IOException { createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getSubmodelJSONString(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); @@ -909,7 +905,7 @@ public void getFileWithNoAuthorization() throws IOException { CloseableHttpResponse retrievalResponse = getElementWithNoAuthorization(getSMEFileDownloadURL(SPECIFIC_SUBMODEL_ID_2, FILE_SUBMODEL_ELEMENT_IDSHORT_PATH)); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); - + deleteElementWithAuthorization(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); } @@ -989,7 +985,6 @@ public void setFileWithNoAuthorization() throws IOException { deleteElementWithAuthorization(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); } - @Test public void deleteFileWithCorrectRoleAndPermission() throws IOException { createElementOnRepositoryWithAuthorization(createSubmodelRepositoryUrl(submodelRepositoryBaseUrl), getSubmodelJSONString(SUBMODEL_SIMPLE_2_JSON), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); @@ -1068,10 +1063,49 @@ public void deleteFileWithNoAuthorization() throws IOException { CloseableHttpResponse retrievalResponse = deleteElementWithNoAuthorization(getSMEFileDownloadURL(SPECIFIC_SUBMODEL_ID_2, FILE_SUBMODEL_ELEMENT_IDSHORT_PATH)); assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); - + deleteElementWithAuthorization(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID_2), getAccessToken(DummyCredentialStore.ADMIN_CREDENTIAL)); } + @Test + public void patchSubmodelValueWithCorrectRoleAndPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPatchRequest(getSpecificSubmodelValueOnlyAccessURL(SPECIFIC_SUBMODEL_ID), getJSONValueAsString("authorization/newSubmodelValue.json"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void patchSubmodelValueWithCorrectRoleAndSpecificSubmodelPermission() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_UPDATER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPatchRequest(getSpecificSubmodelValueOnlyAccessURL(SPECIFIC_SUBMODEL_ID), getJSONValueAsString("authorization/newSubmodelValue.json"), accessToken); + assertEquals(HttpStatus.NO_CONTENT.value(), retrievalResponse.getCode()); + } + + @Test + public void patchSubmodelValueWithCorrectRoleAndUnauthorizedSpecificSubmodel() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_TWO_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPatchRequest(getSpecificSubmodelValueOnlyAccessURL(SPECIFIC_SUBMODEL_ID), getJSONValueAsString("authorization/newSubmodelValue.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void patchSubmodelValueWithInsufficientPermissionRole() throws IOException { + String accessToken = getAccessToken(DummyCredentialStore.BASYX_READER_CREDENTIAL); + + CloseableHttpResponse retrievalResponse = updateElementWithAuthorizationPatchRequest(getSpecificSubmodelValueOnlyAccessURL(SPECIFIC_SUBMODEL_ID), getJSONValueAsString("authorization/newSubmodelValue.json"), accessToken); + assertEquals(HttpStatus.FORBIDDEN.value(), retrievalResponse.getCode()); + } + + @Test + public void patchSubmodelValueWithNoAuthorization() throws IOException { + CloseableHttpResponse retrievalResponse = updateElementWithNoAuthorizationPatchRequest(getSpecificSubmodelAccessURL(SPECIFIC_SUBMODEL_ID), getSubmodelJSONString(SUBMODEL_SIMPLE_1_JSON)); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), retrievalResponse.getCode()); + } + private CloseableHttpResponse uploadFileToSubmodelElementWithAuthorization(String url, String fileName, java.io.File file, String accessToken) throws IOException { CloseableHttpClient client = HttpClients.createDefault(); @@ -1201,6 +1235,14 @@ private CloseableHttpResponse updateElementWithNoAuthorizationPutRequest(String return BaSyxHttpTestUtils.executePutOnURL(url, content); } + private CloseableHttpResponse updateElementWithAuthorizationPatchRequest(String url, String content, String accessToken) throws IOException { + return BaSyxHttpTestUtils.executeAuthorizedPatchOnURL(url, content, accessToken); + } + + private CloseableHttpResponse updateElementWithNoAuthorizationPatchRequest(String url, String content) throws IOException { + return BaSyxHttpTestUtils.executePatchOnURL(url, content); + } + private CloseableHttpResponse deleteElementWithAuthorization(String url, String accessToken) throws IOException { return BaSyxHttpTestUtils.executeAuthorizedDeleteOnURL(url, accessToken); } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/resources/authorization/newSubmodelValue.json b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/resources/authorization/newSubmodelValue.json new file mode 100644 index 000000000..94281eab0 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/test/resources/authorization/newSubmodelValue.json @@ -0,0 +1,148 @@ +[ + { + "modelType": "Property", + "value": "5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "MaxRotationSpeed", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Range", + "max": "300", + "min": "200", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "RotationSpeedRange", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "SubmodelElementCollection", + "category": "PARAMETER", + "idShort": "smc1", + "value": [ + { + "modelType": "File", + "contentType": "application/json", + "value": "testFile.json", + "category": "PARAMETER", + "idShort": "FileData", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Property", + "value": "5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "specificSubmodelElementIdShort", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + } + ], + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "SubmodelElementCollection", + "category": "PARAMETER", + "idShort": "smc2", + "value": [ + { + "modelType": "File", + "contentType": "application/json", + "value": "testFile.json", + "category": "PARAMETER", + "idShort": "specificFileSubmodelElementIdShort", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Range", + "max": "300", + "min": "200", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "specificSubmodelElementIdShort", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Property", + "value": "5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "specificSubmodelElementIdShort-2", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + } + ], + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + } + ] \ No newline at end of file diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java index ea1c76331..aee137d32 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java @@ -209,4 +209,10 @@ public void setFileValue(String submodelId, String idShortPath, String fileName, decorated.setFileValue(submodelId, idShortPath, fileName, inputStream); } + @Override + public void patchSubmodelElements(String submodelId, List submodelElementList) { + // TODO: Eventing + decorated.patchSubmodelElements(submodelId, submodelElementList); + } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java index 8f215660b..5efbc202f 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java @@ -168,5 +168,10 @@ public void deleteFileValue(String submodelId, String idShortPath) throws Elemen public String getName() { return decorated.getName(); } + + @Override + public void patchSubmodelElements(String submodelId, List submodelElementList) { + decorated.patchSubmodelElements(submodelId, submodelElementList); + } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java index 2fa2dad07..10ddf2bb9 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java @@ -209,4 +209,9 @@ private boolean submodelExistsOnRegistry(String submodelId, SubmodelRegistryApi } } + @Override + public void patchSubmodelElements(String submodelId, List submodelElementList) { + decorated.patchSubmodelElements(submodelId, submodelElementList); + } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryApiHTTPController.java b/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryApiHTTPController.java index a2d57c544..3d111664d 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryApiHTTPController.java +++ b/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryApiHTTPController.java @@ -102,7 +102,8 @@ public ResponseEntity deleteSubmodelElementByPathSubmodelRepo( } @Override - public ResponseEntity getAllSubmodels(@Base64UrlEncodedIdentifierSize(min = 1, max = 3072) @Valid Base64UrlEncodedIdentifier semanticId, @Valid String idShort, @Min(1) @Valid Integer limit, @Valid Base64UrlEncodedCursor cursor, @Valid String level, @Valid String extent) { + public ResponseEntity getAllSubmodels(@Base64UrlEncodedIdentifierSize(min = 1, max = 3072) @Valid Base64UrlEncodedIdentifier semanticId, @Valid String idShort, @Min(1) @Valid Integer limit, + @Valid Base64UrlEncodedCursor cursor, @Valid String level, @Valid String extent) { if (limit == null) { limit = 100; } @@ -176,7 +177,7 @@ public ResponseEntity postSubmodelElementSubmodelRepo(Base64Url repository.createSubmodelElement(submodelIdentifier.getIdentifier(), body); return new ResponseEntity(HttpStatus.CREATED); } - + @Override public ResponseEntity putSubmodelElementByPathSubmodelRepo( @Parameter(in = ParameterIn.PATH, description = "The Submodel’s unique id (UTF8-BASE64-URL-encoded)", required = true, schema = @Schema()) @PathVariable("submodelIdentifier") Base64UrlEncodedIdentifier submodelIdentifier, @@ -248,6 +249,12 @@ public ResponseEntity deleteFileByPath(Base64UrlEncodedIdentifier submodel } } + @Override + public ResponseEntity patchSubmodelByIdValueOnly(Base64UrlEncodedIdentifier submodelIdentifier, @Valid List body, @Valid String level) { + repository.patchSubmodelElements(submodelIdentifier.getIdentifier(), body); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + private ResponseEntity handleSubmodelElementValueSetRequest(Base64UrlEncodedIdentifier submodelIdentifier, String idShortPath, SubmodelElementValue body) { repository.setSubmodelElementValue(submodelIdentifier.getIdentifier(), idShortPath, body); return new ResponseEntity(HttpStatus.NO_CONTENT); @@ -272,9 +279,7 @@ public ResponseEntity invokeOperationSubmodelRepo(Base64UrlEnco } private OperationResult createOperationResult(OperationVariable[] result) { - return new DefaultOperationResult.Builder() - .outputArguments(Arrays.asList(result)) - .build(); + return new DefaultOperationResult.Builder().outputArguments(Arrays.asList(result)).build(); } private String getEncodedCursorFromCursorResult(CursorResult cursorResult) { @@ -296,4 +301,5 @@ private void closeInputStream(InputStream fileInputstream) { e.printStackTrace(); } } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryHTTPApi.java b/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryHTTPApi.java index 3bf3b9915..ef9831888 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryHTTPApi.java +++ b/basyx.submodelrepository/basyx.submodelrepository-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/http/SubmodelRepositoryHTTPApi.java @@ -450,7 +450,7 @@ ResponseEntity invokeOperationSubmodelRepo( @Parameter(in = ParameterIn.PATH, description = "IdShort path to the submodel element (dot-separated)", required = true, schema = @Schema()) @PathVariable("idShortPath") String idShortPath, @Parameter(in = ParameterIn.DEFAULT, description = "Operation request object", required = true, schema = @Schema()) @Valid @RequestBody OperationRequest body, @Parameter(in = ParameterIn.QUERY, description = "Determines whether an operation invocation is performed asynchronously or synchronously", schema = @Schema(defaultValue = "false")) @Valid @RequestParam(value = "async", required = false, defaultValue = "false") Boolean async); - + @Operation(summary = "Updates an existing submodel element at a specified path within submodel elements hierarchy", description = "", tags = { "Submodel Repository API" }) @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Submodel element updated successfully"), @@ -473,4 +473,25 @@ ResponseEntity putSubmodelElementByPathSubmodelRepo( @Parameter(in = ParameterIn.QUERY, description = "Determines the structural depth of the respective resource content", schema = @Schema(allowableValues = { "deep" }, defaultValue = "deep")) @Valid @RequestParam(value = "level", required = false, defaultValue = "deep") String level); + @Operation(summary = "Updates the values of an existing Submodel", description = "", tags = { "Submodel Repository API" }) + @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Submodel updated successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation = SubmodelValueOnly.class))), + + @ApiResponse(responseCode = "400", description = "Bad Request, e.g. the request parameters of the format of the request body is wrong.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + @ApiResponse(responseCode = "401", description = "Unauthorized, e.g. the server refused the authorization attempt.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + @ApiResponse(responseCode = "403", description = "Forbidden", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + @ApiResponse(responseCode = "404", description = "Not Found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + }) + @RequestMapping(value = "/submodels/{submodelIdentifier}/$value", produces = { "application/json" }, method = RequestMethod.PATCH) + ResponseEntity patchSubmodelByIdValueOnly( + @Parameter(in = ParameterIn.PATH, description = "The Submodel’s unique id (UTF8-BASE64-URL-encoded)", required = true, schema = @Schema()) @PathVariable("submodelIdentifier") Base64UrlEncodedIdentifier submodelIdentifier, + @Parameter(in = ParameterIn.DEFAULT, description = "Submodel object in its ValueOnly representation", required = false, schema = @Schema()) @Valid @RequestBody List body, + @Parameter(in = ParameterIn.QUERY, description = "Determines the structural depth of the respective resource content", schema = @Schema(allowableValues = { "deep", + "core" }, defaultValue = "deep")) @Valid @RequestParam(value = "level", required = false, defaultValue = "deep") String level); + } diff --git a/basyx.submodelservice/basyx.submodelservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/InMemorySubmodelService.java b/basyx.submodelservice/basyx.submodelservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/InMemorySubmodelService.java index c517f8027..f3c1aa9e6 100644 --- a/basyx.submodelservice/basyx.submodelservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/InMemorySubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-backend-inmemory/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/InMemorySubmodelService.java @@ -221,4 +221,9 @@ public OperationVariable[] invokeOperation(String idShortPath, OperationVariable private String getFullIdShortPath(String idShortPath, String submodelElementId) { return idShortPath + "." + submodelElementId; } + + @Override + public void patchSubmodelElements(List submodelElementList) { + this.submodel.setSubmodelElements(submodelElementList); + } } diff --git a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java index 55198707b..02068b489 100644 --- a/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-client/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/client/ConnectedSubmodelService.java @@ -143,4 +143,9 @@ public OperationVariable[] invokeOperation(String idShortPath, OperationVariable throw new FeatureNotImplementedException(); } + @Override + public void patchSubmodelElements(List submodelElementList) { + throw new FeatureNotImplementedException(); + } + } diff --git a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelService.java b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelService.java index d8c9e2ffa..8bf2a383c 100644 --- a/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelService.java +++ b/basyx.submodelservice/basyx.submodelservice-core/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/SubmodelService.java @@ -123,6 +123,13 @@ public interface SubmodelService { */ public void deleteSubmodelElement(String idShortPath) throws ElementDoesNotExistException; + /** + * Replaces the submodel elements in a submodel + * + * @param submodelElementList + */ + public void patchSubmodelElements(List submodelElementList); + /** * Invokes an operation * diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApi.java b/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApi.java index 00f4c0dae..8e12e20eb 100644 --- a/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApi.java +++ b/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApi.java @@ -25,6 +25,8 @@ package org.eclipse.digitaltwin.basyx.submodelservice.http; +import java.util.List; + import org.eclipse.digitaltwin.aas4j.v3.model.OperationRequest; import org.eclipse.digitaltwin.aas4j.v3.model.OperationResult; import org.eclipse.digitaltwin.aas4j.v3.model.Result; @@ -287,7 +289,7 @@ ResponseEntity postSubmodelElementByPath( ResponseEntity invokeOperation( @Parameter(in = ParameterIn.PATH, description = "IdShort path to the submodel element (dot-separated)", required = true, schema = @Schema()) @PathVariable("idShortPath") String idShortPath, @Parameter(in = ParameterIn.DEFAULT, description = "Operation request object", required = true, schema = @Schema()) @Valid @RequestBody OperationRequest body); - + @Operation(summary = "Updates an existing submodel element at a specified path within submodel elements hierarchy", description = "", tags = { "Submodel API" }) @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Submodel element updated successfully"), @@ -305,4 +307,19 @@ ResponseEntity putSubmodelElementByPath(@Parameter(in = ParameterIn.PATH, @Parameter(in = ParameterIn.DEFAULT, description = "Requested submodel element", required = true, schema = @Schema()) @Valid @RequestBody SubmodelElement body, @Parameter(in = ParameterIn.QUERY, description = "Determines the structural depth of the respective resource content", schema = @Schema(allowableValues = { "deep" }, defaultValue = "deep")) @Valid @RequestParam(value = "level", required = false, defaultValue = "deep") String level); + + @Operation(summary = "Updates the values of the Submodel", description = "", tags = { "Submodel API" }) + @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Submodel updated successfully"), + + @ApiResponse(responseCode = "400", description = "Bad Request, e.g. the request parameters of the format of the request body is wrong.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + @ApiResponse(responseCode = "403", description = "Forbidden", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))), + + @ApiResponse(responseCode = "200", description = "Default error handling for unmentioned status codes", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Result.class))) }) + @RequestMapping(value = "/submodel/$value", produces = { "application/json" }, consumes = { "application/json" }, method = RequestMethod.PATCH) + ResponseEntity patchSubmodelValueOnly(@Parameter(in = ParameterIn.DEFAULT, description = "Submodel object in its ValueOnly representation", required = false, schema = @Schema()) @Valid @RequestBody List body, + @Parameter(in = ParameterIn.QUERY, description = "Determines the structural depth of the respective resource content", schema = @Schema(allowableValues = { + "deep" }, defaultValue = "deep")) @Valid @RequestParam(value = "level", required = false, defaultValue = "deep") String level); } diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApiController.java b/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApiController.java index 4b3b1ef4d..67b340fc9 100644 --- a/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApiController.java +++ b/basyx.submodelservice/basyx.submodelservice-http/src/main/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceHTTPApiController.java @@ -214,6 +214,16 @@ public ResponseEntity putSubmodelElementByPath( return new ResponseEntity<>(HttpStatus.NO_CONTENT); } + @Override + public ResponseEntity patchSubmodelValueOnly(@Parameter(in = ParameterIn.DEFAULT, description = "Requested submodel element", required = true, schema = @Schema()) @Valid @RequestBody List body, + @Parameter(in = ParameterIn.QUERY, description = "Determines the structural depth of the respective resource content", schema = @Schema(allowableValues = { + "deep" }, defaultValue = "deep")) @Valid @RequestParam(value = "level", required = false, defaultValue = "deep") String level) { + + service.patchSubmodelElements(body); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + @Override public ResponseEntity invokeOperation( @Parameter(in = ParameterIn.PATH, description = "IdShort path to the submodel element (dot-separated)", required = true, schema = @Schema()) @PathVariable("idShortPath") String idShortPath, diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelElementsTestSuiteHTTP.java b/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelElementsTestSuiteHTTP.java index f2d6f9047..16750e70d 100644 --- a/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelElementsTestSuiteHTTP.java +++ b/basyx.submodelservice/basyx.submodelservice-http/src/test/java/org/eclipse/digitaltwin/basyx/submodelservice/http/SubmodelServiceSubmodelElementsTestSuiteHTTP.java @@ -479,6 +479,20 @@ public void getValuesSerializationOfSubmodel() throws IOException, ParseExceptio BaSyxHttpTestUtils.assertSameJSONContent(expectedValue, BaSyxHttpTestUtils.getResponseAsString(response)); } + @Test + public void patchSubmodelValues() throws FileNotFoundException, IOException, ParseException { + String patch = getJSONValueAsString("value/newSubmodelValue.json"); + + CloseableHttpResponse patchResponse = BaSyxHttpTestUtils.executePatchOnURL(createSubmodelValueURL(), patch); + assertEquals(HttpStatus.NO_CONTENT.value(), patchResponse.getCode()); + + CloseableHttpResponse getResponse = BaSyxHttpTestUtils.executeGetOnURL(createSubmodelValueURL()); + + String expected = getJSONValueAsString("value/expectedNewSubmodelValue.json"); + + BaSyxHttpTestUtils.assertSameJSONContent(expected, BaSyxHttpTestUtils.getResponseAsString(getResponse)); + } + @Test public void invokeOperation() throws FileNotFoundException, IOException, ParseException { String parameters = getJSONValueAsString("operation/parameters.json"); diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/expectedNewSubmodelValue.json b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/expectedNewSubmodelValue.json new file mode 100644 index 000000000..f5e5cf213 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/expectedNewSubmodelValue.json @@ -0,0 +1,131 @@ +{ + "new_MultiLanguage": [ + { + "en": "Hello" + }, + { + "de": "Hallo" + } + ], + "new_SimpleList": [ + "new_4370" + ], + "new_SubmodelElementCollection": [ + { + "new_FileData": { + "contentType": "application/json", + "value": "new_testFile.json" + } + }, + { + "new_MaxRotationSpeed": "new_5000" + } + ], + "new_BlobData": { + "contentType": "application/xml", + "value": "Test content of XML file" + }, + "new_MaxRotationSpeed": "new_5000", + "new_RelationshipElement": { + "first": { + "type": "ModelReference", + "keys": [ + { + "type": "DataElement", + "value": "new_DataElement" + } + ] + }, + "second": { + "type": "ExternalReference", + "keys": [ + { + "type": "BasicEventElement", + "value": "new_BasicEventElement" + } + ] + } + }, + "new_SubmodelElementList": [ + { + "min": 200, + "max": 300 + }, + "new_5000" + ], + "new_ReferenceElement": { + "type": "ModelReference", + "keys": [ + { + "type": "DataElement", + "value": "new_DataElement" + } + ] + }, + "new_SimpleCollection": [ + { + "new_MyFirstSubmodelElement": "new_4370" + } + ], + "new_FileData": { + "contentType": "application/json", + "value": "new_testFile.json" + }, + "new_AnnotatedRelationshipElement": { + "first": { + "type": "ModelReference", + "keys": [ + { + "type": "DataElement", + "value": "new_DataElement" + } + ] + }, + "second": { + "type": "ExternalReference", + "keys": [ + { + "type": "BasicEventElement", + "value": "new_BasicEventElement" + } + ] + }, + "annotation": [ + { + "new_MaxRotationSpeed": "new_5000" + }, + { + "new_RotationSpeedRange": { + "min": 200, + "max": 300 + } + } + ] + }, + "new_EntityData": { + "statements": [ + { + "new_MaxRotationSpeed": "new_5000" + }, + { + "new_RotationSpeedRange": { + "min": 200, + "max": 300 + } + } + ], + "entityType": "CoManagedEntity", + "globalAssetId": "globalAssetID", + "specificAssetIds": [ + { + "specificAssetIdName": "new_specificValue" + } + ] + }, + "new_RotationSpeed": "new_4370", + "new_elementToDelete": "new_4370", + "new_RotationSpeedRange": { + "min": 200, + "max": 300 + } +} \ No newline at end of file diff --git a/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/newSubmodelValue.json b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/newSubmodelValue.json new file mode 100644 index 000000000..7d79ef4f3 --- /dev/null +++ b/basyx.submodelservice/basyx.submodelservice-http/src/test/resources/value/newSubmodelValue.json @@ -0,0 +1,440 @@ +[ + { + "modelType": "AnnotatedRelationshipElement", + "category": "PARAMETER", + "idShort": "new_AnnotatedRelationshipElement", + "first": { + "keys": [ + { + "type": "DataElement", + "value": "new_DataElement" + } + ], + "type": "ModelReference" + }, + "second": { + "keys": [ + { + "type": "BasicEventElement", + "value": "new_BasicEventElement" + } + ], + "type": "ExternalReference" + }, + "annotations": [ + { + "modelType": "Property", + "value": "new_5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_MaxRotationSpeed", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Range", + "max": "300", + "min": "200", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_RotationSpeedRange", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + } + ], + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Blob", + "contentType": "application/xml", + "value": "VGVzdCBjb250ZW50IG9mIFhNTCBmaWxl", + "category": "PARAMETER", + "idShort": "new_BlobData", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Entity", + "entityType": "CoManagedEntity", + "specificAssetIds": [ + { + "name": "specificAssetIdName", + "value": "new_specificValue" + } + ], + "statements": [ + { + "modelType": "Property", + "value": "new_5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_MaxRotationSpeed", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Range", + "max": "300", + "min": "200", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_RotationSpeedRange", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + } + ], + "category": "Entity", + "idShort": "new_EntityData", + "globalAssetId": "globalAssetID", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "File", + "contentType": "application/json", + "value": "new_testFile.json", + "category": "PARAMETER", + "idShort": "new_FileData", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Property", + "value": "new_5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_MaxRotationSpeed", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "MultiLanguageProperty", + "category": "PARAM", + "idShort": "new_MultiLanguage", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + }, + "value": [ + { + "language": "en", + "text": "Hello" + }, + { + "language": "de", + "text": "Hallo" + } + ] + }, + { + "modelType": "ReferenceElement", + "category": "PARAMETER", + "idShort": "new_ReferenceElement", + "value": { + "keys": [ + { + "type": "DataElement", + "value": "new_DataElement" + } + ], + "type": "ModelReference" + }, + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "RelationshipElement", + "category": "PARAMETER", + "idShort": "new_RelationshipElement", + "first": { + "keys": [ + { + "type": "DataElement", + "value": "new_DataElement" + } + ], + "type": "ModelReference" + }, + "second": { + "keys": [ + { + "type": "BasicEventElement", + "value": "new_BasicEventElement" + } + ], + "type": "ExternalReference" + }, + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Property", + "value": "new_4370", + "valueType": "xs:integer", + "category": "VARIABLE", + "idShort": "new_RotationSpeed", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_http://customer.com/cd/1/1/18EBD56F6B43D895" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Range", + "max": "300", + "min": "200", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_RotationSpeedRange", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "SubmodelElementCollection", + "idShort": "new_SimpleCollection", + "value": [ + { + "modelType": "Property", + "value": "new_4370", + "valueType": "xs:integer", + "category": "VARIABLE", + "idShort": "new_MyFirstSubmodelElement" + } + ] + }, + { + "modelType": "SubmodelElementList", + "idShort": "new_SimpleList", + "orderRelevant": true, + "value": [ + { + "modelType": "Property", + "value": "new_4370", + "valueType": "xs:integer", + "category": "VARIABLE", + "idShort": "new_MySecondSubmodelElement" + } + ] + }, + { + "modelType": "SubmodelElementCollection", + "category": "PARAMETER", + "idShort": "new_SubmodelElementCollection", + "value": [ + { + "modelType": "File", + "contentType": "application/json", + "value": "new_testFile.json", + "category": "PARAMETER", + "idShort": "new_FileData", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Property", + "value": "new_5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_MaxRotationSpeed", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + } + ], + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "SubmodelElementList", + "category": "PARAMETER", + "idShort": "new_SubmodelElementList", + "orderRelevant": true, + "value": [ + { + "modelType": "Range", + "max": "300", + "min": "200", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_RotationSpeedRange", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Property", + "value": "new_5000", + "valueType": "xs:integer", + "category": "PARAMETER", + "idShort": "new_MaxRotationSpeed", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + } + ], + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "new_0173-1#02-BAA120#008" + } + ], + "type": "ExternalReference" + } + }, + { + "modelType": "Property", + "value": "new_4370", + "valueType": "xs:integer", + "category": "VARIABLE", + "idShort": "new_elementToDelete" + }, + { + "modelType": "Operation", + "idShort": "new_square", + "inputVariables": [ + { + "value": { + "modelType": "Property", + "valueType": "xs:int", + "idShort": "new_input" + } + } + ], + "outputVariables": [ + { + "value": { + "modelType": "Property", + "valueType": "xs:int", + "idShort": "new_result" + } + } + ] + } +] \ No newline at end of file