diff --git a/.github/workflows/basyx_test.yml b/.github/workflows/basyx_test.yml index 27e3943b7..c67070f00 100644 --- a/.github/workflows/basyx_test.yml +++ b/.github/workflows/basyx_test.yml @@ -736,6 +736,39 @@ jobs: exit 1; fi + test-basyx-aasdigitaltwinregistry: + runs-on: ubuntu-latest + name: AAS Digital Twin Registry Service Test + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'adopt' + cache: maven + + - name: Build BaSyx + run: mvn clean install ${MVN_ARGS_BUILD_BASYX} + + - name: Test AAS Digital Twin Registry Service + run: mvn test -f "basyx.aasdigitaltwinregistry/pom.xml" + + - name: Fail if no Surefire/Failsafe reports found + run: | + if ! find . -type f \( -path "*/target/surefire-reports/*.xml" -o -path "*/target/failsafe-reports/*.xml" \) | grep -q .; then + echo "No Surefire or Failsafe test reports found. Failing CI."; + exit 1; + fi + + - name: Fail if no Surefire/Failsafe reports found + run: | + if ! find . -type f \( -path "*/target/surefire-reports/*.xml" -o -path "*/target/failsafe-reports/*.xml" \) | grep -q .; then + echo "No Surefire or Failsafe test reports found. Failing CI."; + exit 1; + fi + - name: Stop environment if: always() run: docker compose --project-directory ./ci down diff --git a/.github/workflows/docker-milestone-release.yml b/.github/workflows/docker-milestone-release.yml index 6955f66be..2f0dc8c94 100644 --- a/.github/workflows/docker-milestone-release.yml +++ b/.github/workflows/docker-milestone-release.yml @@ -43,6 +43,8 @@ jobs: path: basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/main/docker - name: submodel-registry-log-mongodb path: basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/main/docker + - name: digitaltwinregistry + path: basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component steps: - name: Checkout Code diff --git a/.github/workflows/docker-snapshot-release.yml b/.github/workflows/docker-snapshot-release.yml index 7c6d7c100..e4eef8337 100644 --- a/.github/workflows/docker-snapshot-release.yml +++ b/.github/workflows/docker-snapshot-release.yml @@ -59,6 +59,8 @@ jobs: path: basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/main/docker - name: submodel-registry-log-mongodb path: basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/main/docker + - name: digitaltwinregistry + path: basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component steps: - name: Checkout Code diff --git a/basyx.aasdigitaltwinregistry/README.md b/basyx.aasdigitaltwinregistry/README.md new file mode 100644 index 000000000..3f155b0c3 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/README.md @@ -0,0 +1,151 @@ + + +# BaSyx Digital Twin Registry + +## Overview +The **Digital Twin Registry** serves as a combined module that merges the capabilities of `AASRegistry` and `AASDiscovery`. +When a client calls the `/shell-description` endpoint, the module dynamically constructs both an `AssetAdministrationShellDescriptor` and an `aasDiscoveryDocumentEntity`. + +This dual-output ensures that the asset shell becomes immediately discoverable and accessible, blending registry and discovery functionalities in a seamless operation. + +--- + +## How It Works + +- **Endpoint Integration** + + A single REST endpoint (`/shell-description`) triggers the generation of: + - An **AAS Descriptor**, representing the asset's metadata and management interface. + - A **Discovery Document**, enabling other components to locate or resolve the AAS. + +- **Unified Workflow** + By combining `AASRegistry` and `AASDiscovery`, the module streamlines the typical sequential two-step — *discover then retrieve* — into a single integrated operation. + +--- + +## Module Structure in the BaSyx SDK + +- **New Module Introduction** + Within the main BaSyx SDK, a new module — `digitaltwinregistry` — has been introduced. + It follows the **decorator pattern**, meaning it wraps around existing functionality to extend behavior without modifying original code. + +- **Delegate-Based Design** + At its core, the module implements or creates a **delegate** for the `ShellDescriptorsApiDelegate` interface. + This delegate intercepts API calls (particularly related to shell descriptions) and injects the registry-and-discovery logic — making the module effectively pluggable and maintainable. + +--- + +## Summary + +In essence, the **Digital Twin Registry module**: + +- Combines **registry** and **discovery** into a unified action via `/shell-description`. +- Is implemented as a **decorator delegate** (`ShellDescriptorsApiDelegate`), making it both modular and maintainable. +- Seamlessly integrates with existing BaSyx storage options and aligns with broader architectural goals, such as centralized registries, tagging, and scalable discovery. + +## Environment +This document describes the environment variables used to configure the BaSyx Digital Twin Registry application. The application supports multiple profiles with different storage backends. + +--- + +## Configuration Files +The application uses three YAML configuration files: + +- `application.yml` - Base configuration +- `application-InMemory.yml` - In-memory storage profile +- `application-MongoDB.yml` - MongoDB storage profile + +--- + +## Environment Variables + +### Base Configuration (`application.yml`) + +| Environment Variable | Default Value | Description | +|-----------------------|---------------|-------------| +| `SPRING_PROFILE` | MongoDB | Active Spring profile (`InMemory` or `MongoDB`) | +| `LOGGING_LEVEL` | INFO | Logging level for root and BaSyx components | + +--- + +### Server Configuration + +| Property | Default Value | Description | +|----------------|---------------|-------------| +| `server.port` | 8081 | HTTP server port | + +--- + +### CORS Configuration + +| Property | Value | Description | +|--------------------------------|-----------------------------------------------------|-------------| +| `basyx.cors.allowed-methods` | GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD | Allowed HTTP methods | +| `basyx.cors.allowed-origins` | * | Allowed origins (CORS) | + +--- + +### Management Endpoints + +| Property | Value | Description | +|------------------------------------------|------------------------------|-------------| +| `management.endpoints.web.exposure.include` | health,metrics,mappings | Exposed actuator endpoints | + +--- + +### SpringDoc/Swagger Configuration + +| Property | Value | Description | +|-----------------------------------|--------------------|-------------| +| `springdoc.api-docs.enabled` | true | Enable API documentation | +| `springdoc.swagger-ui.enabled` | true | Enable Swagger UI | +| `springdoc.swagger-ui.path` | /swagger-ui.html | Swagger UI path | +| `springdoc.swagger-ui.csrf.enabled` | false | Disable CSRF protection for Swagger | + +--- + +## InMemory Profile Configuration + +**Profile Name:** `InMemory` + +### Environment Variables +_No additional environment variables required for InMemory profile._ + +### Configuration Properties + +| Property | Value | Description | +|---------------------|------------|-------------| +| `basyx.backend` | InMemory | Use in-memory storage backend | +| `registry.type` | InMemory | Registry type | +| `registry.discovery.enabled` | true | Enable discovery service | + +### Auto-configuration Exclusions +The InMemory profile excludes MongoDB auto-configuration: + +- `org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration` +- `org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration` + +--- + +## MongoDB Profile Configuration + +**Profile Name:** `MongoDB` + +### Environment Variables + +| Environment Variable | Default Value | Description | +|---------------------------|-----------------------------------------|-------------| +| `AUTHENTICATION_DATABASE` | aasregistry | MongoDB authentication database name | +| `DATABASE_HOST` | localhost | MongoDB host address | +| `DATABASE_PORT` | localhost | MongoDB port (**Note:** should be a numeric port) | +| `DATABASE_USERNAME` | smartsystemhub | MongoDB username | +| `DATABASE_PASSWORD` | smartsystemshubdatabaseforfactoryX | MongoDB password | + +### Configuration Properties + +| Property | Value | Description | +|---------------------|---------|-------------| +| `basyx.backend` | MongoDB | Use MongoDB storage backend | +| `registry.type` | MongoDB | Registry type | +| `registry.discovery.enabled` | true | Enable discovery service | +| `basyx.aasdiscoveryservice.mongodb.collectionName` | aasregistry | MongoDB collection name | \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/Dockerfile b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/Dockerfile new file mode 100644 index 000000000..28a25d9fd --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/Dockerfile @@ -0,0 +1,22 @@ +FROM eclipse-temurin:17 +USER nobody +WORKDIR /application +ARG JAVA_OPTS +ENV JAVA_OPTS=$JAVA_OPTS +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} basyxExecutable.jar + +COPY src/main/resources/application.yml application.yml +COPY src/main/resources/application-MongoDB.yml application-MongoDB.yml +COPY src/main/resources/application-InMemory.yml application-InMemory.yml + +ARG PORT=8081 +ENV SERVER_PORT=${PORT} +ARG CONTEXT_PATH=/ +ENV SERVER_SERVLET_CONTEXT_PATH=${CONTEXT_PATH} +EXPOSE ${SERVER_PORT} + +HEALTHCHECK --interval=30s --timeout=3s --retries=3 --start-period=15s \ + CMD curl --fail http://localhost:${SERVER_PORT}${SERVER_SERVLET_CONTEXT_PATH%/}/actuator/health || exit 1 + +ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar basyxExecutable.jar \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/pom.xml b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/pom.xml new file mode 100644 index 000000000..082d66394 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/pom.xml @@ -0,0 +1,224 @@ + + + 4.0.0 + + + org.eclipse.digitaltwin.basyx + aasdigitaltwinregistry + ${revision} + + + basyx.digitaltwinregistry.component + BaSyx Digital Twin Registry Component + BaSyx Digital Twin Registry Component + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-client-native + ${revision} + + + org.eclipse.digitaltwin.basyx + aasregistry-feature-discovery-integration + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-authorization + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-hierarchy + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-hierarchy-example + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-paths + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-plugins + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-basemodel + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-basetests + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-inmemory-storage + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-kafka-events + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-mongodb-storage + ${revision} + + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-release-kafka-mem + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-release-kafka-mongodb + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-release-log-mem + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service-release-log-mongodb + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice.component + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-http + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-core + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-backend-mongodb + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-backend + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-feature-authorization + ${revision} + + + + org.eclipse.digitaltwin.aas4j + aas4j-dataformat-xml + + + org.eclipse.digitaltwin.aas4j + aas4j-dataformat-aasx + + + org.eclipse.digitaltwin.aas4j + aas4j-model + + + + org.xmlunit + xmlunit-core + + + org.xmlunit + xmlunit-matchers + + + + com.fasterxml.woodstox + woodstox-core + 7.1.1 + + + org.codehaus.woodstox + stax2-api + 4.2.2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-client + + + org.eclipse.digitaltwin.basyx + aasregistry-feature-discovery-integration + 2.0.0-SNAPSHOT + compile + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.DigitalTwinRegistry + + + + + repackage + + + + + + + \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/DigitalTwinRegistry.java b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/DigitalTwinRegistry.java new file mode 100644 index 000000000..e36029bb2 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/DigitalTwinRegistry.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.digitaltwinregistry.component; + +import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.component.AasDiscoveryServiceComponent; +import org.eclipse.digitaltwin.basyx.aasregistry.service.api.*; +import org.eclipse.digitaltwin.basyx.aasregistry.service.configuration.HomeController; +import org.eclipse.digitaltwin.basyx.aasregistry.service.events.RegistryEventLogSink; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +@SpringBootApplication + +@ComponentScan( + basePackages = { + "org.eclipse.digitaltwin.basyx" + }, + excludeFilters = { + + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = BasyxSearchApiDelegate.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = BasyxDescriptionApiDelegate.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = BasyxRegistryApiDelegate.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = org.eclipse.digitaltwin.basyx.aasregistry.service.api.DescriptionApiController.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = RegistryEventLogSink.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = org.eclipse.digitaltwin.basyx.aasregistry.service.api.DescriptionApi.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = org.eclipse.digitaltwin.basyx.http.description.DescriptionController.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = DescriptionApiDelegate.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = SearchApiController.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = ShellDescriptorsApiController.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = HomeController.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = AasDiscoveryServiceComponent.class + ), + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = org.eclipse.digitaltwin.basyx.aasregistry.service.configuration.SpringDocConfiguration.class + ), + + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = org.eclipse.digitaltwin.basyx.aasdiscoveryservice.http.documentation.AasDiscoveryServiceApiDocumentationConfiguration.class + ) + } +) +public class DigitalTwinRegistry { + public static void main(String[] args) { + SpringApplication.run(DigitalTwinRegistry.class, args); + } +} \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/controllerAdvice/GlobalExceptionHandler.java b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/controllerAdvice/GlobalExceptionHandler.java new file mode 100644 index 000000000..ca22a60e0 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/controllerAdvice/GlobalExceptionHandler.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.controllerAdvice; + +import java.time.OffsetDateTime; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.digitaltwin.basyx.aasregistry.model.Message; +import org.eclipse.digitaltwin.basyx.aasregistry.model.Result; +import org.eclipse.digitaltwin.basyx.aasregistry.model.Message.MessageTypeEnum; +import org.eclipse.digitaltwin.basyx.core.exceptions.AssetLinkDoesNotExistException; +import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingAssetLinkException; +import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException; +import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; +import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException; +import org.eclipse.digitaltwin.basyx.core.exceptions.FeatureNotSupportedException; +import org.eclipse.digitaltwin.basyx.core.exceptions.FileDoesNotExistException; +import org.eclipse.digitaltwin.basyx.core.exceptions.IdentificationMismatchException; +import org.eclipse.digitaltwin.basyx.core.exceptions.InsufficientPermissionException; +import org.eclipse.digitaltwin.basyx.core.exceptions.MissingIdentifierException; +import org.eclipse.digitaltwin.basyx.core.exceptions.NotInvokableException; +import org.eclipse.digitaltwin.basyx.core.exceptions.NullSubjectException; +import org.eclipse.digitaltwin.basyx.core.exceptions.OperationDelegationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.server.ResponseStatusException; + +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { + Result result = new Result(); + OffsetDateTime timestamp = OffsetDateTime.now(); + String reason = HttpStatus.BAD_REQUEST.getReasonPhrase(); + for (ObjectError error : ex.getAllErrors()) { + result.addMessagesItem(new Message().code(reason).messageType(MessageTypeEnum.EXCEPTION).text(error.toString()).timestamp(timestamp)); + } + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleExceptions(ResponseStatusException ex) { + return newResultEntity(ex, HttpStatus.valueOf(ex.getStatusCode().value())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleExceptions(Exception ex) { + return newResultEntity(ex, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(ElementDoesNotExistException.class) + public ResponseEntity handleElementNotFoundException(ElementDoesNotExistException exception, WebRequest request) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(AssetLinkDoesNotExistException.class) + public ResponseEntity handleElementNotFoundException(AssetLinkDoesNotExistException exception, WebRequest request) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(FileDoesNotExistException.class) + public ResponseEntity handleElementNotFoundException(FileDoesNotExistException exception, WebRequest request) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(CollidingIdentifierException.class) + public ResponseEntity handleCollidingIdentifierException(CollidingIdentifierException exception, WebRequest request) { + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + + @ExceptionHandler(MissingIdentifierException.class) + public ResponseEntity handleMissingIdentifierException(MissingIdentifierException exception, WebRequest request) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(CollidingAssetLinkException.class) + public ResponseEntity handleCollidingIdentifierException(CollidingAssetLinkException exception, WebRequest request) { + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException exception) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(IdentificationMismatchException.class) + public ResponseEntity handleIdMismatchException(IdentificationMismatchException exception) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(FeatureNotSupportedException.class) + public ResponseEntity handleFeatureNotSupportedException(FeatureNotSupportedException exception) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + + @ExceptionHandler(NotInvokableException.class) + public ResponseEntity handleNotInvokableException(NotInvokableException exception) { + return new ResponseEntity<>(HttpStatus.METHOD_NOT_ALLOWED); + } + + @ExceptionHandler(ElementNotAFileException.class) + public ResponseEntity handleElementNotAFileException(ElementNotAFileException exception) { + return new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED); + } + + @ExceptionHandler(InsufficientPermissionException.class) + public ResponseEntity handleInsufficientPermissionException(InsufficientPermissionException exception, WebRequest request) { + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(NullSubjectException.class) + public ResponseEntity handleNullSubjectException(NullSubjectException exception) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(OperationDelegationException.class) + public ResponseEntity handleNullSubjectException(OperationDelegationException exception) { + return new ResponseEntity<>(HttpStatus.FAILED_DEPENDENCY); + } + + private ResponseEntity newResultEntity(Exception ex, HttpStatus status) { + log.info("Application went into exception {}", ex.getLocalizedMessage()); + Result result = new Result(); + Message message = newExceptionMessage(ex.getMessage(), status); + result.addMessagesItem(message); + + return ResponseEntity + .status(status) + .contentType(MediaType.APPLICATION_JSON) + .body(result); + } + + + private Message newExceptionMessage(String msg, HttpStatus status) { + Message message = new Message(); + message.setCode("" + status.value()); + message.setMessageType(MessageTypeEnum.EXCEPTION); + message.setTimestamp(OffsetDateTime.now()); + message.setText(msg); + return message; + } +} \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/documentation/DTRegistryApiDocumentationConfiguration.java b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/documentation/DTRegistryApiDocumentationConfiguration.java new file mode 100644 index 000000000..65142dec8 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/documentation/DTRegistryApiDocumentationConfiguration.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.documentation; + +import io.swagger.v3.oas.models.OpenAPI; +import org.eclipse.digitaltwin.basyx.http.documentation.RepositoryApiDocumentationConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Primary; + +@Configuration +public class DTRegistryApiDocumentationConfiguration extends RepositoryApiDocumentationConfiguration { + + private static final String TITLE = "BaSyx Digital Twin Registry"; + private static final String DESCRIPTION = "BaSyx Digital Twin Registry API"; + + @Bean + @Primary + public OpenAPI customOpenAPI() { + return new OpenAPI().info(apiInfo()); + } + + @Override + protected Info apiInfo() { + return new Info().title(TITLE) + .description(DESCRIPTION) + .version(VERSION) + .contact(apiContact()) + .license(apiLicence()); + } +} \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application-InMemory.yml b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application-InMemory.yml new file mode 100644 index 000000000..b661f2987 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application-InMemory.yml @@ -0,0 +1,17 @@ +spring: + config: + activate: + on-profile: InMemory + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration + - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration + + +basyx: + backend: InMemory + +registry: + type: InMemory + discovery: + enabled: true \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application-MongoDB.yml b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application-MongoDB.yml new file mode 100644 index 000000000..2a14ad337 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application-MongoDB.yml @@ -0,0 +1,25 @@ +spring: + config: + activate: + on-profile: MongoDB + data: + mongodb: + authentication-database: ${AUTHENTICATION_DATABASE:db-name} + database: ${AUTHENTICATION_DATABASE:db-name} + host: ${DATABASE_HOST:localhost} + port: ${DATABASE_PORT:27017} + username: ${DATABASE_USERNAME:db-username} + password: ${DATABASE_PASSWORD:db-password} + +basyx: + backend: MongoDB + aasdiscoveryservice: + mongodb: + collectionName: ${AUTHENTICATION_DATABASE:db-name} + + + +registry: + type: MongoDB + discovery: + enabled: true \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application.yml b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application.yml new file mode 100644 index 000000000..c84a8fafc --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/main/resources/application.yml @@ -0,0 +1,48 @@ +basyx: + aasdiscoveryservice: + cors: + allowed-methods: GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD + allowed-origins: '*' + + + aasregistry: + feature: + discoveryintegration: + enabled: true + baseUrl: http://localhost:8081 + + endpoints: + web: + exposure: + include: health,metrics,mappings + +server: + port: 8081 + +spring: + profiles: + active: logEvents,${SPRING_PROFILE:InMemory} + main: + allow-bean-definition-overriding: true + application: + name: BaSyx Digital Twin Registry + + +description: + profiles: https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRegistryServiceSpecification/SSP-001, + https://admin-shell.io/aas/API/3/0/AssetAdministrationShellRegistryServiceSpecification/SSP-002, + https://admin-shell.io/aas/API/3/0/DiscoveryServiceSpecification/SSP-001 + +logging: + level: + root: ${LOGGING_LEVEL:INFO} + org.eclipse.digitaltwin.basyx: ${LOGGING_LEVEL:INFO} + +springdoc: + api-docs: + enabled: true + swagger-ui: + enabled: true + path: /swagger-ui.html + csrf: + enabled: false \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/DigitalTwinRegistryTests.java b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/DigitalTwinRegistryTests.java new file mode 100644 index 000000000..4173480e9 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/DigitalTwinRegistryTests.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.tests; + +import org.junit.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class DigitalTwinRegistryTests { + + @Test + public void contextLoads() {} +} diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/controllerAdvice/GlobalExceptionHandlerTest.java b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/controllerAdvice/GlobalExceptionHandlerTest.java new file mode 100644 index 000000000..c1fa80223 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/controllerAdvice/GlobalExceptionHandlerTest.java @@ -0,0 +1,270 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.tests.controllerAdvice; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.controllerAdvice.GlobalExceptionHandler; +import org.eclipse.digitaltwin.basyx.aasregistry.model.Message; +import org.eclipse.digitaltwin.basyx.aasregistry.model.Result; +import org.eclipse.digitaltwin.basyx.aasregistry.model.Message.MessageTypeEnum; +import org.eclipse.digitaltwin.basyx.core.exceptions.AssetLinkDoesNotExistException; +import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingAssetLinkException; +import org.eclipse.digitaltwin.basyx.core.exceptions.CollidingIdentifierException; +import org.eclipse.digitaltwin.basyx.core.exceptions.ElementDoesNotExistException; +import org.eclipse.digitaltwin.basyx.core.exceptions.ElementNotAFileException; +import org.eclipse.digitaltwin.basyx.core.exceptions.FeatureNotSupportedException; +import org.eclipse.digitaltwin.basyx.core.exceptions.FileDoesNotExistException; +import org.eclipse.digitaltwin.basyx.core.exceptions.IdentificationMismatchException; +import org.eclipse.digitaltwin.basyx.core.exceptions.InsufficientPermissionException; +import org.eclipse.digitaltwin.basyx.core.exceptions.MissingIdentifierException; +import org.eclipse.digitaltwin.basyx.core.exceptions.NotInvokableException; +import org.eclipse.digitaltwin.basyx.core.exceptions.NullSubjectException; +import org.eclipse.digitaltwin.basyx.core.exceptions.OperationDelegationException; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.context.request.WebRequest; + +@Slf4j +public class GlobalExceptionHandlerTest { + + private GlobalExceptionHandler exceptionHandler; + private WebRequest mockWebRequest; + + @Before + public void setUp() { + exceptionHandler = new GlobalExceptionHandler(); + mockWebRequest = mock(WebRequest.class); + } + + @Test + public void testHandleValidationException() { + log.info("Started unit test - testHandleValidationException()"); + MethodArgumentNotValidException ex = mock(MethodArgumentNotValidException.class); + ObjectError error1 = new ObjectError("object", "Error message 1"); + ObjectError error2 = new ObjectError("object", "Error message 2"); + List errors = Arrays.asList(error1, error2); + when(ex.getAllErrors()).thenReturn(errors); + ResponseEntity response = exceptionHandler.handleValidationException(ex); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertNotNull(response.getBody()); + assertNotNull(response.getBody().getMessages()); + assertEquals(2, response.getBody().getMessages().size()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleGenericException() { + log.info("Started unit test - testHandleGenericException()"); + Exception ex = new Exception("Generic error"); + ResponseEntity response = exceptionHandler.handleExceptions(ex); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); + assertResultHasExceptionMessage(response.getBody(), "Generic error"); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleElementDoesNotExistException() { + log.info("Started unit test - testHandleElementDoesNotExistException()"); + ElementDoesNotExistException ex = new ElementDoesNotExistException("Element not found"); + ResponseEntity response = exceptionHandler.handleElementNotFoundException(ex, mockWebRequest); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleAssetLinkDoesNotExistException() { + log.info("Started unit test - testHandleAssetLinkDoesNotExistException()"); + AssetLinkDoesNotExistException ex = new AssetLinkDoesNotExistException("Asset link not found"); + ResponseEntity response = exceptionHandler.handleElementNotFoundException(ex, mockWebRequest); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleFileDoesNotExistException() { + log.info("Started unit test - testHandleFileDoesNotExistException()"); + FileDoesNotExistException ex = new FileDoesNotExistException("File not found"); + ResponseEntity response = exceptionHandler.handleElementNotFoundException(ex, mockWebRequest); + assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleCollidingIdentifierException() { + log.info("Started unit test - testHandleCollidingIdentifierException()"); + CollidingIdentifierException ex = new CollidingIdentifierException("Colliding identifier"); + ResponseEntity response = exceptionHandler.handleCollidingIdentifierException(ex, mockWebRequest); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleMissingIdentifierException() { + log.info("Started unit test - testHandleMissingIdentifierException()"); + MissingIdentifierException ex = new MissingIdentifierException("Missing identifier"); + ResponseEntity response = exceptionHandler.handleMissingIdentifierException(ex, mockWebRequest); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleCollidingAssetLinkException() { + log.info("Started unit test - testHandleCollidingAssetLinkException()"); + CollidingAssetLinkException ex = new CollidingAssetLinkException("Colliding asset link"); + ResponseEntity response = exceptionHandler.handleCollidingIdentifierException(ex, mockWebRequest); + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleIllegalArgumentException() { + log.info("Started unit test - testHandleIllegalArgumentException()"); + IllegalArgumentException ex = new IllegalArgumentException("Illegal argument"); + ResponseEntity response = exceptionHandler.handleIllegalArgumentException(ex); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleIdentificationMismatchException() { + log.info("Started unit test - testHandleIdentificationMismatchException()"); + IdentificationMismatchException ex = new IdentificationMismatchException("ID mismatch"); + ResponseEntity response = exceptionHandler.handleIdMismatchException(ex); + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleFeatureNotSupportedException() { + log.info("Started unit test - testHandleFeatureNotSupportedException()"); + FeatureNotSupportedException ex = new FeatureNotSupportedException("Feature not supported"); + ResponseEntity response = exceptionHandler.handleFeatureNotSupportedException(ex); + assertEquals(HttpStatus.NOT_IMPLEMENTED, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleNotInvokableException() { + log.info("Started unit test - testHandleNotInvokableException()"); + NotInvokableException ex = new NotInvokableException("Not invokable"); + ResponseEntity response = exceptionHandler.handleNotInvokableException(ex); + assertEquals(HttpStatus.METHOD_NOT_ALLOWED, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleElementNotAFileException() { + log.info("Started unit test - testHandleElementNotAFileException()"); + ElementNotAFileException ex = new ElementNotAFileException("Element not a file"); + ResponseEntity response = exceptionHandler.handleElementNotAFileException(ex); + assertEquals(HttpStatus.PRECONDITION_FAILED, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleInsufficientPermissionException() { + log.info("Started unit test - testHandleInsufficientPermissionException()"); + InsufficientPermissionException ex = new InsufficientPermissionException("Insufficient permission"); + ResponseEntity response = exceptionHandler.handleInsufficientPermissionException(ex, mockWebRequest); + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleNullSubjectException() { + log.info("Started unit test - testHandleNullSubjectException()"); + NullSubjectException ex = new NullSubjectException("Null subject"); + ResponseEntity response = exceptionHandler.handleNullSubjectException(ex); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testHandleOperationDelegationException() { + log.info("Started unit test - testHandleOperationDelegationException()"); + OperationDelegationException ex = new OperationDelegationException("Operation delegation failed"); + ResponseEntity response = exceptionHandler.handleNullSubjectException(ex); + assertEquals(HttpStatus.FAILED_DEPENDENCY, response.getStatusCode()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testNewExceptionMessage() throws Exception { + log.info("Started unit test - testNewExceptionMessage()"); + String errorMessage = "Test error message"; + HttpStatus status = HttpStatus.BAD_REQUEST; + + java.lang.reflect.Method method = GlobalExceptionHandler.class.getDeclaredMethod("newExceptionMessage", String.class, HttpStatus.class); + method.setAccessible(true); + + Message message = (Message) method.invoke(exceptionHandler, errorMessage, status); + assertNotNull(message); + assertEquals(String.valueOf(status.value()), message.getCode()); + assertEquals(MessageTypeEnum.EXCEPTION, message.getMessageType()); + assertEquals(errorMessage, message.getText()); + assertNotNull(message.getTimestamp()); + log.info("Unit test conducted successfully"); + } + + @Test + public void testNewResultEntity() throws Exception { + log.info("Started unit test - testNewResultEntity()"); + Exception ex = new Exception("Test exception"); + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + + java.lang.reflect.Method method = GlobalExceptionHandler.class.getDeclaredMethod("newResultEntity", Exception.class, HttpStatus.class); + method.setAccessible(true); + + ResponseEntity response = (ResponseEntity) method.invoke(exceptionHandler, ex, status); + assertEquals(status, response.getStatusCode()); + assertNotNull(response.getBody()); + assertResultHasExceptionMessage(response.getBody(), "Test exception"); + log.info("Unit test conducted successfully"); + } + + private void assertResultHasExceptionMessage(Result result, String expectedMessage) { + assertNotNull(result); + assertNotNull(result.getMessages()); + assertEquals(1, result.getMessages().size()); + Message message = result.getMessages().get(0); + assertEquals(expectedMessage, message.getText()); + assertEquals(MessageTypeEnum.EXCEPTION, message.getMessageType()); + assertNotNull(message.getTimestamp()); + } +} \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/documentation/DTRegistryApiDocumentationConfigurationTest.java b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/documentation/DTRegistryApiDocumentationConfigurationTest.java new file mode 100644 index 000000000..4886720be --- /dev/null +++ b/basyx.aasdigitaltwinregistry/basyx.digitaltwinregistry.component/src/test/java/org/eclipse/digitaltwin/basyx/digitaltwinregistry/component/tests/documentation/DTRegistryApiDocumentationConfigurationTest.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.tests.documentation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.digitaltwin.basyx.digitaltwinregistry.component.documentation.DTRegistryApiDocumentationConfiguration; +import org.junit.Before; +import org.junit.Test; + +import io.swagger.v3.oas.models.OpenAPI; + +@Slf4j +public class DTRegistryApiDocumentationConfigurationTest { + + private DTRegistryApiDocumentationConfiguration config; + + @Before + public void setUp() { + config = new DTRegistryApiDocumentationConfiguration(); + } + + @Test + public void testCustomOpenAPI() { + log.info("Started unit test - testCustomOpenAPI()"); + OpenAPI openAPI = config.customOpenAPI(); + + assertNotNull(openAPI); + assertNotNull(openAPI.getInfo()); + assertEquals("BaSyx Digital Twin Registry", openAPI.getInfo().getTitle()); + assertEquals("BaSyx Digital Twin Registry API", openAPI.getInfo().getDescription()); + log.info("Successfully conducted unit test"); + } + + @Test + public void testBeanPrimaryAnnotation() throws NoSuchMethodException { + log.info("Started unit test - testBeanPrimaryAnnotation()"); + java.lang.reflect.Method method = DTRegistryApiDocumentationConfiguration.class.getMethod("customOpenAPI"); + boolean hasPrimaryAnnotation = method.isAnnotationPresent(org.springframework.context.annotation.Primary.class); + assertTrue("customOpenAPI method should have @Primary annotation", hasPrimaryAnnotation); + log.info("Successfully conducted unit test"); + } + + @Test + public void testConfigurationAnnotation() { + log.info("Started unit test - testConfigurationAnnotation()"); + boolean hasConfigurationAnnotation = config.getClass().isAnnotationPresent(org.springframework.context.annotation.Configuration.class); + assertTrue("Class should have @Configuration annotation", hasConfigurationAnnotation); + log.info("Successfully conducted unit test"); + } +} \ No newline at end of file diff --git a/basyx.aasdigitaltwinregistry/pom.xml b/basyx.aasdigitaltwinregistry/pom.xml new file mode 100644 index 000000000..5910eae25 --- /dev/null +++ b/basyx.aasdigitaltwinregistry/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + org.eclipse.digitaltwin.basyx + basyx.parent + ${revision} + + + aasdigitaltwinregistry + BaSyx Digital Twin Registry + BaSyx Digital Twin Registry + pom + + + basyx.digitaltwinregistry.component + + + \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/pom.xml b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/pom.xml new file mode 100644 index 000000000..74a062133 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/pom.xml @@ -0,0 +1,58 @@ + + 4.0.0 + + org.eclipse.digitaltwin.basyx + basyx.aasregistry + ${revision} + + + aasregistry-feature-discovery-integration + BaSyx aasRegistry feature-discovery-integration + BaSyx aasRegistry feature-discovery-integration + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-service + ${revision} + + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-client + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasdiscoveryservice-core + ${revision} + + + org.eclipse.digitaltwin.basyx + basyx.aasregistry-feature-hierarchy + ${revision} + true + + + + org.projectlombok + lombok + + + + org.springframework.boot + spring-boot-starter-test + test + + + \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistry.java b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistry.java new file mode 100644 index 000000000..e00e450eb --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistry.java @@ -0,0 +1,141 @@ + +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasregistry.feature.discovery.integration; + +import jakarta.validation.Valid; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.digitaltwin.aas4j.v3.model.SpecificAssetId; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSpecificAssetId; +import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.core.AasDiscoveryService; +import org.eclipse.digitaltwin.basyx.aasregistry.model.*; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.AasDescriptorAlreadyExistsException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.AasDescriptorNotFoundException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.SubmodelAlreadyExistsException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.SubmodelNotFoundException; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.DescriptorFilter; +import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; + +import java.util.Base64; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class DiscoveryIntegrationAasRegistry implements AasRegistryStorage { + + private final AasRegistryStorage decorated; + + private final AasDiscoveryService discoveryApi; + + public DiscoveryIntegrationAasRegistry(AasDiscoveryService discoveryApi, AasRegistryStorage decorated) { + this.discoveryApi = discoveryApi; + this.decorated = decorated; + } + + @Override + public CursorResult> getAllAasDescriptors(@NonNull PaginationInfo pRequest, @NonNull DescriptorFilter filter) { + return decorated.getAllAasDescriptors(pRequest, filter); + } + + @Override + public AssetAdministrationShellDescriptor getAasDescriptor(@NonNull String aasDescriptorId) throws AasDescriptorNotFoundException { + return decorated.getAasDescriptor(aasDescriptorId); + } + + @Override + public void insertAasDescriptor(AssetAdministrationShellDescriptor descr) throws AasDescriptorAlreadyExistsException { + decorated.insertAasDescriptor(descr); + @Valid List ids = descr.getSpecificAssetIds(); + List specificAssetIds = ids.stream() + .map(rId -> { + SpecificAssetId assetId = new DefaultSpecificAssetId(); + assetId.setName(rId.getName()); + assetId.setValue(rId.getValue()); + return assetId; + }).collect(Collectors.toList()); + discoveryApi.createAllAssetLinksById(descr.getId(), specificAssetIds); + } + + @Override + public void replaceAasDescriptor(@NonNull String aasDescriptorId, @NonNull AssetAdministrationShellDescriptor descriptor) throws AasDescriptorNotFoundException { + decorated.replaceAasDescriptor(aasDescriptorId, descriptor); + List specificAssetIds = descriptor.getSpecificAssetIds().stream() + .map(rId -> { + SpecificAssetId assetId = new DefaultSpecificAssetId(); + assetId.setName(rId.getName()); + assetId.setValue(rId.getValue()); + return assetId; + }).collect(Collectors.toList()); + + discoveryApi.deleteAllAssetLinksById(aasDescriptorId); + discoveryApi.createAllAssetLinksById(aasDescriptorId, specificAssetIds); + } + + @Override + public void removeAasDescriptor(@NonNull String aasDescriptorId) throws AasDescriptorNotFoundException { + decorated.removeAasDescriptor(aasDescriptorId); + discoveryApi.deleteAllAssetLinksById(aasDescriptorId); + } + + @Override + public CursorResult> getAllSubmodels(@NonNull String aasDescriptorId, @NonNull PaginationInfo pRequest) throws AasDescriptorNotFoundException { + return decorated.getAllSubmodels(aasDescriptorId, pRequest); + } + + @Override + public SubmodelDescriptor getSubmodel(@NonNull String aasDescriptorId, @NonNull String submodelId) throws AasDescriptorNotFoundException, SubmodelNotFoundException { + return decorated.getSubmodel(aasDescriptorId,submodelId); + } + + @Override + public void insertSubmodel(@NonNull String aasDescriptorId, @NonNull SubmodelDescriptor submodel) throws AasDescriptorNotFoundException, SubmodelAlreadyExistsException { + decorated.insertSubmodel(aasDescriptorId,submodel); + } + + @Override + public void replaceSubmodel(@NonNull String aasDescriptorId, @NonNull String submodelId, @NonNull SubmodelDescriptor submodel) throws AasDescriptorNotFoundException, SubmodelNotFoundException { + decorated.replaceSubmodel(aasDescriptorId, submodelId, submodel); + } + + @Override + public void removeSubmodel(@NonNull String aasDescriptorId, @NonNull String submodelId) throws AasDescriptorNotFoundException, SubmodelNotFoundException { + decorated.removeSubmodel(aasDescriptorId, submodelId); + } + + @Override + public Set clear() { + return decorated.clear(); + } + + @Override + public ShellDescriptorSearchResponse searchAasDescriptors(@NonNull ShellDescriptorSearchRequest request) { + return decorated.searchAasDescriptors(request); + } +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryConfiguration.java b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryConfiguration.java new file mode 100644 index 000000000..7c3e1e5b4 --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryConfiguration.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasregistry.feature.discovery.integration; + +import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.client.ConnectedAasDiscoveryService; +import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.core.AasDiscoveryService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(name = "basyx.aasregistry.feature.discoveryintegration.enabled", havingValue = "true", matchIfMissing = false) +public class DiscoveryIntegrationAasRegistryConfiguration { + + @Value("${basyx.aasregistry.feature.discoveryintegration.baseUrl:#{null}}") + private String discoveryBasePath; + + @Bean + public AasDiscoveryService getConnectedAasDiscoveryService() { + return new ConnectedAasDiscoveryService(discoveryBasePath); + } +} diff --git a/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryFeature.java b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryFeature.java new file mode 100644 index 000000000..656fe730c --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryFeature.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasregistry.feature.discovery.integration; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.core.AasDiscoveryService; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorageFeature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@ConditionalOnProperty(name = "basyx.aasregistry.feature.discoveryintegration.enabled", havingValue = "true", matchIfMissing = false) +public class DiscoveryIntegrationAasRegistryFeature implements AasRegistryStorageFeature { + + @Value("${basyx.aasregistry.feature.discoveryintegration.baseUrl:#{null}}") + private String discoveryBaseURL; + + private final AasDiscoveryService discoveryApi; + private final ApplicationContext applicationContext; + + @Autowired + public DiscoveryIntegrationAasRegistryFeature(AasDiscoveryService discoveryApi, ApplicationContext applicationContext) { + this.discoveryApi = discoveryApi; + this.applicationContext = applicationContext; + } + + @Override + public AasRegistryStorage decorate(AasRegistryStorage decorated) { + if (decorated instanceof DiscoveryIntegrationAasRegistry) { + return decorated; + } + if (containsDiscoveryIntegrationInChain(decorated)) { + return decorated; + } + return new DiscoveryIntegrationAasRegistry(discoveryApi, decorated); + } + + private boolean containsDiscoveryIntegrationInChain(AasRegistryStorage storage) { + AasRegistryStorage current = storage; + while (current != null) { + if (current instanceof DiscoveryIntegrationAasRegistry) { + return true; + } + + try { + java.lang.reflect.Field decoratedField = current.getClass().getDeclaredField("decorated"); + decoratedField.setAccessible(true); + current = (AasRegistryStorage) decoratedField.get(current); + } catch (Exception e) { + break; + } + } + return false; + } + + @Override + public boolean isEnabled() { + return discoveryBaseURL != null && !discoveryBaseURL.isBlank(); + } + + @Override + public String getName() { + return "AasRegistry Discovery Integration"; + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryTest.java b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryTest.java new file mode 100644 index 000000000..80dc5885c --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/feature/discovery/integration/DiscoveryIntegrationAasRegistryTest.java @@ -0,0 +1,235 @@ +/******************************************************************************* + * Copyright (C) 2025 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.aasregistry.feature.discovery.integration; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.digitaltwin.aas4j.v3.model.SpecificAssetId; +import org.eclipse.digitaltwin.basyx.aasdiscoveryservice.core.AasDiscoveryService; +import org.eclipse.digitaltwin.basyx.aasregistry.model.*; +import org.eclipse.digitaltwin.basyx.aasregistry.service.errors.*; +import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.AasRegistryStorage; +import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@Slf4j +class DiscoveryIntegrationAasRegistryTest { + + @Mock + private AasRegistryStorage decoratedStorage; + + @Mock + private AasDiscoveryService discoveryService; + + private DiscoveryIntegrationAasRegistry registry; + + private static final String AAS_DESCRIPTOR_ID = "testAasId"; + private static final String SUBMODEL_ID = "testSubmodelId"; + + @BeforeEach + void setUp() { + registry = new DiscoveryIntegrationAasRegistry(discoveryService, decoratedStorage); + } + + @Test + void insertAasDescriptorShouldCallDiscoveryServiceWithConvertedAssetIds() throws Exception { + log.info("Started unit test - insertAasDescriptorShouldCallDiscoveryServiceWithConvertedAssetIds()"); + AssetAdministrationShellDescriptor descriptor = createTestDescriptor(AAS_DESCRIPTOR_ID); + descriptor.setSpecificAssetIds(createRegistrySpecificAssetIds()); + + registry.insertAasDescriptor(descriptor); + + verify(decoratedStorage).insertAasDescriptor(descriptor); + + ArgumentCaptor idCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor> assetIdsCaptor = ArgumentCaptor.forClass(List.class); + verify(discoveryService).createAllAssetLinksById(idCaptor.capture(), assetIdsCaptor.capture()); + + assertEquals(AAS_DESCRIPTOR_ID, idCaptor.getValue()); + assertEquals(2, assetIdsCaptor.getValue().size()); + log.info("Successfully conducted unit test"); + } + + @Test + void replaceAasDescriptorShouldDeleteThenRecreateAssetLinks() throws Exception { + log.info("Started unit test - replaceAasDescriptorShouldDeleteThenRecreateAssetLinks()"); + AssetAdministrationShellDescriptor descriptor = createTestDescriptor(AAS_DESCRIPTOR_ID); + descriptor.setSpecificAssetIds(createRegistrySpecificAssetIds()); + + registry.replaceAasDescriptor(AAS_DESCRIPTOR_ID, descriptor); + + InOrder inOrder = inOrder(discoveryService); + verify(decoratedStorage).replaceAasDescriptor(AAS_DESCRIPTOR_ID, descriptor); + inOrder.verify(discoveryService).deleteAllAssetLinksById(AAS_DESCRIPTOR_ID); + inOrder.verify(discoveryService).createAllAssetLinksById(eq(AAS_DESCRIPTOR_ID), anyList()); + inOrder.verifyNoMoreInteractions(); + log.info("Successfully conducted unit test"); + } + + @Test + void replaceAasDescriptorWithEmptyAssetIdsShouldStillInvokeDiscovery() throws Exception { + log.info("Started unit test - replaceAasDescriptorWithEmptyAssetIdsShouldStillInvokeDiscovery()"); + AssetAdministrationShellDescriptor descriptor = createTestDescriptor(AAS_DESCRIPTOR_ID); + descriptor.setSpecificAssetIds(Collections.emptyList()); + + registry.replaceAasDescriptor(AAS_DESCRIPTOR_ID, descriptor); + + verify(discoveryService).deleteAllAssetLinksById(AAS_DESCRIPTOR_ID); + verify(discoveryService).createAllAssetLinksById(eq(AAS_DESCRIPTOR_ID), eq(Collections.emptyList())); + log.info("Successfully conducted unit test"); + } + + @Test + void removeAasDescriptorShouldDeleteAssetLinks() throws Exception { + log.info("Started unit test - removeAasDescriptorShouldDeleteAssetLinks()"); + registry.removeAasDescriptor(AAS_DESCRIPTOR_ID); + + verify(decoratedStorage).removeAasDescriptor(AAS_DESCRIPTOR_ID); + verify(discoveryService).deleteAllAssetLinksById(AAS_DESCRIPTOR_ID); + log.info("Successfully conducted unit test"); + } + + @Test + void getAasDescriptorShouldDelegateToStorageOnly() throws Exception { + log.info("Started unit test - getAasDescriptorShouldDelegateToStorageOnly()"); + AssetAdministrationShellDescriptor expected = createTestDescriptor(AAS_DESCRIPTOR_ID); + when(decoratedStorage.getAasDescriptor(AAS_DESCRIPTOR_ID)).thenReturn(expected); + + AssetAdministrationShellDescriptor result = registry.getAasDescriptor(AAS_DESCRIPTOR_ID); + + assertEquals(expected, result); + verifyNoInteractions(discoveryService); + log.info("Successfully conducted unit test"); + } + + @Test + void getAllSubmodelsShouldDelegateToStorageOnly() throws Exception { + log.info("Started unit test - getAllSubmodelsShouldDelegateToStorageOnly()"); + PaginationInfo pagination = new PaginationInfo(10, null); + CursorResult> expected = + new CursorResult<>("cursor", List.of(createTestSubmodelDescriptor(SUBMODEL_ID))); + + when(decoratedStorage.getAllSubmodels(AAS_DESCRIPTOR_ID, pagination)).thenReturn(expected); + + CursorResult> result = registry.getAllSubmodels(AAS_DESCRIPTOR_ID, pagination); + + assertEquals(expected, result); + verifyNoInteractions(discoveryService); + log.info("Successfully conducted unit test"); + } + + @Test + void insertSubmodelShouldDelegateToStorageOnly() throws Exception { + log.info("Started unit test - insertSubmodelShouldDelegateToStorageOnly()"); + SubmodelDescriptor submodel = createTestSubmodelDescriptor(SUBMODEL_ID); + + registry.insertSubmodel(AAS_DESCRIPTOR_ID, submodel); + + verify(decoratedStorage).insertSubmodel(AAS_DESCRIPTOR_ID, submodel); + verifyNoInteractions(discoveryService); + log.info("Successfully conducted unit test"); + } + + @Test + void clearShouldDelegateToStorageOnly() { + log.info("Started unit test - clearShouldDelegateToStorageOnly()"); + Set expected = Set.of("id1", "id2"); + when(decoratedStorage.clear()).thenReturn(expected); + + Set result = registry.clear(); + + assertEquals(expected, result); + verifyNoInteractions(discoveryService); + log.info("Successfully conducted unit test"); + } + + @Test + void searchAasDescriptorsShouldDelegateToStorageOnly() { + log.info("Started unit test - searchAasDescriptorsShouldDelegateToStorageOnly()"); + ShellDescriptorSearchRequest request = new ShellDescriptorSearchRequest(); + ShellDescriptorSearchResponse expected = new ShellDescriptorSearchResponse(); + when(decoratedStorage.searchAasDescriptors(request)).thenReturn(expected); + + ShellDescriptorSearchResponse result = registry.searchAasDescriptors(request); + + assertEquals(expected, result); + verifyNoInteractions(discoveryService); + log.info("Successfully conducted unit test"); + } + + @Test + void insertAasDescriptorShouldPropagateStorageException() throws Exception { + log.info("Started unit test - insertAasDescriptorShouldPropagateStorageException()"); + AssetAdministrationShellDescriptor descriptor = createTestDescriptor(AAS_DESCRIPTOR_ID); + doThrow(new AasDescriptorAlreadyExistsException(AAS_DESCRIPTOR_ID)) + .when(decoratedStorage).insertAasDescriptor(descriptor); + + assertThrows(AasDescriptorAlreadyExistsException.class, () -> registry.insertAasDescriptor(descriptor)); + verifyNoInteractions(discoveryService); + log.info("Successfully conducted unit test"); + } + + private AssetAdministrationShellDescriptor createTestDescriptor(String id) { + AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor(); + descriptor.setId(id); + descriptor.setIdShort("testIdShort"); + descriptor.setAdministration(new AdministrativeInformation()); + return descriptor; + } + + private List createRegistrySpecificAssetIds() { + org.eclipse.digitaltwin.basyx.aasregistry.model.SpecificAssetId id1 = + new org.eclipse.digitaltwin.basyx.aasregistry.model.SpecificAssetId(); + id1.setName("assetType"); + id1.setValue("type1"); + + org.eclipse.digitaltwin.basyx.aasregistry.model.SpecificAssetId id2 = + new org.eclipse.digitaltwin.basyx.aasregistry.model.SpecificAssetId(); + id2.setName("manufacturer"); + id2.setValue("companyX"); + + return List.of(id1, id2); + } + + private SubmodelDescriptor createTestSubmodelDescriptor(String id) { + SubmodelDescriptor submodel = new SubmodelDescriptor(); + submodel.setId(id); + submodel.setIdShort("submodelShort"); + submodel.setAdministration(new AdministrativeInformation()); + return submodel; + } +} \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/test/resources/application.yml b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/test/resources/application.yml new file mode 100644 index 000000000..7dc4e513d --- /dev/null +++ b/basyx.aasregistry/basyx.aasregistry-feature-discovery-integration/src/test/resources/application.yml @@ -0,0 +1,32 @@ +basyx: + aasregistry: + feature: + discoveryintegration: + enabled: false + +spring: + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration + - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration + main: + allow-bean-definition-overriding: true + data: + mongodb: + uri: + +management: + endpoints: + web: + exposure: + include: + endpoint: + health: + enabled: false + metrics: + enabled: false + +logging: + level: + org.eclipse.digitaltwin.basyx: INFO + org.springframework: WARN \ No newline at end of file diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml index d56731eb4..ff430e3eb 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/pom.xml @@ -74,5 +74,11 @@ basyx.aasregistry-service-basetests test + + org.eclipse.digitaltwin.basyx + aasregistry-feature-discovery-integration + test + ${revision} + diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml index e8b7a7201..b2cfb34b5 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/pom.xml @@ -81,5 +81,11 @@ elasticsearch-java 9.1.4 + + org.eclipse.digitaltwin.basyx + aasregistry-feature-discovery-integration + test + ${revision} + diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml index 8fdef0705..e17a4f67b 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/pom.xml @@ -71,6 +71,11 @@ spring-boot-starter-test test - + + org.eclipse.digitaltwin.basyx + aasregistry-feature-discovery-integration + test + ${revision} + diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml index 34ed77bf1..861771f42 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/pom.xml @@ -37,7 +37,6 @@ org.eclipse.digitaltwin.basyx basyx.aasregistry-service-mongodb-storage - org.eclipse.digitaltwin.basyx basyx.aasregistry-feature-search @@ -72,6 +71,11 @@ elasticsearch-java 9.1.4 + + org.eclipse.digitaltwin.basyx + aasregistry-feature-discovery-integration + test + ${revision} + - - + \ No newline at end of file diff --git a/basyx.aasregistry/pom.xml b/basyx.aasregistry/pom.xml index ac15e4d62..350078a07 100644 --- a/basyx.aasregistry/pom.xml +++ b/basyx.aasregistry/pom.xml @@ -58,6 +58,7 @@ basyx.aasregistry-feature-hierarchy basyx.aasregistry-feature-hierarchy-example basyx.aasregistry-feature-search + basyx.aasregistry-feature-discovery-integration @@ -339,4 +340,4 @@ - + \ No newline at end of file diff --git a/pom.xml b/pom.xml index a3cee4467..80ce0d42b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,3 +1,4 @@ + @@ -24,6 +25,7 @@ basyx.conceptdescriptionrepository basyx.aasxfileserver basyx.aasdiscoveryservice + basyx.aasdigitaltwinregistry BaSyx Parent Parent POM for Eclipse BaSyx @@ -1585,4 +1587,4 @@ - + \ No newline at end of file